java程序在运行的过程中,会有大量的对象在堆中创建,其中大部分对象都是朝生夕死,jvm垃圾回收就是对程序中不再使用的对象进行回收,使jvm有足够的空间创建之后的对象。
1.无用对象的判定
1.1 引用计数法
当有一个地方引用对象时,计数器+1,;当引用失效时,计数器-1;当计数器为0时为失效的对象。
缺点是无法解决循环引用的问题。
1.2 可达性分析算法
通过一系列称为GC Roots的对象作为起点,从起点开始向下搜索,搜索走过的路径称为引用链,当一个对象没有出现在任何引用链上,说明为无用对象,需要被回收。
可以作为GC roots的对象:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
当对象没有在引用链上时,并不会立即回收,虚拟机会调用一次对象的finalize方法,完成一次自救,调用finalize之后,如果还是无用的对象才会被回收 - 如果没有重新finalize方法,对象会直接被回收
- 如果对象的finalize方法已经调用过一次,之后就不会调用了
注意:finalize 方法已经不建议使用,了解一下即可
2. 引用级别
在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为 强引用 (Strong Reference)、软引用 (Soft Reference)、弱引用 (Weak Reference)、虚引用 (Phantom Reference) 4 种,这 4 种引用强度依次逐渐减弱。
引用类型 | 说明 | 实现 |
---|---|---|
强引用 | 只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象 | Object obj = new Object(); |
软引用 | 被软引用关联的对象,在系统即将发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。 | SoftReference sfRefer = new SoftReference<>(new Object()); sfRefer.get(); // 可以获得引用对象值 |
弱引用 | 它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次 GC 发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象 | WeakReference weakRefer = new WeakReference<>(new Object()); weakRefer.get(); // 可以获得引用对象值 |
虚引用 | 最弱的一种引用关系,被虚引用关联的对象,完全不会对其生存时间构成影响,也无法通过虚引用获取引用对象值。为应用设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 | PhantomReference phantom = new PhantomReference<>(new Object(), new ReferenceQueue<>()); |
3. 垃圾收集算法
3.1 标记清除算法 Mark-Sweep
和名字一样算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,之后清除所有标记的对象。
缺点:效率不高,标记和清除两个阶段的效率都不高;会产生大量的不连续的碎片,锁片过多会导致以后创建大对象的时候,由于没有足够的空间而出发下一次垃圾回收。
3.2 复制算法
复制算法将内存分为大小相等的两块,每次只使用其中的一块,当一块用完时,就将所有的对象复制到另一块,之后清除使用过的空间
- 优点:内存分配时不需要考虑内存碎片的问题;实现简单运行高效
- 缺点:降低了内存的利用率,只能使用内存大小的一半空间;如果对象的存活率高,对象的复制效率会变低。
- 新生代回收算法
是目前新生代的回收算法,由于新生代大部分对象都是朝生夕死的,所以并不需要1:1划分空间,而是分为一个较大的Eden区和两块较小的Survivor空间,每次只使用其中的Eden和一个Survivor区,当回收时,将Eden和Survivor中的存活的对象一次性的复制到另一块Survivor中,之后清空掉Eden和刚才用过的Survivor空间。HotSpot虚拟机Eden和Survivor默认的比例为8:1.空间利用率为90%,只有10%会被浪费。
- 分配担保
由于我们不能保证每次回收到是少于10%的对象存活,如果空闲的Survivor区空间不足以存储存活的对象,对象会通过分配担保机制直接进入到老年代。
3.3 标记-整理算法
算法分为两步,第一步对需要回收的对象进行标记,之后将存活的对象向一端移动,然后清理掉边界以外的内存,适用于老年代回收算法。
4. 分代收集算法
java根据对象生存周期的不同,将堆分为新生代和老年代。新生代中,每次垃圾收集都有大批的对象死去,因此使用复制回收算法。老年代对象存活率高、没有额外的空间作为分配担保,因此使用“标记-清理”或 “标记-清除”算法
5. HotSpot 算法实现
5.1 stop-the-world原因
在gc枚举根节点时,不允许在分析的过程中对象的引用关系还在变化的情况,否则分析结果的正确性就没法保证。即使在号称不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
5.2 gc效率的保证
在枚举根节点时,jvm并不需要一个不漏的检查完所有执行上下文和全局引用位置,在hotspot中,使用一组称为oopMap的数据结构来达到这个目的的,在类加载完成的时候,虚拟机就把对象内什么偏移量上时什么数据类型的数据计算出来,这样gc在扫描时就知道这些信息了。
5.3 安全点
在gc时,线程并不是在所有的位置都能暂停下来,只有到达了安全点才可以暂停,安全点就是jvm记录了oommap的位置。
有两种方式让线程跑到安全点上中断
- 抢占式中断
抢占式中断不需要线程代码主动配合,在gc发生时,首先吧所有线程都中断,如果发现中断的地方不在安全点上,就恢复线程让他跑到安全点上。 - 主动式中断
主动式中断,不直接对线程操作,仅仅设一个标志位,各个线程主动去轮训这个标志,发现中断标志为真就主动挂起线程,轮训的地方和安全点重合,hotspot采用这种中断方式。
5.4 安全区
安全区是指在一段代码的片段中,引用关系不会发生变化。在这个区域的任何地方开始GC都是安全的。例如线程sleep block时。
6. 垃圾收集器
并行:只多条垃圾收集线程并行工作,但是用户线程任然处于等待状态
并发:用户线程和垃圾收集线程同时执行。
6.1 Serial收集器
是最基本、发展历史最悠久的收集器,是一个单线程收集器,在收集时会暂停应用的线程;采用复制回收算法;
6.2 ParNew 收集器
ParNew是Serial的多线程版本,使用复制回收算法,与Series相比没有太多的创新之处,但是它是运行在server模式虚拟机的默认收集器,因为除了Series收集器,只有它能与CMS收集器搭配使用。
参数:
- -XX:+UseParNewGC 开启ParNew收集器
- -XX:+UseConcMarkSweepGC 开启CMS收集器时,新生代默认使用ParNew收集器
- -XX:ParalleGCThreads 设置垃圾收集的线程数
6.3 Parallel Scavenge收集器
也是一个新生代收集器,使用复制回收算法,又是并行的收集器;
Parallel Scavenge收集器的关注点是达到一个可控的吞吐量,高吞吐量可以高效的利用CPU时间,尽快完成程序的运算任务,主要适用与后台运算而不需要太多交互的任务。
吞吐量=用户线程执行时间/(用户线程时间+gc时间)
cms收集器追求的是缩短垃圾回收时用户线程的停顿时间,适用于交互频繁的系统,较少的停顿可以提升用户的体验
参数
- -XX:MaxGCPauseMillis 控制垃圾收集的最大停顿时间 大于0的毫秒数
- -XX:GCTimeRatio 直接设置吞吐量的大小
- -XX:+UseAdaptiveSizePolicy 开关参数,设置后会自动调整Eden和Survivor区域的比例(-XX:SurvivorRatio)和 晋升老年代年龄参数(-XX:PretenureSizeThreshold)以达到一个最优的停顿时间或者最大的吞吐量。
6.4 Serial Old收集器
Serial Old是Series的老年代版本,采用标记-整理算法。
在Server模式下有两大用途,在1.5之前与Parallel Scavenge搭配使用,另一个用途是作为CMS的后备预案,当CMS出现Concurrent Mode Failure时使用。
6.5 Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,采用“标记-整理”算法
在注重吞吐量的应用中,Parallel Scavenge和Parallel Old是一个完美的组合。
6.6 CMS (并发标记-清除收集器)
CMS(Concurrent Mark Sweep)是一种以获取最大回收停顿时间为目标的收集器,目前很大一部分互联网站或B/S系统服务上,这类应用尤其重视服务的响应速度,希望系统的停顿时间最短,已给用户带来较好的用户体验,CMS非常适合这类应用。
整个过程分为4步
- 初始标记
- 并发标记
- 重新标记
- 并发清除
其中初始标记和重新标记任然需要stopwold,但是这两个步骤耗时是比较短的,并发标记和并发清除耗时比较长,但是可以与用户线程同时执行。所以从整体上看cms收集器与用户线程是一起并发执行的。
缺点
- CMS收集器对CPU资源非常敏感;cms与用户线程并发的时候会占用cpu资源,如果系统的cpu资源有限,一样会影响用户的体验。
- CMS收集器无法处理浮动垃圾,可能出现 Concurrent Mode Failure失败导致另一次的Full GC。
- 浮动垃圾
CMS在并发清理阶段,用户的线程依然在运行,在这个过程中还会有新的垃圾不断产生,cms无法在当次收集中处理它们,只有等到下一次GC时再清理,这部分垃圾称为浮动垃圾。
由于收集时,用户线程还需要运行,所以cms需要预留一部分内存给用户线程使用, 不能等到内存用完才进行收集,cms默认当老年代使用率68%就会启动收集,如果老年代增长的不是太快可以调整这个参数。-XX:CMSInitiatingOccupancyFraction。
如果cms预留的空间无法满足用户线程的需求,就会出现Concurrent Mode Failure失败,这时虚拟机将启动后备预案,临时使用SerialOld收集器进行老年代收集,这样停顿时间就更长了。
也就是说XX:CMSInitiatingOccupancyFraction参数调的太大,会容易导致Concurrent Mode Failure问题
- CMS采用标记-清除收集器,会产生内存碎片,在分配大对象时容易产生fullgc,为了解决这个问题CMS提供了一个
-XX:+UseCMSCompactAtFullCollection开关参数,用于在CMS收集器顶不住要进行FullGC时开启碎片整理过程。
-XX:CMSFullGCsBeforeCompaction 这行多少次垃圾收集后,进行一次带压缩的GC
6.7 G1收集器
G1 (Garbase-First)是一个面向服务端应用的垃圾收集器
- G1的特点
- 支持并发与并行,充分利用多核CPU的优势,减少gc停顿时间
- 不需要与其他收集器配合使用,可以独立管理堆
- 没有垃圾碎片,g1从整体上看是基于标记-整理算法,从两个region上看是基于复制算法,这两种算法意味着G1不会产生内存碎片
- 可预测的停顿时间:这是G1相对于CMS的一大优势,这可以将gc限定在指定的停顿时间内完成。
-
原理
G1将整个Java堆划分为多个大小相等独立的区域(Region),每个区域内依然存在新生代和老年代的划分.G1之所以能建立可预测的停顿时间模型。是因为它有计划的避免在整个堆中进行全区域的垃圾回收。G1跟踪各个Region里面的垃圾回收价值大小,并维护一个优先级列表,根据每次允许的时间回收价值最大的Region区(这就是Garbase-First的由来), -
回收过程
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
回收步骤基本与CMS一致,初始标记和最终标记阶段需要暂停用户线程,并发标记与筛选回收可以并发执行。
7. 垃圾回收参数总结
参数 | 描述 |
---|---|
UseSerialGC | client模式下的默认值,打开此开关后,使用Serial +SerialOld收集器组合 |
UseParNewGC | 打开此开关后使用ParNew+SerialOld收集器组合 |
UseConcMarkSweepGC | 打开此开关后,使用ParNew+CMS+SerialOld的收集器组合进行内存回收 |
UseParallelOldGC | 打开此开关后,使用ParallelScavenge+Parallel Old收集器组合进行内存回收 |
SurvivorRatio | 新生代中Eden与survivor区域的容量比值,默认为8;8:1 |
PretenureSizeThreshold | 直接晋升到老年代对象的大小,这与这个参数的对象将直接进入老年代 |
MaxTenuringThreshold | 直接晋升到老年代的对象年龄 |
UseAdaptiveSizePolicy | 动态调整Java堆各个区域的大小以及进入到老年代的年龄 |
HandlePromotionFailure | 是否允许分配担保失败,既老年代的剩余空间不足以应付新生代的整个Eden和Survivor区所有对象都存活的极端情况 |
ParallelGCThreads | 设置并行GC时的线程数 |
GCTimeRatio | GC时间占总时间的比率,默认值为99,既允许1%的GC时间,仅在Parallel Scavenge收集器时使用 |
MaxGCPauseMills | GC的最大停顿时间,仅在Parallel Scavenge收集器生效 |
CMSInitiatingOccupancyFraction | 设置老年代剩余多少空间后开始CMS收集,仅在CMS收集器生效 |
UseCMSCompactAtFullCollection | 设置CMS收集器在完成垃圾回收时要进行一次碎片整理 |
CMSFullGCsBeforeCompaction | 设置CMS收集器在进行若干次收集后启动一次碎片整理 |
8. 内存分配策略
- 对象优先在Eden区分配,当Eden区不足时,会触发Minor GC
- 大对象直接进入老年代
- 长期存活的对象进入到老年代,如果对象经过几次MinorGC后任然存活会进入到老年代