JVM垃圾回收

目录

  • 1 回收对象判断
  • 2 回收算法
  • 3 分代回收
  • 4 垃圾回收器
    • 4.1 三种垃圾回收器对比
    • 4.2 G1
      • 4.2.1 阶段划分
      • 4.2.2 跨代引用
      • 4.2.3 版本特性
    • 4.3 Remark理解
    • 4.4 Full 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 分代回收

JVM垃圾回收_第1张图片

①对象首先分配在伊甸园区域

②新生代空间不足时,触发 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

三种垃圾回收器图解说明如下:

JVM垃圾回收_第2张图片

串行:两个要点,①GC在安全点后,为防止对象地址改变等问题,②GC线程运行所有用户线程STW

JVM垃圾回收_第3张图片

吞吐量优先:①GC并行,线程数可控,默认为CPU核数(用户线程STW)②动态调整幸存区比例③根据堆的大小合理控制GC的时间占比和最大总时间

JVM垃圾回收_第4张图片

响应时间优先(CMS):

①该回收器最大的不同在于,由于不是每个阶段都STW,因此需要考虑用户线程的在GC的同时造成的影响(比如新的对象和浮动垃圾的产生,引用地址的改变等)

②各标记阶段的区别:初始标记阶段的范围为根对象,并发标记扩大到整个堆,重新标记阶段才会STW,对整个堆范围标记,而且考虑到Old里的根对象跨代引用新生代对象,配合开启参数在该阶段之前就minorGC清理新生代垃圾

③关于退化:并发清理阶段用户线程仍运行,导致内存占用仍然上升,因此需要设置百分比参数启动CMS,避免清理阶段OOM,另外,由于该回收器在老年代算法为标记清除,因此会产生大量内存碎片,引发并发失败,所以需要退化为串行GC来整理内存

4.2 G1

4.2.1 阶段划分

很大程度上和CMS相似,但兼顾吞吐量和低延时,不同在于它将内存划分成region来管理,各阶段内容如下:

JVM垃圾回收_第5张图片

①新生代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 跨代引用

JVM垃圾回收_第6张图片

老年代中根对象引用了新生代对象,成为跨代引用

老年代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代价太高,新生代的各区域分配大小可以粗略计算得出,方法如下:

新生代能容纳所有【并发量 * (请求-响应)】的数据

幸存区大到能保留【当前活跃对象+需要晋升对象】

晋升阈值配置得当,让长时间存活对象尽快晋升

你可能感兴趣的:(jvm,jvm,java,算法)