JVM--垃圾回收
JVM–垃圾回收(GarbageCollection)
背景
-
JVM–垃圾回收(GarbageCollection)
-
博主以黑马JVM进行学习
概述
- 如何判断对象可以回收
- 垃圾回收算法
- 分代垃圾回收
- 垃圾回收器
- 垃圾回收调优
如何判断对象可以回收
-
引用计数法
-
被引用一次就+1
-
可能出现循环引用问题
-
-
可达性分析算法
- 先确定根对象,即肯定不能被回收的对象
- 看对象有没有被根对象直接或间接使用,如果有不能回收,没有可以回收
- Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
- 扫描堆中的对象,看是否能沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收
- 哪些对象可以作为GC Root呢?
- 通过Memory Analyzer(MAT)工具和jmap工具
- jmap -dump:format=b,live,file=1.bin 21384
format=b:生成二进制格式的堆转储文件live:只导出存活的对象(会触发一次 Full GC)file=1.bin:导出到 1.bin 文件21384:目标 Java 进程的 PID
- 根对象有
- System Class: 系统加载的类(如 rt.jar 中的 java.lang.String),不会被回收
- Native Stack: JNI 方法引用的对象(本地方法栈中的对象)
- Thread: 活跃线程(如正在运行的用户线程、GC 线程)
- Busy Monitor: 持有同步锁(synchronized)的对象
-
四种引用
-
强引用
- 只有所有的GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
-
软引用(SoftReference)
-
-Xmx20m -XX:+PrintGCDetails -verbose:gc
-
当没有强引用它时,仅有软引用引用该对象时,在垃圾回收后,当内存不充足时,会再次触发垃圾回收
-
关联了引用队列,但软引用锁关联的对象被回收时,软引用对象会进入引用队列,方便进一步释放(即可以配合引用队列来释放软引用自身)
-
引用队列ReferenceQueue<>()
-
-
弱引用(WeakReference)
-
-Xmx20m -XX:+PrintGCDetails -verbose:gc
-
当没有强引用它时,仅有软引用引用该对象时,不管内存充不充足,都会回收弱引用对象
-
弱引用对象会进入引用队列,方便进一步释放(即可以配合引用队列来释放弱引用自身)
-
-
虚引用
-
必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler 线程调用虚引用相关方法释放直接内存
-
Cleaner虚引用对象
-
当ByteBuffer被垃圾回收时,ByteBuffer的直接内存部分不能被java管理,所以虚引用对象进入引用队列,调用Unsafe.freeMemory方法释放直接内存
-
-
终结器引用
- 所有对象都会继承Object对象,都会有finallize()方法
- 当对象被垃圾回收时,终结器引用对象就会进入引用队列
- finallize()方法工作效率很低,第一次并不能释放
- 无需手动编码。但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象
-
对比
引用类型 回收时机 典型应用 强引用 永不回收 普通对象引用( Object o = new Object())软引用 内存不足时 缓存(如图片缓存,内存不足自动释放) 弱引用 GC 时必回收 ThreadLocal、WeakHashMap(缓存临时数据) 虚引用 仅作为回收通知 DirectByteBuffer 直接内存释放 终结器引用 两次 GC 后回收 Object.finalize ()(已被标记为过时) -
垃圾回收算法
- 标记清除
- 根据GCRoot是否引用进行标记
- 再清除,记住位置坐标
- 优点:速度快
- 缺点:容易产生内存碎片,内存空间不连续
- 标记整理
- 根据GCRoot是否引用进行标记
- 整理:避免出现内存碎片,让内存变得更紧凑
- 优点:不易产生内存碎片
- 缺点:效率较低
- 复制
- 把内存区划分成大小相等的俩个区,一个称为FROM,另一个分为TO(空着)
- 把FROM区存活的对象复制到TO区,清空FROM,再进行俩个区交换·
- 优点:不会产生内存碎片
- 缺点:会占用双倍内存空间
- 对比
| 算法 | 优点 | 缺点 | 适用区域 |
|---|---|---|---|
| 标记清除 | 速度快、不移动对象 | 内存碎片、空间不连续 | 老年代(CMS) |
| 标记整理 | 无内存碎片 | 效率低(需要移动对象) | 老年代(SerialOld) |
| 复制 | 无碎片、效率高 | 占用双倍内存 | 新生代(Eden/Survivor) |
分代垃圾回收
- 分为老年代(垃圾回收频率较低)、新生代(经常释放)
- 新生代三个区:伊甸园、幸存区From、幸存区To
- 对象创建时,先存伊甸园中,当伊甸园满时(新生代空间不足时),就会触发一次垃圾回收(Minor GC),如果存活就复制到幸存区To并寿命+1同时幸存区To变成From,而From变成To
- minor gc 会引发 stop the world ,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当在幸存区的对象超过了一个阈值(最大寿命是15(4bit)),则晋升到老年代
- 当新生代和老年代都放不下对象时,会先尝试触发一次minor gc ,之后仍然不足,就会触发一次Full GC
- Full GC 也会引发 stop the world ,不过时间更长
- GC分析
- 相关的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 |
- CG大对象oom,在新生代空间不够的情况下,会直接晋升老年代
垃圾回收器
-
串行
- 单线程
- 堆内存较小,适合个人电脑
- 开启语句:-XX:+UseSerialGC = Serial(工作在新生代:复制算法) + SerialOld(老年代:标记整理算法)
- 因为是单线程工作,所以当垃圾回收线程工作时,其它线程会发生阻塞
-
吞吐量优先
- 多线程
- 堆内存较大,多核cpu
- 让单位时间内,STW的时间最短
- 开启语句:
- -XX:+UseParallelGC ~ -XX:+UseParallelOldGC
- -XX:+UseAdaptiveSizePolicy //采用自适应调整大小
- -XX:GCTimeRatio=ratio //调整堆的大小 公示 1/1+ratio
- -XX:MaxGCPauseMillis=ms //最大暂停时间毫秒数,默认200ms
- -XX:ParallelGCThreads=n //控制线程数
- 垃圾回收线程数与cpu多少核有关
-
响应时间优先
- 多线程
- 堆内存较大,多核cpu
- 尽可能让单次STW的时间最短
- 其它用户线程仍然可以运行
- 开启语句:
- -XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
- -XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads //并行的垃圾回收线程/并发的垃圾回收线程
- -XX:CMSInitiatingOccupancyFraction=percent //何时执行垃圾回收
- -XX:+CMSScavengeBeforeRemark //在重新标记之前,做一次垃圾回收
- 对整个应用的吞吐量是有影响的
-
G1:Garbage First
-
同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是200ms
-
超大堆内存,会将堆划分为多个大小相等的Region
-
整体上是标记+整理算法,两个区域之间是复制算法
-
相关JVM参数
- -XX:+UseG1GC //jdk9以后默认启动
- -XX:G1HeapRegionSize=size
- -XX:MaxGCPauseMillis= time
-
G1垃圾回收阶段
- 循环: —>young Collection—>Young Collection + Concurrent Mark —>Mixed Collection—>
-
Young Collection
- 会STW
-
Young Collection + CM
- 在Young GC 时会进行GC Root的初始标记
- 老年代占用堆空间比例达到阈值,进行并发标记(不会STW),由下面的JVM参数决定
- -XX:InitiatingHeapOccupancyPercent=percent(默认45%s)
-
Mixed Collection
-
会对E(伊甸园区)、S(幸存区)、O(老年区)进行全面的垃圾回收
-
最终标记(Remark)会STW
-
拷贝存活(Evacuation)会STW
-XX:MaxGCPauseMillis=ms //最大暂停时间
-
-
-
Full GC
-
SerialGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
-
ParallelGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
-
CMS
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足
-
G1
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足,分俩中情况
- 第一种,清理垃圾速度足够清理干净
- 第二种,清理垃圾速度比产生垃圾速度慢,就会退为并行垃圾回收
-
-
Young Collection 跨代引用
- 新生代回收的跨代引用(老年代引用新生代)问题
- 使用查表技术,将老年代分成card,有脏卡区
- 卡表与Remembered Set
- 在引用变更时通过post -write barrier + dirty card queue
- concurrent refinement threads 更新 Remember Set
- 新生代回收的跨代引用(老年代引用新生代)问题
-
Remark(重标记)
- pre-write barrier + satb_mark_queue
-
JDK 8u20字符串去重
-
优点:节省大量内存
-
缺点:略微多占用了cpu时间,新生代回收时间略微增加
-
-XX:+UseStringDeduplication
-
String s1 = new String("hello"); // char[]{'h','e','l','l','o'} String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
-
-
将所有新分配的字符串放到一个队列
-
当新生代回收时,G1并发检查是否有字符串重复
-
如果它们的值一样,让它们引用同一个char[]
-
注意,与String.intern()不一样
- String.intern()关注的是字符串对象
- 而字符串去重关注的是char[]
- 在JVM内部,使用了不同的字符串表
-
-
JDK 8u40并发标记类卸载
- 所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载所加载的所有类
- -XX:+ClassUnloadingWithConcurrentMark 默认开启
-
JDK 8u60回收巨型对象
- 一个对象大于region的一半时,称之为巨型对象
- G1不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉(希望越早回收越好)
-
JDK 9并发标记起始时间的调整
- 并发标记必须在堆空间占满前完成,否则退化为FullGC
- JDK9之前需要使用-XX:InitiatingHeapOccupancyPercent(默认45%)
- JDK9可以动态调整
- -XX:InitiatingHeapOccupanyPercent用来设置初始值
- 进行数据采样并动态调整
- 总会添加一个安全的空挡空间
-
JDK 9更高效的回收
-
| 回收器 | 核心目标 | 适用场景 | JVM 参数 |
|---|---|---|---|
| Serial + SerialOld | 简单、低内存 | 单机程序、小堆内存(<1G) | -XX:+UseSerialGC |
| ParallelGC | 高吞吐量 | 后台计算、批处理程序 | -XX:+UseParallelGC |
| CMS | 低延迟 | 电商、Web 应用(JDK8) | -XX:+UseConcMarkSweepGC |
| G1 | 平衡吞吐 & 延迟 | 大堆内存(>4G)、分布式应用 | -XX:+UseG1GC |
| ZGC/Shenandoah | 极低延迟(<10ms) | 高并发、低延迟场景(JDK11+) | -XX:+UseZGC |
垃圾回收调优
-
预备知识
- 掌握GC相关的VM参数,会基本的空间调整
- 掌握相关工具
- 调优跟应用、环境有关
- https://docs.oracle.com/en/java/javase 官网学习
- 查看虚拟机运行的参数:-XX:+PrintFlagsFinal -version | findstr “GC”
-
调优领域
- 内存
- 锁竞争
- cpu占用
- io
-
确定目标
- 【低延迟】还是【高吞吐量】,选择合适的回收器
- CMS,G1,ZGC
- ParallelGC(高吞吐量)
- Zing
-
最快的GC是不发生GC
- 查看FullGC前后的内存占用,考虑下面几个问题
- 数据是不是太多?
- resultSet = statement.executeQuery(“select * from 大表 limit n”)
- 数据表示是否太臃肿?
- 对象图(用到哪个查哪个)
- 对象大小 (能用基本类型int 4字节,就不用Integer 24字节)
- 是否存在内存泄漏?
- static Map map =
- 软引用
- 弱引用
- 第三方缓存实现,如redis等
- 数据是不是太多?
- 查看FullGC前后的内存占用,考虑下面几个问题
-
新生代调优
-
内存调优都建议从新生代开始
-
所有的new操作的内存分配非常廉价
- TLAB thread-local allocation buffer
-
死亡对象的回收代价是零
-
大部分对象用过即死
-
Minor GC的时间远远低于Full GC
-
新生代内存越大越好?
- 一开始,空间大小越大,吞吐量越高,到一定时间后,垃圾回收时间变长,吞吐量开始下降
- 建议大小:新生代能容纳所有【并发量* (请求-响应)】的数据
- 幸存区大到能保留【当前活跃对象+需要晋升对象】
- 晋升阈值配置得当,让长时间存活的对象尽快晋升
- -XX:MaxTenuringThreshold=threshold 调整最大晋升阈值
- -XX:+PrintTenuringDistribution
- 一开始,空间大小越大,吞吐量越高,到一定时间后,垃圾回收时间变长,吞吐量开始下降
-
-
老年代调优
- 以CMS为例
- CMS的老年代内存越大越好
- 先尝试不做调优,如果没有Full GC那么已经。。。,否则先尝试调优新生代
- 观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4~1/3
- -XX:CMSInitiationOccupancyFraction=percent //值越低,触发时间越早
- 以CMS为例
-
案例
-
案例1 Full GC 和Minor GC频繁
- 先增大新生代内存
-
案例2 请求高峰期发生Full GC,单次暂停时间特别长(CMS)
- 使用工具查看是哪个阶段暂停时间长
-
案例3 老年代充裕情况下,发生Full GC(CMS JDK1.7)
- 1.7永久代,空间设置小了,就会触发GC
-
问题现象 解决方案 Minor GC 频繁 增大新生代内存(-Xmn) Full GC 频繁 增大老年代内存、检查内存泄漏、调整 CMS 触发阈值 STW 时间过长 切换到 G1/ZGC、降低 MaxGCPauseMillis JDK7 永久代 OOM 增大永久代(-XX:MaxPermSize=256m)
-