垃圾收集器
了解了垃圾收集算法有复制算法、标记–清除算法和标记–压缩算法。此时相当于对垃圾收集的理解还处于一种理论状态,相当于只定义了接口,还没有完成实现细节。Hotspot虚拟机提供了多种垃圾收集器,每种收集器都有各自的特点,虽然我们要对各个收集器进行比较,但并非为了挑选一个最好的,而是针对具体应用选择最合适的。
关于垃圾收集器的性能指标主要有两点:
- 停顿时间:因为GC而导致程序不能工作的时间长度
- 吞吐量:在特定的时间周期内一个应用的工作量的最大值。对关注吞吐量的应用来说长暂停时间是可以接受的,由于高吞吐量的应用关注的基准在更长周期时间上,所以快速响应时间不在考虑之内。
上图是HotSpot虚拟机中的7个垃圾收集器,连线表示垃圾收集器可以配合使用。
这里需要注意的是:G1收集器可以回收年轻代,也可以回收老年代,而其他的收集器只能针对特定代内存进行回收。
一. 串型收集器
1.1 serial 收集器
serial收集器是最基本,发展历史最悠久的收集器。
client模式下默认收集器配置就是串型收集器,因为在客户端模式下,分配给虚拟机管理的内存一般来说不会很大,Serial收集器收集几十M甚至一两百M的年轻代停顿时间可以控制在一百多ms内,只要不是太频繁,这点停顿是可以接受的。
串型收集器采用单线程stop-the-world的方式进行收集,当内存不足时,串型GC设置停顿标识,待所有线程都进入安全点(safePoint),应用线程暂停,串型GC开始工作,采用单线程方式回收空间并整理内存。
单线程意味着复杂度更低,占用内存更少,垃圾回收效率高,但同时也意味着不能有效利用多核优势。事实上,串型收集器特别适合堆内存不高,单核甚至双核CPU的场景。
开启方式:
-XX:+UseSerialGC
打开此开关后,使用Serial + Serial Old收集器组合来进行内存回收。
Serial Old收集器是Serial收集器的老年代版本,通常也是给client模式下的虚拟机使用,如果在Server模式下:
- 在jdk1.5 以及之前的版本中与Parallel Scavenge收集器搭配使用(因为Parallel Old收集器此时还未面世)
- 作为CMS收集器的后备预案,在并发收集发生失败时使用(Concurrent Mode Failure)
二. 并发收集器
-XX:+UseParallelGC 开启,使用Parallel Scavenge + Serial Old收集器组合来进行内存回收
-XX:+UseParallelOldGC开启,使用Parallel Scavenge + Parallel Old收集器组合来进行内存回收
其他收集器都是以关注停顿时间为目标,而并行收集器是以吞吐量为目标的。
- 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。
- 而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
2.1 ParNew 收集器
ParNew是Serial的多线程版本,由多条GC线程并行进行垃圾清理,但是清理过程依然需要stop-the-world
ParNew追求 低停顿时间,与Serial唯一区别就是使用了多线程进行垃圾收集,在多CPU环境下性能比Serial会有一定程度的提升,但是线程切换需要额外的开销,因此单CPU下性能不如Serial。
2.2 Parallel Scavenge 垃圾收集器
Parallel Scavenge收集器提供了两个参数用于控制吞吐量:
- -XX:MaxGCPauseMillis:控制最大垃圾收集停顿时间,收集器尽可能的保证回收时间不超过设定值
- -XX:GCTimeRatio:直接设置吞吐量大小的(0~100的整数)
缩短停顿时间的原理就是以牺牲吞吐量和年轻代空间来换取的:年轻代空间变小,垃圾回收就越频繁,导致吞吐量下降。
Parallel Scavenge收集器还提供了一个参数:-XX:+UseAdaptiveSizePolicy,用于打开系统自适应的调节策略。不需要手动设置年轻代,Eden和Survivor区的比例及晋升老年代对象的年龄,虚拟机会自动根据当前系统的运行情况收集性能监控信息,动态调整这些参数使其能够拥有最合适的停顿时间和最大的吞吐量。
2.3 CMS 垃圾收集器
这是一种以获取最短停顿时间为目标的收集器。
回收机制:
- 初始标记:仅仅只是标记一下GCRoots能直接关联到的对象,速度很快,需要停顿。
- 并发标记:使用多条标记线程,与用户线程并发执行,此过程进行可达性分析,标记出所有废弃对象,速度很慢
- 重新标记:多条线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来,需要停顿
- 并发清除:只使用一条GC线程,与用户线程并发执行,清除刚刚标记的对象,这个过程很耗时
CMS特点:
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致CPU利用率不高
- 无法处理浮动垃圾:浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次GC时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着CMS收集不能像其他收集器那样等待老年代快满的时候再回收。
- 标记清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配对象,不得不提前出发Full GC。
2.4 G1收集器
G1是一款面向服务端应用的垃圾收集器,没有新生代和老年代的概念,而是将堆划分成一块块独立的区域,当要进行垃圾回收时,首先估计每个区域中垃圾的数量,每次从垃圾回收价值最大的区域中开始回收,因此可以获得最大的回收效率。
从整体上看,G1是基于 标记-整理 算法实现的收集器,从两个区域之间看是基于复制算法实现的,这意味着运行期间不会产生内存空间碎片。
每个区域都有一个remembered Set,用于记录本区域中所有对象引用的对象所在区域,进行可达性分析时,只要在GC Roots中再加上Remembered Set即可防止对整个堆内存进行遍历。
如果不计算维护 Remembered Set 的操作,G1 收集器的工作过程分为以下几个步骤:
- 初始标记:仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。需要停顿
- 并发标记:使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢。
- 最终标记:使用多条标记线程并发执行。需要停顿
- 筛选回收:回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行。
2.5 总结一下
收集器 | 串行/并行/并发 | 年轻代/老年代 | 收集算法 | 目标 | 适用场景 |
Serial | 串行 | 年轻代 | 复制 | 响应速度优先 | 单 CPU 环境下的 Client 模式 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 |
ParNew | 串行 + 并行 | 年轻代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 |
Parallel Scavenge | 串行 + 并行 | 年轻代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 串行 + 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并行 + 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 |
G1 | 并行 + 并发 | 年轻代 + 老年代 | 标记-整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS |
三. 内存分配与回收策略
关于对象的内存分配,也就是在堆上分配。主要分配在年轻代的Eden区上,少数情况也可能直接分配在老年代中。
3.1 Minor GC
当Eden区空间不足时,会触发Minor GC
Minor GC发生在年轻代上,因为年轻代对象存活时间很短,因此Minor GC会频繁执行,执行速度也很快。
工作流程:
- 应用不断创建对象,通常都是分配在Eden区,当其空间不足时,会触发Minor GC,仍然被引用的对象存活下来,复制到JVM选择Survivor区域,而没有被引用的对象则被回收
- 经过一次Minor GC,Eden就会空闲下来,直到再次达到Minor GC触发条件,这时候另一个Survivor区域就会成为To区域,Eden区域的存活对象和From区域对象都会被复制到To区域,并且存活的年龄计数会被+1
- 类似第二步的过程会发生很多次,直到有对象年龄计数达到阈值,这时候就会发生所谓的晋升过程,超过阈值的对象会被晋升到老年代
3.2 Full GC
Full GC发生在老年代上,老年代对象和年轻代的相反,其存活时间长,因此Full GC很少执行,而且执行速度要比Minor GC慢很多。
3.2.1 内存分配策略
- 对象优先在Eden区分配
- 大对象直接进入老年代
- 长期存活的对象进入老年代
- 动态对象年龄判定:如果在Survivor区中相同年龄所有对象大小的总和大于Survivor空间的一半,则年龄大于等于该年龄的对象会直接进入老年代。
- 分配空间担保:在发生Minor GC之前,虚拟机先见擦好老年代最大可用的连续空间是否大于年轻代所有对象总空间,如果条件成立的话,那么Minor GC可以确认是安全的,如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。
3.2.2 Full GC的触发条件
- 手动调用System.gc() 方法。这个方法是建议虚拟机进行Full GC,所以不一定会执行
- 老年代空间不足
- 方法区空间不足
- Minor GC 的平均晋升空间大小大于老年代可用空间
- 对象大小大于 To 区和老年代的可用内存