目录
- [1 回收对象判断](#1 回收对象判断)
- [2 回收算法](#2 回收算法)
- [3 分代回收](#3 分代回收)
- [4 垃圾回收器](#4 垃圾回收器)
-
- [4.1 三种垃圾回收器对比](#4.1 三种垃圾回收器对比)
- [4.2 G1](#4.2 G1)
-
- [4.2.1 阶段划分](#4.2.1 阶段划分)
- [4.2.2 跨代引用](#4.2.2 跨代引用)
- [4.2.3 版本特性](#4.2.3 版本特性)
- [4.3 Remark理解](#4.3 Remark理解)
- [4.4 Full GC](#4.4 Full GC)
- [5 GC调优](#5 GC调优)
1 回收对象判断
①引用计数法:顾名思义,只要对象的引用个数不为零,则不会被回收,因此存在对象间相互引用导致引用个数无法归零,对象无法回收导致的内存泄露问题
②可达性分析法(JVM使用):GC Root对象直接或间接引用的对象,除此外的会被回收,
③如何找到② 中GC Root对象,首先jmap -dump:format=b,live,file=filename pid 命令将GC后的堆占用情况快照转储为二进制文件,再借助eclipse提供的java堆分析工具MAT,分析该文件
5种引用 | 回收条件 | 使用方式 |
---|---|---|
强引用 | 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收 | |
软引用 | 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用 | 可以配合引用队列来释放软引用自身 |
弱引用 | 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 | 可以配合引用队列来释放弱引用自身 |
虚引用 | 主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存 | 必须配合引用队列使用 |
终结器引用 | 垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象 | 无需手动编码,但其内部配合引用队列使用 |
2 回收算法
算法 | 优点 | 缺点 |
---|---|---|
标记清除 | 速度快,对GC后的内存地址打标记为可覆盖写,即完成清除 | 会造成内存碎片 |
标记整理 | 没有内存碎片,因为标记完会移动到相邻内存地址 | 速度慢 |
复制 | 没有内存碎片,因为会复制到相邻内存地址 | 需要占用双倍内存空间 |
3 分代回收
①对象首先分配在伊甸园区域
②新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
③当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
④当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
⑤当新对象过大直接超过新生代大小,则不走晋升流程,直接分配在老年代区域
⑥多个用户线程的内存占用情况互相之间不影响,比如,主线程里创建的新线程OOM,不会导致整个进程的终止
相关 VM 参数
含义 | 参数 |
---|---|
堆初始大小 | -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 |
4 垃圾回收器
4.1 三种垃圾回收器对比
回收器 | 范围 | 适用对象 | 特点 | 回收算法 | 开启参数 |
---|---|---|---|---|---|
串行 | 单线程 | 堆内存较小,适合个人电脑 | 不紧不慢 | 新生代采用复制算法,老年代为标记+整理 | -XX:+UseSerialGC = Serial + SerialOld |
吞吐量优先 | 多线程 | 堆内存较大,多核 cpu | 让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高 | 新生代采用复制算法,老年代为标记+整理 | -XX:+UseParallelGC ~ -XX:+UseParallelOldGC(开启一个自动开另一个) -XX:+UseAdaptiveSizePolicy -XX:GCTimeRatio=ratio -XX:MaxGCPauseMillis=ms -XX:ParallelGCThreads=n |
响应时间优先 | 多线程 | 堆内存较大,多核 cpu | 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5 跟上面比就是,上面GC占的总时间短,这个是每次GC控制在最短时间完成 | 新生代采用复制算法,老年代为标记+清除 | -XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld(并发失败则退化为后续方案,需要配置相关参数备用) -XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads -XX:CMSInitiatingOccupancyFraction=percent -XX:+CMSScavengeBeforeRemark |
三种垃圾回收器图解说明如下:
串行:两个要点,①GC在安全点后,为防止对象地址改变等问题,②GC线程运行所有用户线程STW
吞吐量优先:①GC并行,线程数可控,默认为CPU核数(用户线程STW)②动态调整幸存区比例③根据堆的大小合理控制GC的时间占比和最大总时间
响应时间优先(CMS):
①该回收器最大的不同在于,由于不是每个阶段都STW,因此需要考虑用户线程的在GC的同时造成的影响(比如新的对象和浮动垃圾的产生,引用地址的改变等)
②各标记阶段的区别:初始标记阶段的范围为根对象,并发标记扩大到整个堆,重新标记阶段才会STW,对整个堆范围标记,而且考虑到Old里的根对象跨代引用新生代对象,配合开启参数在该阶段之前就minorGC清理新生代垃圾
③关于退化:并发清理阶段用户线程仍运行,导致内存占用仍然上升,因此需要设置百分比参数启动CMS,避免清理阶段OOM,另外,由于该回收器在老年代算法为标记清除,因此会产生大量内存碎片,引发并发失败,所以需要退化为串行GC来整理内存
4.2 G1
4.2.1 阶段划分
很大程度上和CMS相似,但兼顾吞吐量和低延时,不同在于它将内存划分成region来管理,各阶段内容如下:
①新生代GC:还是老规矩,伊甸园中幸存对象拷贝到幸存区region等待晋升,满足阈值则晋升至老年代,整个过程会STW
②新生代GC+并发标记: Young GC 时会进行 GC Root 的初始标记,老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
③混合收集:会对 伊甸园、幸存区、老年代 进行全面垃圾回收,整个过程包含最终标记、拷贝存活两个阶段,会STW,为了实现最短的GC暂停时间,在老年代GC时会优先收集垃圾多的region,点题Garbage First
-XX:MaxGCPauseMillis=ms
④不难理解,回收算法为整体上是 标记+整理 算法,两个区域之间是 复制 算法
开启参数:
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
4.2.2 跨代引用
老年代中根对象引用了新生代对象,成为跨代引用
老年代region进一步细分为卡表(card Table),其中引用新生代对象的标记为脏卡,以此在做GC Root遍历时,减少搜索范围
对应的新生代region有Remembered Set来记录外部引用(脏卡)
对象的引用发生变更时需要更新脏卡,异步线程从写屏障和脏卡队列中获取变更信息来更新脏卡
4.2.3 版本特性
版本 | 功能 | 开启参数 | 特点 |
---|---|---|---|
JDK 8u20 | 字符串去重 | -XX:+UseStringDeduplication | 将所有新分配的字符串放入一个队列 当新生代回收时,G1并发检查是否有字符串重复 如果它们值一样,让它们引用同一个 char[] 这么做节省内存,但占用CPU回收young的时间 |
JDK 8u40 | 并发标记类卸载 | -XX:+ClassUnloadingWithConcurrentMark | 所有对象都经过并发标记后,就能知道哪些类不再被使用, 当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 |
JDK 8u60 | 回收巨型对象 | 一个对象大于 region 的一半时,称之为巨型对象 G1 不会对巨型对象进行拷贝 回收时被优先考虑 G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生代垃圾回收时处理掉 | |
JDK 9 | 并发标记起始时间的调整 | -XX:InitiatingHeapOccupancyPercent 用来设置初始值,进行数据采样并动态调整 | 并发标记必须在堆空间占满前完成,否则退化为 FullGC |
4.3 Remark理解
由于并发标记阶段并未STW,因此用户线程可能对已经标记过的对象改变其它对象对其的引用,比如被标记为待回收的对象加上新的引用,结果导致被引用的对象会被回收,因此为了防止该现象,当对象的引用将要发生变化,在此之前,写屏障(pre-writer barrier)指令将其放入队列(satb_mark_queue),等到Remark阶段再将其更新回收标记防止误回收
4.4 Full GC
问题:单纯的老年代空间不足就会发生Full GC吗?
ANS:以四种垃圾回收器说明,确实新生代不足会minor GC,但是,只有串行GC和并行GC时老年代内存不足会Full GC,CMS和G1则不是,G1的old占比到达45%则会触发并发标记和后续的混合收集阶段,CMS也是到达阈值则触发GC,但是还不能达到Full GC标准,只有并发清理的速度跟不上垃圾产生的速度,才会触发Full GC
5 GC调优
工具:
java官网查看参数
查看虚拟机参数:
"jdk安装目录\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"
手段:
缓存数据使用软引用/弱引用,在内存吃紧时回收,或第三方缓存实现如redis
新生代特点:
TLAB 线程私有可分配内存,避免多个线程创建对象时互相干扰
采用复制算法,死亡对象回收代价为零,大部分对象用过即死,少量存活,因此minor GC时间远低于Full GC
新生代内存占比并非越大越好,占比1/4~1/2,否则old太小触发full GC
整体思想为:尽量多给老年代分配更多内存(观察Full GC时old内存占比,然后调大 1/4 ~ 1/3 ),毕竟full GC代价太高,新生代的各区域分配大小可以粗略计算得出,方法如下:
新生代能容纳所有【并发量 * (请求-响应)】的数据
幸存区大到能保留【当前活跃对象+需要晋升对象】
晋升阈值配置得当,让长时间存活对象尽快晋升