JVM 内存结构
{Back to Index}
Table of Contents
1 内存结构
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 栈中,并指向堆中实例。
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
标记清除法先通过根节点标记所有可达对象,然后清除所有不可达对象。 该算法效率高但是容易产生内存碎片(空间不连续)。
Figure 3: mark-sweep 算法示意
3.2.2 Mark-Compact
效果等同于标记清除法执行完成后在进行一次内存碎片整理,这种算法效率较低,适用于老年代的内存回收。
Figure 4: mark-compact 算法示意
3.2.3 Copying
核心思想是将内存空间分为两块,每次只使用其中一块,在进行垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中去, 之后清除正在使用的内存块中的 所有对象 ,并交换两个内存块的角色。
复制算法的优势是效率高,回收后的内存空间没有碎片,但带来的 代价是内存折半, 典型的空间换时间的做法,适用于新生代垃圾回收。
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
。
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)。
新生代和老年代可以选择指派不同的回收器。
Figure 7: 垃圾收集器
3.4.1 串行模式(SerialGC)
使用单线程执行垃圾回收工作。
- \(-XX+UseSerialGC\)
- 新生代和老年代都使用串行回收器
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 暂停时间,回收器会动态调整堆大小以达到该目标,该参数用于优化响应速度的(与上面的参数其实是对立的)
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 ,该对象才会被真正回收。