目录
1、如何判断对象可以回收
1.1 引用计数法
1.2 可达性分析算法
1.3 四种引用:强、软、弱、虚引用
2、垃圾回收算法
2.1 标记清除
2.2 标记整理
2.3 复制
3、分代垃圾回收
3.1 相关JVM参数
4、垃圾回收器
4.1 串行
4.2 吞吐量优先
4.3 响应时间优先
4.4 G1 垃圾回收器
5、垃圾回收调优
5.1 调优领域
5.2 确定目标
5.3 最快的GC是不生发GC
5.4 新生代调优
5.5 老年代调优
5.6 案例
每个对象有一个引用计数器,当对象被引用一次则计数器加1,当对象引用失效一次则计数器减1,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。
从GC Roots作为起点开始搜索,那么整个连通图中的对象便都是活对象,对于GC Roots无法到达的对象便成了垃圾回收的对象,随时可被GC回收。
哪些对象可以作为GC Root ?
1. 强引用
2. 软引用(SoftReference)
3. 弱引用(WeakReference)
4. 虚引用(PhantomReference)
5. 终结器引用(FinalReference)
定义:Mark Sweep
描述:分为标记和清除两阶段:首先标记出所有需要回收的对象,然后统一回收所有被标记的对象。
定义:Mark Compact
描述:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
定义:Copy
描述:将可用内存容量划分为大小相等的两块,每次只用其中一块。当这块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:自带整理功能,这样不会产生大量不连续的内存空间,适合年轻代垃圾回收。
当前商业虚拟机的垃圾收集都采用分代收集。此算法没啥新鲜的,就是将上述三种算法整合了一下。具体如下:
根据各个年代的特点采取最适当的收集算法:
含义 | 参数 | 备注 |
---|---|---|
堆初始大小 | -Xms | |
堆最大大小 | -Xmx 或 -XX:MaxHeapSize=size | |
新生代大小 | -Xmn 或(-XX:NewSize=size + -XX:MaxNewSize=size) | |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy | |
幸存区比例 | -XX:SurvivorRatio=ratio | |
晋升阈值 | -XX:MaxTenuringThreshold=threshold | |
晋升详情 | -XX:+PrintTenuringDistribution | |
GC详情 | -XX:+PrintGCDetails -verbose:gc | |
FullGC前 MinorGC | -XX:+ScavengeBeforeFullGC |
Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器;JDK1.3.1前是HotSpot新生代收集的唯一选择;
-XX:+UseSerialGC = Serial + SerialOld // -XX:+UseSerialGC 添加该参数来显示的使用串行垃圾收集器
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC // JDK1.8默认开启,只要开启UseParallelGC,就对应开启
-XX:+UseAdaptiveSizePolicy // 自适应动态调整伊甸园和幸存区的内存比例
-XX:GCTimeRatio=ratio // 目标1:1 / (1 + ratio) 一般设置ratio为19,20分钟垃圾回收不超过1分钟;会动态调整堆空间大小适应
-XX:MaxGCPauseMillis=ms // 目标2:最大暂停用户线程时间,默认200ms
-XX:ParallelGCThreads=n // 垃圾回收线程数
// 垃圾回收时,CPU会飚得很高
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThread=threads // ParallelGCThreads为4,则ConcGCThread应该是ParallelGCThreads的1/4,对CPU占用没有Par那么高
-XX:CMSInitiatingOccupancyFraction=percent // 执行CMS执行占比,预留空间给浮动垃圾
-XX:+CMSScavengeBeforeRemark // 在CMS垃圾标记前开启新生代垃圾回收,这样重新标记对象要少得多,Full GC时间从接近2秒,降低到300ms左右
//CMS致命问题:CMS会产生内存碎片,如果内存碎片过多,垃圾回收会退化到SerialOld单线程垃圾回收器
初始标记非常快,不影响用户工作工程;并发标记
初始标记:仅仅单线程标记GC Roots的直接关联对象,并且STW,这个过程非常短暂,可以忽略不计;
并发标记:使用GC Roots Tracing算法,进行跟踪标记RC Roots间接相关的对象,不会STW;
重新标记:因为之前并发标记,其他用户线程不暂停,可能产生了新垃圾,所以需要重新标记;
清除垃圾:与用户线程并行执行垃圾回收,使用清除算法
CMS缺点:因为与用户工作程一起并发执行,所以会边清理,一边会产生新的垃圾
JAVA 堆垃圾回收示例:
// GC 分析 大对象OOM
public class T01_Gc_Demo01 {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) throws InterruptedException {
// ArrayList list = new ArrayList<>();
// list.add(new byte[_8MB]);
// list.add(new byte[_8MB]);
// 一个线程OOM,不会导致整个进程挂掉
new Thread(() -> {
ArrayList list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}, "Thread01").start();
System.out.println("sleep...");
TimeUnit.SECONDS.sleep(10);
}
}
定义:Garbage First,优先回收最有价值的垃圾区域,达到暂停时间不短的目标
适用场景
相关JVM参数
总结:G1垃圾回收器,使用标记-整理算法,可以避免CMS标记-清除算法产生的内存碎片问题;在两个Region区域之间,则是使用复制算法。JDK8没有默认G1垃圾回收器,需要手动开启G1
1)G1垃圾回收阶段
2) Young Collection 新生代回收
如果伊甸园进行垃圾回收,则会将伊甸园区存活的对象使用复制算法到Survivor区
当Survivor进行垃圾回收时,对象年龄超过15次,放入老年代;年龄不足15次放入另一个Survivor区域
3) Young Collection + CM(新生代回收+CM)
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
4)Mixed Collection (混合回收)
会对E、S、O进行全面垃圾回收
-XX:MaxGCPauseMillis=ms
5)Full GC
6)Young Collection 跨代引用
如果遍历整个老年代根对象,显然效率会非常低;老年代设计对应一个卡表,每个卡512K,如果某个卡中的对象引用了对象,我们将此卡标记为脏卡,减少扫描范围,提升垃圾回收效率。
7)Remark 重标记
在对象引用改变之前,采用写屏障,表示未处理完毕;同时将对象存入一个引用队列进行处理
8)JDK 8u20 字符串去重
-XX:+UseStringDeduplication // 使用此功能,需要打开此配置,默认是打开
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
9)JDK 8u40 并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
-XX:+ClassUnloadingWithConcurrentMark 默认启用
10)JDK 8u60回收巨型对象
如下图,巨型对象在G1垃圾回收模型情况:
11)JDK 9 并发标记起始时间的调整
12)JDK9 更高效的回收
预备知识
调优原则:让长时间存活对象尽快晋升,如果长时间存活对象大量停留在新生代,新生代采用复制算法,复制来复制去,性能较低而且是个负担
科学运算,追求高吞吐量;互联网项目追求低延迟;高吞吐量垃圾回收,目前没有太多选择就一下ParallelGC;
低延迟垃圾回收,可以选CMS,G1, ZGC。目前互联公司还是很多在用CMS,JDK9 默认G1,不推荐CMS;因为CMS采用标记-清除算法会产生内存碎片,内存碎片多了之后会退化为serialOld,产生大幅度、长时间停顿,给用户的体验是不稳定
如何给新生代调优呢?是不是将新生代内存调得越大越好?下面是Oracle官方文档说明截图
网页链接:https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE
上述大致中文翻译:设置年轻代的堆的初始大小和最大大小(以字节为单位)。 字母k或K表示千字节,m或M表示兆字节,g或G表示千兆字节。 堆的年轻代区域用于新对象。 与其他区域相比,在该区域执行GC的频率更高。 如果年轻代设置太小,则会执行大量 minor gc垃圾回收。 如果设置太大,则仅执行full gc垃圾回收才有效,这可能需要很长时间才能完成。 Oracle官方建议设置年轻代的大小保持大于堆总大小的25%,并且小于堆总大小的50%。
总结:新生代,还是需要调大一些,因为新生代采用复制算法,需要移动对象,复制算法性能效率较低。
公式:新生代能容纳所有【并发量 * (请求 - 响应)】的数据
以CMS 为例
文章最后,给大家推荐一些受欢迎的技术博客链接:
欢迎扫描下方的二维码或 搜索 公众号“大数据高级架构师”,我们会有更多、且及时的资料推送给您,欢迎多多交流!