学习权重30%。
理解问题:
垃圾收集器的特点与 运作原理,JVM自动内存分配与回收的主要规则。
判断对象存活
引用计数法Reference Counting
在对象中添加一个计数器,有引用就+1,引用失效就-1。
缺陷
无法解决对象互相引用导致无法回收的问题。
可达性分析(Reachability Analysis)算法
可达性分析算法理论上要求全过程都基于一个能保障一致性的快照中才能够进行分析, 这意味着必须全程冻结用户线程的运行。
JVM使用增量更新和原始快照两种方案来实现可达性分析的并发,用于削减可达性分析的停顿时间。
以根对象GC Roots为起点,依据引用向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果对象到GC Roots间没有任何引用链相连,则对象不可达。

可作为GC Roots的对象
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized关键字)持有的对象。
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
引用的扩展
Java希望对象不是只有“引用”、“未被引用”两种状态,有些对象现可以判断空间是否充足再进行回收。因此,拓展了引用的概念。
弱引用、强引用、软引用、虚引用
强:为null才会在合适时间被清除(JVM停止运行时)。
软:内存不足时JVM才会回收
弱:不论内存充足,都会回收,生命周期更短。
虚:任何时候都可以回收。
引用类型
|
被回收时间
|
说明
|
强引用
|
无引用关系时
|
传统引用定义,引用赋值 Object o = new Object()这种
|
软引用
|
内存溢出异常前
|
对象缓存
|
弱引用
|
下一次垃圾收集前
|
对象缓存
|
虚引用
|
随关联对象
|
为一个对象设置虚 引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知
|
进行对象回收
对象经历一次标记、一次筛选、一次标记后被执行回收。
第一次可达性分析判断不可达进行标记,第二次筛选是否需要执行finalize()方法。将需要执行finalize()方法的对象防至F-Queue队列,然后标记完成。
注:避免使用finalize()方法
回收方法区
可以通过参数配置。
方法区垃圾收集的性价比很低。但在频繁自定义类加载器的场景中,还是需要java虚拟机具备类卸载的能力。
垃圾收集算法
JVM使用的是“Tarcing GC 追踪式垃圾收集”类型的算法
分代收集理论
1、弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
2、强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消 亡。
3、跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极 少数。
按照对象熬过垃圾收集过程的次数进行分代,即区域划分。
不同区域执行垃圾收集回收的频次不一样、垃圾收集算法也不一样。
对于跨代引用,使用特殊内存区域存储跨代引用对象。
收集分类
部分收集(Partial GC)
指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
- 新生代收集(Minor GC/Young GC)
目标只是新生代的垃圾收集,Eden区满触发。
- 老年代收集(Major GC/Old GC)
指目标只是老年代的垃圾收集。目前只有CMS收集器会有单 独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。
停顿时间较长,但比Full GC短。
老年代的空间使用率达到一定阈值时触发,通过 -XX:CMSInitiatingOccupancyFraction参数设置老年代占用比例。默认值
- 混合收集(Mixed GC)
指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收 集器会有这种行为。
整堆收集(Full GC)
收集整个Java堆和方法区的垃圾收集。一种重型的垃圾回收操作,通常导致程序线程暂停(Stop-The-World STW)。
可以通过System.gc()方法调用进行FullGC。
或是在老年代空间不足、堆内存空间不足时进行Full GC。
可以开启GC日志进行分析。
标记-清除算法(mark-sweep)
最早出现的基础算法。
首先标记出所有需要回 收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回 收所有未被标记的对象。
标记的过程就是可达性分析的过程。
需要停顿用户线程来进行标记、清理回收对象,不过停顿时间相对要短。
缺点
需要大量标记、清除操作,效率低;
内存空间碎片化问题严重,碎片化会导致连续空间不足不好存储对象从而触发再一次的垃圾收集工作。

标记-复制算法
目的是解决“标记-清除算法”回收大量对象效率低的问题。
将可用 内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着 的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
根据分代理论,需要复制的活着的对象是少数的。所以每次复制的对象不多。
优点
避免碎片产生,高效
缺点
可用内存为原来的一半,空间浪费严重

标记-整理算法 Mark-Compact
为对象存活率高的区域设计的算法,针对老年代对象。
让所有存活的对象都向内存空间的一端移动,直接清理掉边界以外的内存。
注意:移动对象需要更改对象全部引用,需要暂停全部用户程序才能进行。总的来说,JVM设计者做了内存分配、内存回收的权衡以提升总吞吐量。

吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)
根节点枚举
可达性分析时,需要先找到根节点GC Roots然后再查找引用链。
HotSpot使用OopMap( Ordinary Object Pointer Map对象指针Map)数据结构存储根节点对象引用。
在OopMap协助下HotSpot可以快速准确完成GC Roots枚举。
记忆集 Remembered Set(跨代对象引用集合)
依据分代收集理论的跨代假说,跨代对象极少,而垃圾收集器针对跨代对象的特殊数据结构就是记忆集Remembered Set。用以避免把整个老年代加进GC Roots扫描范围、或是跨代引起的范围扫描。
记忆集是一种 用于记录从非收集区域指向收集区域的指针集合(跨代引用集合)的抽象数据结构。
集合对象的粒度可以分为:
- 字长精度:每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个 精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。
- 对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
- 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。
最常用的是“卡精度”,Card Table卡表就是卡精度下记忆集的一种实现。
总结:对于如何避免跨代引用问题造成的大范围扫描本质还是通过标识来实现的。标识出这一块内存有跨代指针,然后就可以将这一块内存加入GC Root中进行扫描。有点类似索引避免全表扫描。
卡表的维护
卡表标识的时间:有其他分代区域中对象引用了本区域对象时,其对应的 卡表元素就应该变脏(进行标识)。
如何在对象赋值的那一刻去更新维护卡表呢:须找到一个在机器码层面的手段,把维护卡表的动作放到每一
个赋值操作之中。
总结:操作附带同步变化,相关性维护。
并发可达性分析
已知标记对象进行可达性分析时需要暂停用户进程。
JVM使用增量更新和原始快照两种方案来实现可达性分析的并发,用于削减可达性分析的停顿时间。
具体实现说明略。
收集器
Serial收集器:针对新生代,采用复制算法。
ParNew(并行)收集器:新生代采用复制算法,可与CMS收集器配合使用于老年代。
Parallel Scavenge(并行)收集器 (JDK8默认新生代):针对新生代,采用复制算法,注重吞吐量。
Serial Old(串行)收集器:针对老年代,采用标记-整理算法。
Parallel Old(并行)收集器 (JDK8默认老年代):针对老年代,采用标记-整理算法,是Parallel Scavenge的老年代版本。
CMS(Concurrent Mark-Sweep)收集器:主要作用于老年代,基于标记-清除算法,但为了减少内存碎片,有时也会进行整理。
G1(Garbage First)收集器:整体上采用标记-整理算法,局部(如年轻代)采用复制算法。
综上:新生代基本采用“标记-复制算法”,老年代采用“标记-整理算法”。cms采用标记清理。

收集器的差异与选择
JDK8默认收集器为新生代Parallel Scavenge和Parallel Old,JKD9开始G1收集器成为默认收集器雏形。
G1相较于默认的两个收集器,通过分区域管理和预测停顿时间,有更低更可控的延迟时间。
G1低延迟原理:划分更多的独立区域,优先回收垃圾最多的区域。支持并发标记和部分整理。
并发和分区管理带来的开销更高。
维度
|
Parallel Scavenge + Parallel Old
|
G1
|
吞吐量
|
高,适合批处理任务
|
稍低,但仍可接受
|
停顿时间
|
较长,随堆增大显著增加
|
可控,支持低延迟目标
|
堆大小适应性
|
小堆表现良好,大堆停顿变长
|
专为大堆设计(>6GB 更优)
|
内存碎片
|
少,整理算法有效
|
较少,但极端情况可能触发 Full GC
|
调优复杂度
|
低,几乎无需调整
|
中等,需要根据应用调整参数
|
适用场景
|
计算密集型、延迟不敏感
|
交互式应用、大内存、延迟敏感
|
总结:可用内存>6G。追求地延迟就选G1
生产
Full GC监控与问题排查
- 可以使用jstat -gc命令进行监控
- 可以通过参数-XX:+PrintGCDetails -Xloggc: 开启GC日志
- 可以通过jmap生成堆转储文件来看
日志分析 → 工具监控 → 原因定位 → 参数/代码优化 → 验证