图解 Java 内存模型

1. Java 内存模型的总体设计

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 方法区 (JDK1.7及之前)
  • 直接内存 (JDK1.7及之前)
  • 元空间 (JDK1.8及之后)

其结构如下图所示:

pS6LMPe.png

2. 线程共享区(堆与方法区)的设计

堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 EdenSurvivor 区,最后 SurvivorFrom SurvivorTo Survivor 组成,新生代中的 Eden:From Survivor:To Survivor 的比例是 8:1:1。
pS6LnUO.png

2.1 新生代

新生带(年轻代):新对象和没达到一定年龄的对象都在新生代

  • Eden 区
    Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老 年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行 一次垃圾回收。
  • S1(ServivorFrom):
    上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
  • S2(ServivorTo):
    保留了一次 MinorGC 过程中的幸存者。

系统执行Minor GC时,将把 Eden 和 ServivorFrom 区域中存活的对象从Eden区和From区复制到另一个幸存者空间To区后并将原位置清空,然后交换From区和To区的标记,所以每次,总有一个幸存者空间总是空的

经过多次 GC 循环后(15),存活下来的对象被移动到老年代。通常,这是通过设置年轻一代对象的年龄阈值来实现的,然后他们才有资格提升到老一代。

2.2 老年代

主要存放应用程序中生命周期长的内存对象。

老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行 了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足 够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没 有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减 少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的 时候,就会抛出 OOM(Out of Memory)异常。

2.3 方法区

方法区(Method Area)与 Java 堆一样,是所有线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫 Non-Heap(非堆),目的应该是与 Java 堆区分开。

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本/字段/方法/接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将类在加载后进入方法区的运行时常量池中存放。运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的是 String.intern()方法。受方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

2.3.1 永久代

jdk1.6及之前方法区 是由永久代来实现的,以至于这个时期说方法区就是指永久代,永久代,运行时常量池(包括字符串常量池),静态变量存放在永久代上

jdk1.7 时期方法区在HotSpot中由永久代(类型信息、字段、方法、常量)和堆(字符串常量池、静态变量)共同实现

永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静 态变量、即时编译器编译后的代码等数据,HotSpot VM把GC分代收集扩展至方法区, 即使用Java 堆的永久代来实现方法区。

  • 在 Java6 版本中,永久代在非堆内存区,静态变量存放在永久代上;

  • 到了 Java7 版本,永久代的静态变量和运行时常量池被合并到了堆中,类型信息、字段、方法、常量则继续留在永久代;

  • 而到了 Java8,永久代(方法区)被元空间取代了。 具体描述在下一节

    2.3.2 元空间

    jdk1.8及之后,取消永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆中

why?
为永久代设置空间大小是很难确定的,且对永久代进行调优较困难

在某些场景下,如果动态加载类过多,容易产生 Perm 区的 OOM。如果某个实际 Web 工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现 OOM。而元空间和永久代最大的区别在于,元空间不在虚拟机中,而是使用本地内存,所以默认情况下,元空间的大小仅受本地内存限制

3.线程私有空间

3.1 程序计数器

存储指向下一条指令的地址,即将要执行的指令代码。

多线程在一个特定的时间段内只会执行其中某一个线程方法,CPU会不停的做任务切换,这样必然会导致经常中断或恢复。为了能够准确的记录各个线程正在执行的当前字节码指令地址,所以为每个线程都分配了一个PC寄存器,每个线程都独立计算,不会互相影响。

它是唯一一个在 JVM 规范中没有规定任何 OutOfMemoryError 情况的区域(因为太简单了

3.2 虚拟机栈

是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成 的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。

栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异 常)都算作方法结束
pS6Orfe.png

3.3 本地方法区

本地方法栈则为 Native 方法服务,就是执行C语言代码的栈(因为JVM底层依赖C实现)

4. JVM 总详细内存图

pS6O6ld.png

PS.参考

  1. 磨刀不误砍柴工:欲知JVM调优先了解JVM内存模型.md

  2. Java 内存区域详解

  3. JVM 基础 - JVM 内存结构


转载无需注明来源,放弃所有权利