JVM 内存结构
{Back to Index}  

Table of Contents

1 内存结构

jvm-arche.png

Figure 1: JVM 基本架构

1.1 Java 堆

Java 堆在虚拟机启动时创建,用于存放对象实例,几乎所有的对象( 包含 interned string )都在堆上分配内存,当对象无法再该空间申请到内存时将抛出 OutOfMemoryError 异常。

堆空间是线程共享的,同时也是垃圾收集器管理的主要区域。可通过 -Xmx -Xms 参数来分别指定最大堆和最小堆。

1.1.1 堆,方法区和 Java 栈的关系

public class SimpleHeap {

    private int id;

    public SimpleHeap(int id){
        this.id = id;
    }

    public void show(){ }

    public static void main(String[] args) {
        SimpleHeap s1 = new SimpleHeap(1);
        SimpleHeap s2 = new SimpleHeap(2);
        s1.show();
        s2.show();
    }
}

上述代码中各对象和局部变量的存放如下图所示。SimpleHeap 实例本身在堆中分配,描述 SimpleHeap 类的信息存放在方法区,局部变量存放在 Java 栈中,并指向堆中实例。

heap-and-stack.png

Figure 2: 堆,方法区和 Java 栈的关系

1.2 Java 栈

每个线程都有一个私有的 Java 栈, 在线程创建的时候被创建。

栈中每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息, 不存在垃圾回收问题, 只要线程一结束该栈就释放,生命周期和线程一致。

线程请求的栈深度大于虚拟机栈所允许的深度,将抛出 StackOverFlowError 异常。

通过虚拟机参数 -Xss 指定栈空间,空间大小决定了 函数调用的深度 ,栈越大,函数可以支持的嵌套调用次数就越多。 1

1.2.1 局部变量表

局部变量表是栈帧的组成部分之一,用于保存函数的参数和局部变量。当函数调用结束,函数帧栈销毁,局部变量表也会随之销毁。

在相同栈容量下,局部变量少的函数可以支持更深层次的函数调用。 2

1.2.2 栈上分配

栈上分配是虚拟机的一项优化技术,基本思想是,对于线程私有的对象, 可以将它们分配在栈上,而不是分配在堆上。

这样做的好处是对象可以在函数调用结束后自行销毁,而不需要垃圾收集器的介入,提高系统性能。

栈上分配可以用于 逃逸分析 ,即判断对象的作用域是否有可能逃出函数体,如果虚拟机发现对象不可能发生逃逸,则可以将对象分配在栈上,而不是堆上。3

1.3 方法区

方法区是一块所有线程共享的内存区域,用于保存类的字段,方法和常量池等。

Java 8 之后使用 元数据区 表示方法区,元数据区的大小可以使用 -XX:MaxMetaspaceSize 来指定,这是一块 堆外的直接内存4

如果不指定大小,默认情况下,虚拟机会用尽所有可用的系统内存。

2 String Table

2.1 \(intern()\)

尝试将字符串对象引用放入 ST ,如果已存在直接返回现有的引用。

3 垃圾回收

3.1 回收依据

常用的用于判断对象是否可以回收的方法有两种:

  • 引用计数

    早期 Python 采用这种方式,缺点是不能解决循环引用的情况。

  • 可达性分析

    JVM 采用这种方式,从根对象 (GC root) 开始遍历,无法到达的对象就是可以回收的。根对象包括:局部变量表中的对象引用,方法区中类静态属性引用,本地方法栈中的对象引用。

3.2 回收算法

3.2.1 Mark-Sweep

标记清除法先通过根节点标记所有可达对象,然后清除所有不可达对象。 该算法效率高但是容易产生内存碎片(空间不连续)。

mark-sweep1.png

Figure 3: mark-sweep 算法示意

3.2.2 Mark-Compact

效果等同于标记清除法执行完成后在进行一次内存碎片整理,这种算法效率较低,适用于老年代的内存回收。

mark-compact1.png

Figure 4: mark-compact 算法示意

3.2.3 Copying

核心思想是将内存空间分为两块,每次只使用其中一块,在进行垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中去, 之后清除正在使用的内存块中的 所有对象 ,并交换两个内存块的角色。

复制算法的优势是效率高,回收后的内存空间没有碎片,但带来的 代价是内存折半, 典型的空间换时间的做法,适用于新生代垃圾回收。

copy.png

Figure 5: copying 算法示意

3.3 分代回收

JVM 内存模型就是采用了分代回收的思想,新生代和老年代可以选择使用不同的回收器。

新生代分为 eden 区, from 区和 to 区 3 个部分。
其中 from 和 to 可以视为用于复制的两块 大小相同,地位相等且可进行角色互换的内存空间。 ( from 和 to 也称为 survivor 区)

在进行垃圾回收时, eden 区的存活对象会被复制到未使用的 survivor 区(假设是 to ),正在使用的 survivor 区(假设是 from )的存活对象也会被复制到 to 区。
大对象或者老年对象直接进入老年代,如果 to 区已满,对象也会直接进入老年代。
此时,eden 和 from 的剩余对象就是垃圾对象, 直接清空即可, to 则存放此次回收后的存活对象。

内存分配与回收大致逻辑:

  • 对象一开始被分配在新生代的 eden 区域。
  • 当 eden 区空间不足时,触发 minor gc ,即将 eden 和 survivor from 中存活的内存使用 copying 算法 复制到 survivor to 中,存活内存年龄 \(+1\) ,同时在逻辑上交换 survivor from/to 区域。
  • minor gc 会暂停用户线程,即触发 STW 。
  • 当对象内存年龄超过 15 时,晋升至老年代。 \(age>15\) 不是必要条件,如遇到内存过大,或新生代内存不足的情况,也会晋升至老年代。
  • 当老年代空间不足时,会先尝试 minor gc ,若空间仍不能满足需求,则触发 full gc
  • full gc 触发 STW ,因为老年代使用 mark-compact 内存回收算法 ,所以停顿时间远长于 minor gc

java-copy.png

Figure 6: 分代回收示意

3.3.1 相关 VM options

含义 options
新生代大小 -Xmn
幸存区占比 -XX:SurvivorRatio=ratio
打印晋升信息 -XX+PrintTenuringDistribution
GC 时打印信息 -Xlog:gc* -verbose:gc

3.4 回收器

不同的回收器决定了执行回收操作时所采用的的 线程模型 (serial/parallel/concurrent) 和 算法模型 (copying/mark-sweep/mark-compact)。
新生代和老年代可以选择指派不同的回收器。

collector.png

Figure 7: 垃圾收集器

3.4.1 串行模式(SerialGC)

使用单线程执行垃圾回收工作。

\(-XX+UseSerialGC\)
新生代和老年代都使用串行回收器

serial.png

Figure 8: 串行收集器运行示意

3.4.2 并行模式 (ParalelGC) 【吞吐量优先】

使用多线程执行垃圾回收工作,尽可能减少 STW 次数。

\(-XX:+UseParallelGC\)
新生代使用 Parallel 收集器,老年代使用串行收集器
\(-XX:+UseParallelOldGC\)
新生代和老年代都使用并行收集器
\(-XX:ParalelGCThreads=n\)
控制 GC 线程数
\(-XX:+UseAdaptiveSizePolicy\)
动态调整新生代 eden 和 survivor 的比例
\(-XX:GCTimeRatio=ratio\)
设置 GC 时间与运行时间的占比,回收器会动态调整堆大小以满足该目标,该参数用于优化吞吐量的
\(-XX:MaxGCPauseMillis=ms\)
设置最大 GC 暂停时间,回收器会动态调整堆大小以达到该目标,该参数用于优化响应速度的(与上面的参数其实是对立的)

parallel.png

Figure 9: 并行收集器运行示意

3.4.3 并发模式【响应时间优先】

多线程并发(不阻塞用户线程)执行垃圾回收工作,尽可能让 STW 时间最短

3.4.3.1 CMS

只用于老年代垃圾回收,实际效果并没有想象中那么好,由于【浮动垃圾】和因使用 sweep 算法造成的过量内存碎片的因素存在,很有可能触发(降级)一次使用串行收集器的内存整理,非常耗时。

\(-XX:+UseConcMarkSweepGC\)
老年代开启 CMS 回收器
\(-XX:+UseParNewGC\)
指定新生代回收器,这是 Serial 收集器的多线程版本
3.4.3.2 G1

4 引用

4.1 强引用

当 GC Roots 无法通过强引用遍历到该对象时,该对象才能被回收。

4.2 Soft

若对象被软引用引用时,当首次垃圾回收后内存仍然不足时,立即再次执行 GC 时被回收,可以配合引用队列使用。

4.3 Weak

若对象被弱引用引用时,只要发生 GC 就会被回收,可以配合引用队列使用。

4.4 Phantom

必须 配合引用队列才能使用,当引用的对象被回收时,该弱引用会进入引用队列,由 Reference Handler 线程调用 \(PhantomReference::clean\) 方法执行收尾动作,通常用于释放 ByteBuffer 占用的直接内存。

4.5 Final

必须 配合引用队列才能使用,通常情况下无需人为创建使用该引用类型,当引用的对象需要回收时( 实际上没有立刻回收 ),Final 引用对象进入引用队列,由 Finalizer 线程调佣 \(Object::finalize\) 方法, 再经过一次 GC ,该对象才会被真正回收。

Footnotes:

Author: Hao Ruan (ruanhao1116@gmail.com)

Created: 2020-03-26 Thu 20:31

Updated: 2022-06-22 Wed 15:19

Emacs 27.2 (Org mode 9.4.4)