概述
GC(Garbage Collection),1960年在MIT的Lisp语言。在java中被发扬光大。GC主要控制内存区域是:堆、方法区(元数据空间)。GC主要完成的3件事情:
- 哪些内存需要回收?(Who)
- 什么时候回收?(When)
- 如何回收?(How)
学习目的,对GC的监控和调节。
确定需要回收对象
引用计数算法
每个对象都有一个引用计数器,为0时回收。
- 逻辑简单清晰
- 循环引用无法回收
循环引用示例
1 | class ReferenceObj { |
循环引用对象也被回收了。这说明当前虚拟机并不是通过 引用计数算法来判断对象是否可回收的。
可达性分析算法
猪油语言Java,C#,Lisp 都是通过可达性分析(Reachability Analysis)来判断对象是否存货的。算法基本思路是通过一系列的成为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
可以作为GC Roots的对象包含如下:
- 虚拟机栈 (栈帧中的本地变量表)中引用对象。
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即Native方法)引用对象
引用
JDK1.2之前引用有两种状态:被引用 or 没有被引用。JDK1.2之后引入了:
- 强引用(Strong Reference)
- 软引用(Soft Reference)
- 弱引用(Weak Reference)
- 虚引用(Phantom Reference)
这四种引用强度依次减弱。
强引用
Object obj=new Object();
obj 就是强引用,只要强引用存在 就不会回收。
软引用
SoftReference<ReferenceObj> softRef = new SoftReference(new ReferenceObj("softRef"));
抛出异常前回收
弱引用
WeakReference<ReferenceObj> weakRef = new WeakReference(new ReferenceObj("weakRef"));
下次垃圾回收执行时回收
虚引用
PhantomReference<ReferenceObj> phantomRef = new PhantomReference(new ReferenceObj("phantomRef"), new ReferenceQueue());
下次垃圾回收执行时回收,永远无法获取到被引用对象。理论上可以通过虚引用+ finalize 获取gc执行情况。
示例代码
1 | /** |
运行结果
1 | strongRef : com.hardydou.jmm.ReferenceObj@694f9431 |
非生即死吗?
在可达性分析算法中不可达的对象并非是”非生即死“,还有“缓刑”阶段。一个对象至少要经过两次标记过程.
示例
1 | class FinalizeEscapeGC { |
执行结果
1 | 0.344: [GC (System.gc()) [PSYoungGen: 7891K->1312K(18944K)] 7891K->1320K(62976K), 0.0020721 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
回收方法区
主要回收:废弃常量,无用的类。
方法区有哪些数据
- 已加载类信息
- 类型常量池
- 域(Field)信息
- 方法(Method)信息
- 所有静态(static)变量(不包含常量)
详情可以看 Java几种常量池
找到废弃变量
这个比较简单,变量不存在引用就可以直接回收。(待细细挖掘)
找到无用类
这个比较复杂,需要满足三个条件:
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class 对象没有任何地方引用,无法再任何地方通过反射访问该类的方法。
虚拟机可以对满足以上3个条件的无用类进行回收,并不是一定会回收。HotSpot虚拟机可以通过 -Xnoclassgc 参数控制,通过-verbose:class 以及 -XX:TraceClassLoading、-XX:TraceClassUnLoading(需要FasetDebug版本的虚拟机支持) 查看类加载卸载信息。
垃圾收集算法
不同虚拟机平台操作内存方法不同,GC算法也不相同。以下是几种典型的思想。
标记-清除算法
标记-清除(Mark-Sweep)这是最基本的算法,先标记需要清理的垃圾,再对已经标记的垃圾进行回收。此方法不足:
- 效率低
- 大量内存碎片
其它算法都是基于对此算法缺点优化所诞生的。
复制算法
将空间分成A、B两等分。同一时间只启用一块空间,当A空间满了,就将存活对象复制到B空间,同时A空间全部销毁。B空间满的时候再将存活对象复制到A空间…如此循环。次算法解决了 内存碎片问题、同时效率也得到了提升。但带来新的缺点:
- 内存使用率低
- 对象存活率高时,大量复制导致效率变低;
复制算法-图示:
实际使用情况:
- 基点:98%对象都是朝生夕死。下面所提到使用都是基点成立时最佳。
- 所有商业虚拟机都在新生代采用复制算法作为垃圾回收算法。
- 非1:1分配,一块较大的Eden、两块较小Survivor空间。每次使用Eden+一块Survivor,回收时将Eden、Survivor中存活对象复制到另一块 Survivor,然后清理掉Eden与Survivor。当 Survivor内存不够时需要借用老年代内存进行分配担保(Handle Promotion)。
- 默认情况下 HotSpot虚拟机,Eden:Survivor(8:1),也就是说默认情况浪费10%的内存
标记-整理算法
根据老年代特点所提出 标记-整理(Mark-Compact)算法,标记阶段与 标记-清除一样,只不过清除阶段不是直接回收,而是向一端移动,然后清理掉边界以外内存。
分代收集算法
当前商业虚拟机的垃圾收集都有采用”分代收集”(Generational Collection)算法,这种算法并非新思路。而是根据对象存活周期的不同将内存划分为几块。一般是把Java堆划分为新生代,老年代。然后根据各个年代的特点采用最适合的收集算法。
HotSpot 算法实现
枚举根节点
可作为GC Roots的节点主要在全局性的引用(常量、类静态属性)与上下文(栈帧中的本地变量)中。GC停顿:gc执行时所有Java执行线程必须停顿(Stop The World)。即时在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
准确式GC:Jvm知道内存中某个位置数据是什么类型,不用扫描就可以快速找到对象引用的位置。
安全点
在OopMap协助下可以快速枚举出根节点。但如果每条指令都操作OopMap 将会占用大量空间,因此 jvm只在一些特定的位置操作OopMap,这些位置就是安全点(SafePoint)。如何确保所有线程都是在安全点停顿呢?
- 抢先式中断(Preemptive Suspension)
GC发生时,所有线程先中断。再检查中断线程是否在安全点,不在再激活让他跑到安全点上。所有虚拟机不再使用这个方法。 - 主动式中断(Voluntary Suspension)
当GC要中断线程时,不直接对线程进行操作,设置一个标识,所有线程去轮询标识,发现中断标识就自行挂起。轮询标志的地方和安全点是重合的。
安全区域
使用SafePoint 解决了如何进入GC的问题,但实际情况并不一定。假如某些程序一直处于sleep或者Blocked状态,这样就无法进入安全点挂起,该如何处理。这就需要安全区(Safe Region)来解决。
安全区是指一段代码片段中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。也可以吧Safe Region 看做是被扩展的Safepoint。
在线程执行到Safe Region中的代码时,首先标识自己进入Safe Region,那样当这段时间JVM发起GC时,就不用管标识自己Safe Region状态的线程了。在线程要离开Safe Region时,要检查系统是否已经完成了根节点枚举(或者GC过程),如果完成了那线程就继续执行,否则就必须等待直到可以安全离开SafeRegion的信号为止。
还有一种情况是,既不在SafeRegion 又处于 sleep 或者Blocked 的线程 存在时 怎么处理呢?放弃GC?
垃圾收集器
垃圾回收算法是方法论,垃圾收集器是具体实现。
HotSpot虚拟机垃圾收集器图示:
上图涵盖7中作用于不同分代的收集器,连线的收集器可以搭配使用。重点在CMS、G1这两个收集器。
Serial收集器
最基本、最悠久收集器,曾经(dk1.3.1之前)是新生代唯一选择。单线程的收集器(Stop The World),执行它必须停止所有线程。
图示:
Stop The World 体验非常差劲,从JDK1.3开始,一直到现在的Jdk1.8 ,HotSpot虚拟机团队为消除或者减少工作线程因为内存回收而导致停顿的努力一直在进行着。从Serial收集器——>Paraller收集器——>Concurrent Mark Sweep(CMS)——>最前沿的G1收集器,越来越复杂,停顿不断再缩短,但还没有办法完全消除。
Serial收集器特点以及应用:
- 简单高效
- Client模式下很好选择
ParNew收集器
Serial收集器多线程版本,除了多线程外其余(例如:-XX:SurvivorRation-XX:pretenureSizeThread-XX:HandlePromotionFailure 等)、收集算法、Stop The World 、对象分配规则、回收策略都与Serial完全一样。
ParNew收集器特点及应用:
- Server模式下新生代首选
- 唯一一个可以与CMS(Jdk1.5推出、划时代意义)收集器配合使用的。
- 单Cpu中 ParNew收集器 ≤ Serial收集器,
- 通过-XX:ParallelGCThreads 参数限制垃圾收集线程数
Parallel Scavenge收集器
不同于其他收集器(CMS关注停顿时间)ParallelScavenge 关注吞吐量,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),例如虚拟机总运行100分钟,其中垃圾回收划掉1分钟,那么吞吐量=99%
Serial Old 收集器
SerialOld是Serial收集器的老年代版本,单线程、标记整理算法。client模式下使用、jdk1.5及以前版本与ParallelScavenge 搭配使用、作为CMS后备预案(在并发收集发生Concurrent Mode Failure时使用)
Parallel Old 收集器
Parallel Old 是Parallel Scavenge收集器的老年代版本,多线程、标记整理算法。Jdk1.6中开始使用,在此之前ParallelScanvenge比较尴尬(只可以与SerialOld【性能很差】搭配使用)。
CMS收集器
Concurrent Mark Sweep 收集器是一种以获取最短回收停顿时间为目的的收集器。标记-清除 算法,过程分为4步:
- 初始标记(CMS initial mark)、stop the word
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)、stop the word
- 并发清除(CMS concurrent sweep)
- CMS默认启动回收线程数(CPU数量+3)/4,
- CMS无法处理浮动垃圾(Floating Garbage),CMS并发清理时,用户线程会产生新的垃圾,这些垃圾被称为浮动垃圾。
- CMS可能出现Concurrent Mode Failure 而导致另一次Full GC
- CMS运行期间预留内存无法满足程序需求,就会出现“Concurrent Mode Failure”,此时虚拟机会启动后备预案(SerialOld收集器)
-XX:CMSInitiatingOccupancyFraction(老年代使用比例,触发老年代GC,太低会GC频繁,太高会引起CMF问题) - 使用标记-清除算法会导致空间碎片。CMS提供一个-XX:+UseCMSCompactAtFullCollection 开关参数(默认开启)用于FullGC时开启内存碎片合并整理,-XX:CMSFullGCsBeforeCompaction,这个参数用于设置执行多少次不压缩FullGC后跟着执行一次压缩(默认为0)。
G1收集器
G1(Garbage-First)收集器,先进、Jdk6u4开始试用,Jdk7u4 转正。
- 并行与并发:充分利用多cpu、多核,缩短STW停顿时间、与java线程并发执行。
- 分代收集:根据对象年龄(新创建、存活一段时间、熬过多次gc)进行区分管理。
- 空间整合:整体来看是 标记-整理算法,局部两个Region之间是复制算法。
- 可预测的停顿:建立可以预测的停顿时间模型。
G1堆内存划分为大小相等的region,保留新生、老年代概念,将region与新、老代关联。每个region 里面设置一个RememberedSet记录Ref信息。
不计算维护RememberedSet的步骤,G1操作步骤分为:
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting And Evacauation)