一. JVM 调优
1.1 JVM 调优如何使用
笔者首先会使用到的工具:
- java 自带工具:JVisualVM 用于监控,jstack 查看线程状态,jmap 用于堆 dump;
- Memory Analyser:载入堆 dump 文件,进行分析。
- 在项目启动的时候,加入 -XX:+PrintGCDetails 参数,可以观察 GC 的频率。观察 GC 频率可以判断 GC 频率是否正常(主要是针对 Full GC)。如果不正常,就可以观察 GC 日志,并且针对 GC 的频率进行原因的猜测。
- 如果有堆 Dump 文件,则可以观察内存中占据最大空间的对象是什么,如果有特定的某些对象占据了太大的对象,则需要重新考虑是否有可以优化的空间?(比如 C3P0 连接池资源过大,太多的 HashMap 节点占据过大内存等)
1.2 GC 回收算法
- 标记-清除算法:对需要回收的对象进行标记,然后清除;
- 复制算法:将内存区域分为两部分 A, B 区域,发生 GC 时,将 B 区域的所有内容复制到 A 区域,然后清除 B 区域所有内容。发生第二次 GC 时反过来。
- 适用于朝生夕死对象较多的情况下;
- 在实际使用时 (如 Parallel Scavange 收集器),多数情况下只在两块小区域中进行复制操作,这两块小区域通常是在新生代中。新生代分为三部分:Eden + Survivor * 2 (To Survivor, From Survivor),三者比例通常为 8:1:1。通常如果发生 minor GC 时,将 Eden + Survivor1 的存活的对象全部拷贝到 Survivor2 中,然后将 Eden 和 Survivor1 的全部对象清除。
- 标记-整理算法:将需要回收的对象进行标记并清除,然后内存碰撞,所有对象的内存移到一端。
- 分代回收算法:垃圾收集器大多都是只针对新生代或者老年代,所以商用的垃圾收集器一般都会分别针对新生代与老年代,用不同的分代回收算法进行垃圾回收。
1.3 GC 回收策略
有几种不同的垃圾收集器,不同收集器各自有各自的收集对象以及收集策略;
1.3.1 Serial
Serial + Serial Old 收集器:两者都是串行收集器;Serial 收集器用于新生代的 Minor GC,单线程的 STW GC;Serial Old 收集器用于旧生代,单线程的 STW Full GC;
1.3.2 ParNew
ParNew 收集器是Serial 收集器的多线程版本,用于新生代的 GC,通常与 CMS 收集器搭配使用。
1.3.3 Parallel Scavenge + Parallel Old
Parallel Scavenge + Parallel Old 收集器,两者都是并行收集器,且都是强调 GC 时间的垃圾收集器。可以通过设置垃圾收集的比率 (程序时间 / (程序时间 + GC 时间)),或者 GC 最大时间,来保障 GC 时间不会过长。但这样也牺牲了新生代空间大小,以及吞吐量大小。
- Parallel Scavenge: 针对新生代,使用复制算法。
- Parallel Old: 针对旧生代,使用标记-整理算法。
1.3.4 CMS 收集器
参考地址:《GC之详解CMS收集过程和日志分析》
CMS (Concurrent Mark Sweep) 收集器用于旧生代,它的设计目的是为了减少回收时间。为了实现这一目的,首先 CMS 用了列表数据结构,用来管理回收可利用空间;另外 GC 线程与用户线程并行处理(即 GC 过程中也会同时执行服务逻辑)。CMS 收集器使用标记-清除算法,步骤如下:
- 初始标记 (Initial Mark):
- 第一次 Stop the world,用来标记 GC Roots 直接关联的对象,以及新生代中存活对象的引用对象;
- 并发标记 (Concurrent Mark):
- 标记 GC Roots 链上的所有对象,GC 线程与应用线程并行执行;
- 该过程不会 Stop the world,但时间最长;
- 重新标记 (Final Remark):
- 第二次 Stop the world,标记在并发标记过程中产生的对象,时间不长;
- 过程中包含两次并发预清理 (Concurrent Preclean);
- 并发清除 (Concurrent Sweep):
- 对所有被标记的对象进行并行的回收(使用标记-清除算法);
- 并发重置线程 (Concurrent Reset):
- 并发的重置 CMS 算法内部的数据结构,准备下一个 CMS 生命周期的使用;
CMS 收集器虽然使用了标记-清除算法,可能会出现大量的内存碎片,但可以通过设置虚拟机参数,使得在 Full GC 之后执行空间压缩合并,清理内存碎片。
1.3.5 G1 收集器
G1 收集器是一种新式的收集器,对旧生代、新生代没有明确的区分对待,因为它将堆内存划分为若干个 Region,旧生代和新生代与之前的定义不同,物理上不再隔离。标记清除的步骤和 CMS 收集器相似。
1.4 GC 分配内存策略
- 给对象分配内存时,首先优先分配到 Eden 区;随着 GC 次数的增加,Eden + Survivor1/2 的内存会用 minor GC 的复制算法将内存转移到 Survivor2/1 中,这样的操作会让对象的 GC 年龄 +1;
- 对于比较大的对象 (比如较长的 String 或者数组),可能会直接进入旧生代。在虚拟机参数中可以设置该参数 -XX:PertenureSize,大于该值的对象便直接进入旧生代。
- 对于年龄比较大的对象,即经过了多次 minor GC 的对象,会进入旧生代。虚拟机参数 -XX:MaxTenuringThreshold 设置该值,默认值为 1;
- 并非所有对象年龄都必须达到 MaxTenuringThreshold 值才会晋升老年代,虚拟机会针对根据对象年龄,以及对象的大小,动态的判定是否可以直接进入老年代。Survivor 空间中相同年龄所有对象的大小之和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入旧生代;
- 分配担保策略:在进行 Minor GC 之前,判断旧生代可用空间是否大于新生代中所有对象大小之和;如果是,则这次 Minor GC 是安全的;如果不是,则需要根据虚拟机参数 HandlePromotionFailure 值,判断是否允许担保失败。如果该值为是,判断老年代可用空间是否大于一个经验值,如果大于,则尝试一次 MinorGC,如果小于,则进行 Full GC。
注:GC 机制见《JVM技术总结之二——GC机制》。