一文带你了解 Java 的内存区域

简介: 内存和存储器这两个术语均指计算机的内部存储空间。存储器包括:内部存储器(内存)、外部存储器(外存)、寄存器。

一、内存简介

内存和存储器这两个术语均指计算机的内部存储空间。存储器包括:内部存储器(内存)、外部存储器(外存)、寄存器。


内存是应用程序在处理过程中放置其使用的数据的地方。内存包括:只读存储器(ROM,Read Only memory)(只读,断电后数据保留)、随机存取存储器(RAM,Random Access Memory)(主存)(内存条)(可读可写,断电后数据丢失)、高速缓冲存储器(CACHE)。


物理内存即随机存取存储器空间。


虚拟内存即硬盘一部分空间映射虚拟的内存。物理内存已满时从物理内存碎片甚至硬盘按需取用空间。虚拟内存对应的存储文件 pagefile.sys 在系统盘根目录下,默认隐藏。虚拟内存使得多个进程在同时运行时可以共享物理内存,这里的共享只是空间上共享,在逻辑上彼此仍然是隔离的。

二、分段和分页管理机制

在早期的计算机中,要运行一个程序,会把这些程序全都装入内存,程序都是直接运行在内存上的,也就是说程序中访问的内存地址都是实际的物理内存地址。


但是如果遇到同时需要运行多个应用程序的时候,操作系统的内存可能就会不太够了。而且还可能会遇到如下问题:


  • 进程地址空间不隔离
  • 内存使用效率低
  • 程序运行的地址不确定


为此,计算机科学家们设计增加一个中间层,利用一种间接的地址访问方法访问物理内存。按照这种方法,程序中访问的内存地址不再是实际的物理内存地址,而是一个虚拟地址,然后由操作系统将这个虚拟地址映射到适当的物理内存地址上。


分段的映射方法虽然解决了上述中的问题一和问题三,但并没能解决问题二,即内存的使用效率问题。在分段的映射方法中,每次换入换出内存的都是整个程序,这样会造成大量的磁盘访问操作,导致效率低下。所以这种映射方法还是稍显粗糙,粒度比较大。实际上,程序的运行有局部性特点,在某个时间段内,程序只是访问程序的一小部分数据,也就是说,程序的大部分数据在一个时间段内都不会被用到。基于这种情况,人们想到了粒度更小的内存分割和映射方法,这种方法就是分页(Paging)。


分页的基本方法是,将地址空间分成许多的页。每页的大小由 CPU 决定,然后由操作系统选择页的大小。


image.png


32 位和 64 位电脑也是指的内存:


  • 32 位处理器:即内存地址长度为 32,拥有 232 的可寻址范围,使用 32 位地址线的最大寻址空间为 2 的 32 次方 bytes,计算后即 4294967296 Bytes,也就是我们常说的 4096MB,32 位地址线的寻址空间封顶即为 4GB。
  • 64 位处理器:即内存地址长度为 64,拥有 264的可寻址范围,64 位系统使用 64 位地址线的最大寻址空间为 2 的 64 次方 bytes,计算后其可寻址空间达到了 18446744073709551616 Bytes,即 16384 PB(PebiByte)或 16777216 TB(TebiByte)。

三、地址空间的划分

一个计算通常有固定大小的内存空间,但是程序并不能使用全部的空间。因为这些空间被划分为内核空间和用户空间,而程序只能使用用户空间的内存。


  • 内核空间:主要的操作系统程序和 c 运行时空间。链接计算机硬件,提供了联网和虚拟内容逻辑的进程
  • 用户空间:Java 实际运行时空间


Java 代码启动后,有如下组建需要占用内存:


  • 堆内存:Java 堆、类和类加载器
  • 栈内存:线程
  • 本地内存:NIO、JNI

四、JVM 架构图

Java 内存模型就是指的下图中的 Runtime Data Area,运行时数据区。Java 内存模型描述了在多线程代码中哪些行为是合法的,以及线程如何通过内存进行交互。它描述了“程序中的变量“ 和 ”从内存或者寄存器获取或存储它们的底层细节”之间的关系。Java 内存模型通过使用各种各样的硬件和编译器的优化来正确实现以上事情。


image.png

运行时数据区域

JVM 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。如下图所示:


image.png


各数据区又可分为:线程独占部分和线程共享部分。

线程独占部分

线程独占部分又包括:程序计数器、虚拟机栈和本地方法栈


image.png


接下来分别介绍一下这几个部分。

程序计数器(Program Counter Register)

程序计数器(Program Counter Register) 是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。


主要特点有:


  • 当前线程所执行的字节码行号指示器(逻辑)
  • 改变计数器的值来选取下一条需要执行的字节码指令
  • 和线程是一对一的关系,即“线程私有”
  • 如果是对 Java 方法计数,此时计数器记录的是正在执行的虚拟机字节码指令的地址
  • 如果是 Native 方法则计数器值为 Undefined
  • 不会发生内存泄漏

Java 虚拟机栈

Java 虚拟机栈(Java Virtual Machine Stacks) 也是线程私有的,它的生命周期与线程相同。


每个 Java 方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储 局部变量表、操作数栈、常量池引用 等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。


虚拟机栈的特点有:


  • Java 方法执行的内存模型
  • 包含多个栈帧


Java 虚拟机是基于「栈」架构的,栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。


每个栈帧存储了:局部变量表、操作数栈、动态链接、方法的返回地址。


image.png

局部变量表和操作数栈

  • 局部变量表:32 位变量槽,存放了编译期可知的各种基本数据类型、对象引用、ReturnAddress 类型。
  • 操作数栈:入栈、出栈、复制、交换、产生消费变量。基于栈的执行引擎,虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据、执行运算,然后把结果压回操作数栈。
  • 动态链接: 每个栈帧都包含一个指向运行时常量池(方法区的一部分)中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态连接。Class 文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期间转化为直接应用,这部分称为动态链接。
  • 方法出口:返回方法被调用的位置,恢复上层方法的局部变量和操作数栈,如果无返回值,则把它压入调用者的操作数栈。


注意:

  1. 递归为什么会引发 java.lang.StackOverflowError 异常原因:递归过深,栈帧数超出虚拟栈深度,引发 Exception in thread "main" java.lang.StackOverflowError 异常
  2. 虚拟机栈过多,无法申请到足够内存会引发 java.lang.OutOfMemoryError 异常

本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈相似,主要作用于标注了 native 的方法。


二者的区别在于:虚拟机栈为 Java 方法服务;本地方法栈为 Native 方法服务。本地方法并不是用 Java 实现的,而是由 C 语言实现的。


注意:本地方法栈也会抛出 StackOverflowError 异常和 OutOfMemoryError 异常

线程共享部分

元空间(MetaSpace)与永久代(PermGen)的区别

元空间和永久代 均是方法区的实现,存放 class 相关信息,method 和 field 。方法区是一种 JVM 的规范。


JDK7 之后,原先位于方法区的字符串常量池移动到了 Java 堆中,JDK8 中元空间替代了永久代。元空间使用本地内存,而永久代使用的是 jvm 的内存


java.lang.OutOfMemoryError:PermGen space不复存在,解决了空间不足的问题。原则上,本地空间多大,元空间就可以多大。但是 jvm 会根据程序需要动态调整所需空间大小。

MetaSpace 相比 PermGen 的优势

  • 字符串常量池存在永久代中,容易出现性能问题和内存溢出
  • 类和方法的信息大小难易确定,给永久代的大小指定带来困难
  • 永久代会为 GC 带来不必要的复杂性
  • 方便 HotSpot 与其他 JVM 如 Jrockit 的集成

Java 堆(Heap)

类实例和数组存储在堆内存中。堆内存也称为共享内存。因为这是多个线程将共享相同数据的地方。


Java 堆(Java Heap) 的作用就是存放对象实例,几乎所有的对象实例都是在这里分配内存。


Java 堆是垃圾收集的主要区域(因此也被叫做"GC 堆")。现代的垃圾收集器基本都是采用分代收集算法,该算法的思想是针对不同的对象采取不同的垃圾回收算法。


因此虚拟机把 Java 堆分成以下三块:


  • 新生代(Young Generation)
  • Eden - Eden 和 Survivor 的比例为 8:1
  • From Survivor
  • To Survivor
  • 老年代(Old Generation)
  • 永久代(Permanent Generation)


当一个对象被创建时,它首先进入新生代,之后有可能被转移到老年代中。新生代存放着大量的生命很短的对象,因此新生代在三个区域中垃圾回收的频率最高。


注意:Java 堆不需要连续内存,并且可以动态扩展其内存,扩展失败会抛出 OutOfMemoryError 异常。

方法区

方法区(Method Area)也被称为永久代。方法区用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。


对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。


和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。JDK 1.7 之前,HotSpot 虚拟机把它当成永久代来进行垃圾回收。可通过参数 -XX:PermSize 和 -XX:MaxPermSize 设置。JDK 1.8 之后,取消了永久代,用 **metaspace(元数据)**区替代。可通过参数 -XX:MaxMetaspaceSize 设置。

运行时常量池

运行时常量池(Runtime Constant Pool) 是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容会在类加载后被放入这个区域。


  • 字面量 - 文本字符串、声明为 final 的常量值等。
  • 符号引用 - 类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。这部分常量也会被放入运行时常量池。

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 JVM 规范中定义的内存区域。


在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

总结

简单的归类,Java 运行时内存区域中:

  • 属于线程私有的区域是:程序计数器、Java 虚拟机栈、本地方法栈;
  • 属于线程共享的区域是:Java 堆、方法区(包括运行时常量池)。


image.png


相关文章
|
19天前
|
Java 编译器 开发者
深入理解Java内存模型(JMM)及其对并发编程的影响
【9月更文挑战第37天】在Java的世界里,内存模型是隐藏在代码背后的守护者,它默默地协调着多线程环境下的数据一致性和可见性问题。本文将揭开Java内存模型的神秘面纱,带领读者探索其对并发编程实践的深远影响。通过深入浅出的方式,我们将了解内存模型的基本概念、工作原理以及如何在实际开发中正确应用这些知识,确保程序的正确性和高效性。
|
11天前
|
缓存 算法 Java
Java中的内存管理:理解与优化
【10月更文挑战第6天】 在Java编程中,内存管理是一个至关重要的主题。本文将深入探讨Java内存模型及其垃圾回收机制,并分享一些优化内存使用的策略和最佳实践。通过掌握这些知识,您可以提高Java应用的性能和稳定性。
36 4
|
11天前
|
存储 监控 算法
Java中的内存管理:理解Garbage Collection机制
本文将深入探讨Java编程语言中的内存管理,着重介绍垃圾回收(Garbage Collection, GC)机制。通过阐述GC的工作原理、常见算法及其在Java中的应用,帮助读者提高程序的性能和稳定性。我们将从基本原理出发,逐步深入到调优实践,为开发者提供一套系统的理解和优化Java应用中内存管理的方法。
|
1月前
|
监控 算法 Java
Java中的内存管理:理解Garbage Collection机制
本文将深入探讨Java编程语言中的内存管理,特别是垃圾回收(Garbage Collection, GC)机制。我们将从基础概念开始,逐步解析垃圾回收的工作原理、不同类型的垃圾回收器以及它们在实际项目中的应用。通过实际案例,读者将能更好地理解Java应用的性能调优技巧及最佳实践。
81 0
|
4天前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理方式,特别是垃圾回收机制。我们将了解Java的自动内存管理是如何工作的,它如何帮助开发者避免常见的内存泄漏问题。通过分析不同垃圾回收算法(如标记-清除、复制和标记-整理)以及JVM如何选择合适的垃圾回收策略,本文旨在帮助Java开发者更好地理解和优化应用程序的性能。
|
6天前
|
存储 Java
Java内存模型
【10月更文挑战第11天】Java 内存模型(JMM)是 Java 虚拟机规范中定义的多线程内存访问机制,解决内存可见性、原子性和有序性问题。它定义了主内存和工作内存的概念,以及可见性、原子性和有序性的规则,确保多线程环境下的数据一致性和操作正确性。使用 `synchronized` 和 `volatile` 等同步机制可有效避免数据竞争和不一致问题。
16 3
|
6天前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
13 2
|
9天前
|
Java 数据挖掘 数据库连接
Java使用直接内存的好处
综上所述,Java直接内存的使用为开发者提供了一种绕过JVM堆限制、直接高效操作内存资源的途径,特别适用于高吞吐量、低延迟和大规模数据处理的场景。虽然直接内存的使用需要更精细的管理以避免内存泄漏和过度消耗系统资源,但恰当的利用能够显著提升应用的性能表现,是现代高性能Java应用不可或缺的工具之一。
10 2
|
10天前
|
Java 数据挖掘 数据库连接
Java使用直接内存的好处
综上所述,Java直接内存的使用为开发者提供了一种绕过JVM堆限制、直接高效操作内存资源的途径,特别适用于高吞吐量、低延迟和大规模数据处理的场景。虽然直接内存的使用需要更精细的管理以避免内存泄漏和过度消耗系统资源,但恰当的利用能够显著提升应用的性能表现,是现代高性能Java应用不可或缺的工具之一。
27 1
|
1月前
|
存储 缓存 Java
java线程内存模型底层实现原理
java线程内存模型底层实现原理
java线程内存模型底层实现原理