JVM内存结构及Java垃圾收集(GC算法、GC收集器)
最近正在复习Java相关的知识,总结一下JVM相关的知识点。
JVM内存结构
先上图
堆内存(线程间共享)
在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,是垃圾收集器管理的主要区域(也叫GC堆),Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时(通过-Xmx和-Xms控制扩展),将会抛出OutOfMemoryError异常。
方法区(线程间共享)(因为GC较少也叫永久代)
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。不需要连续的内存和可以选择固定大小或者可扩展,还可以选择不实现垃圾收集,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
虚拟机栈(线程私有)
生命周期与线程相同。描述的是Java方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;虚拟机动态扩展时无法申请到足够的内存时OOM
本地方法栈(线程私有)
类似于虚拟机栈,虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务,与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
程序计数器(线程私有)
当前线程所执行的字节码的行号指示器。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。此区域是唯一一个不会OOM的区域。
如何通过参数控制各个区域的大小
Java的垃圾收集
如何判断对象是否存活
- 引用计数
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收,但是无法解决循环引用的问题。 - 可达性分析
从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
GC Roots包括:
虚拟机栈中引用的对象;
方法区中类静态属性实体引用的对象;
方法区中常量引用的对象;
本地方法栈中JNI引用的对象。
垃圾回收算法
标记 — 清除算法
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
主要问题:
1、效率较低
2、会产生大量不连续的内存碎片,可能导致当程序在以后的运行过程中需要分配较大对象时,虽然总的空间足够,但是连续的内存空间却不足,不得不提前触发另一次垃圾收集动作。
复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
主要问题:
虽然解决了内存碎片的问题,但是可用内存却减小了一半。
在实际应用中,因为多数对象都是很快就会被垃圾收集的,所以把整个内存区域分成了一个Eden区,两个survivor区,大小比为8:1:1,当回收时,将一个Eden区和一个survivor区活着的对象复制到另一个survivor区,再将这次的Eden和survivor区清理。
标记 — 整理
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集
把Java堆分为新生代和老年代,对各代分别用适当的算法,新生代采用复制算法,老年代采用标记清除或标记整理算法。
垃圾回收器
Serial
- 串行收集器,收集时stop the world,单线程收集器;
- 默认的client模式下的新生代收集器;
- 新生代复制,老年代标记整理,简单高效
ParNew
- Serial收集器的多线程版本,新生代并行,老年代串行,收集时stop the world,server模式下默认的新生代收集器,能搭配CMS;
- 新生代复制算法、老年代标记-整理;
- CPU好的环境里,停顿时间好于serial,并发能力差的环境里不如serial;
Parallel Scavenge
- 类似ParNew收集器,Parallel收集器更关注系统的吞吐量,适合在后台运算不需要太多交互的任务。
- 新生代复制算法、老年代标记-整理
- 可以设置GC最大停顿时间,具有自适应调节策略,以达到最大吞吐量
CMS
以获取最短回收停顿时间为目标的收集器,系统停顿时间最短,给用户带来较好的体验,基于标记—清除,具体过程为:
- 初始标记:stop the world,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
- 并发标记(耗时最长):进行GC Roots Tracing的过程,收集器线程与用户线程一起工作
- 重新标记:stop the world,为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间比初始标记阶段稍长一些,但远比并发标记的时间短。
- 并发清除(耗时最长):并发回收垃圾,收集器线程与用户线程一起工作
整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,但是并发阶段应用程序变慢,降低吞吐量,产生大量空间碎片,也无法处理浮动垃圾(并发清除时产生的垃圾,只能等到下次清除)
G1
- 面向服务端应用的垃圾收集器,整体看基于标记整理算法,局部看基于复制(两个region之间),不会产生内存空间碎片
- 可预测停顿,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒
- 使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。G1跟踪各个region中垃圾堆积的价值大小(回收空间及所需时间),在后台维护一个优先列表,在每次被允许的时间内,优先回收高价值的。
回收过程分如下阶段:
- 初始标记:stop the world,仅仅只是标记一下GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing的过程(进行可达性分析),可与用户程序并发执行
- 最终标记:stop the world,为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,并行执行
- 筛选回收:对各个region回收价值和成本排序,制定回收计划。
什么时候进行Minor GC(新生代GC)
Eden区没有足够的空间时
什么时候进行Full GC(Major GC)
只要老年代的连续空间 > 新生代对象总大小 或 历次晋升的平均大小,就Minor GC,否则进行Full GC
对象什么时候进入老年代
- 大对象直接进入老年代,避免在Eden区和两个survivor区之间进行大量内存复制
- 长期存活的对象进入老年代,Eden区出生,Minor GC后进入Survivor区,年龄为1,之后每过一次Minor GC,年龄+1,到15岁进入老年代
- Survivor空间中年龄相同的所有对象内存大小之和>survivor空间的一半,大于等于该年龄的对象进入老年代
- 空间分配担保,复制算法时,8:1:1 比例下,存活的对象大小超过survivor区时,通过老年代进行担保