(超详解)JVM-垃圾回收
Posted LL.LEBRON
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了(超详解)JVM-垃圾回收相关的知识,希望对你有一定的参考价值。
文章目录
JVM-垃圾回收
本文章参考:黑马程序员JVM
1.如何判断对象可以回收
1-1 引用计数法
- 当一个对象被其他变量引用,该对象计数加一,当某个变量不在引用该对象,其计数减一
- 当一个对象引用没有被其他变量引用时,即计数变为0时,该对象就可以被回收
缺点:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放
1-2 可达性分析算法
- JVM中的垃圾回收器通过可达性分析来探索所有存活的对象
- 扫描堆中的对象,看能否沿着GC Root对象为起点的引用链找到该对象,如果找不到,则表示可以回收
- 可以作为GC Root的对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
- 所有被同步锁(synchronized关键字)持有的对象。
1-3 五种引用
- 强引用
- 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
- 软引用
- 仅有【软引用】引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
- 可以配合【引用队列】来释放软引用自身
- 弱引用
- 仅有【弱引用】引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
- 可以配合【引用队列】来释放弱引用自身
- 虚引用
- 必须配合【引用队列】使用,主要配合
ByteBuffer
使用,被引用对象回收时,会将【虚引用】入队, 由Reference Handler
线程调用虚引用相关方法释放【直接内存】 - 如上图,B对象不再引用
ByteBuffer
对象,ByteBuffer
就会被回收。但是直接内存中的内存还未被回收。这时需要将虚引用对象Cleaner
放入引用队列中,然后调用它的clean
方法来释放直接内存
- 必须配合【引用队列】使用,主要配合
- 终结器引用
- 无需手动编码,但其内部配合【引用队列】使用,在垃圾回收时,【终结器引用】入队(被引用对象暂时没有被回收),再由
Finalizer
线程通过【终结器引用】找到被引用对象并调用它的finalize
方法,第二次 GC 时才能回收被引用对象 - 如上图,B对象不再引用A4对象。这时终结器对象就会被放入引用队列中,引用队列会根据它,找到它所引用的对象。然后调用被引用对象的
finalize
方法。调用以后,该对象就可以被垃圾回收了
- 无需手动编码,但其内部配合【引用队列】使用,在垃圾回收时,【终结器引用】入队(被引用对象暂时没有被回收),再由
软引用使用:
public class Demo1 {
public static void main(String[] args) {
final int _4M = 4*1024*1024;
//使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
List<SoftReference<byte[]>> list = new ArrayList<>();
SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
}
}
软引用_引用队列使用:
public static void main(String[] args) throws IOException {
///使用引用队列,用于移除引用为空的软引用对象
ReferenceQueue<byte[]> queue=new ReferenceQueue<>();
//使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
//关联了引用队列,当软引用所关联的byte数组被回收时,软引用自己就会加入到引用队列queue 中去
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB],queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}
//获取队列中第一个软引用对象
Reference<? extends byte[]> poll = queue.poll();
//遍历引用队列,如果有元素,则移除
while(poll!=null){
list.remove(poll);
poll=queue.poll();
}
System.out.println("=============");
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
弱引用使用:
弱引用的使用和软引用类似,只是将 SoftReference
换为了 WeakReference
public static void main(String[] args) {
//使用弱引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是弱引用
List<WeakReference<byte[]>> list=new ArrayList<>();
for (int i = 0; i < 5; i++) {
WeakReference<byte[]> ref=new WeakReference<>(new byte[_4MB]);
list.add(ref);
for (WeakReference<byte[]> w : list) {
System.out.print(w.get()+" ");
}
System.out.println();
}
System.out.println("循环结束:"+list.size());
}
2.垃圾回收算法
2-1 标记清除
定义:在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识清除相应的内容,给堆内存腾出相应的空间
注意:这里的清除并不是将内存空间字节清零,而是记录这段内存的起始地址,下次分配内存的时候,会直接覆盖这段内存。
优点:速度快
缺点:容易产生内存碎片。一旦分配较大内存的对象,由于内存不连续,导致无法分配,最后就会造成内存溢出问题
2-2 标记整理
定义:在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后整理剩余的对象,将可用的对象移动到一起,使内存更加紧凑,连续的空间就更多。
优点:不会有内存碎片
缺点:速度慢
2-3 复制
定义:将内存分为等大小的两个区域,FROM和TO(TO中为空)。将被GC Root引用的对象从FROM放入TO中,再回收不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间。
优点:不会有内存碎片
缺点:会占用双倍的内存空间。
3.分代垃圾回收
将堆内存分为新生代和老年代,新生代有划分为伊甸园,幸存区To,幸存区From。
3-1 回收流程
对象首先分配在伊甸园区域
新生代空间不足时,触发 Minor GC,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
再次创建对象,若新生代的伊甸园又满了,则会再次触发 Minor GC(minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行),这时不仅会回收伊甸园中的垃圾,还会回收幸存区中的垃圾,再将活跃对象复制到幸存区TO中。回收以后会交换两个幸存区,并让幸存区中的对象寿命加1
当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
当老年代空间不足,会先尝试触发Minor GC,如果之后空间仍不足,那么触发 Full GC,stop the world的时间更长
3-2 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 |
大对象处理策略
遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代
/**
* 演示内存的分配策略
*/
public class Main {
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 -XX:-ScavengeBeforeFullGC
public static void main(String[] args) throws InterruptedException {
ArrayList<byte[]> list=new ArrayList<>();
list.add(new byte[_8MB]);
}
}
线程内存溢出
某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行。这是因为当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常
/**
* 演示内存的分配策略
*/
public class Main {
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 -XX:-ScavengeBeforeFullGC
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}).start();
//主线程还是会正常执行
System.out.println("sleep....");
Thread.sleep(1000L);
}
}
4.垃圾回收器
相关概念:
- 并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态
- 并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上
- 吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%
4-1 串行
- 单线程
- 堆内存小,适合个人电脑
开启串行回收器:
XX:+UseSerialGC = Serial + SerialOld
,新生代**-Serial** ,老年代-SerialOld
安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象。
阻塞:因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态
Serial 收集器:
- 定义:Serial收集器是最基本的、发展历史最悠久的收集器
- 特点:单线程收集器。采用复制算法。工作在新生代
Serial Old收集器:
- 定义:Serial Old是Serial收集器的老年代版本
- 特点:单线程收集器。采用标记-整理算法。工作在老年代
4-2 吞吐量优先
- 多线程
- 堆内存较大,多核cpu
- 让单位时间内暂停时间(STW)最短
- JDK1.8默认使用的垃圾回收器
开启吞吐量优先回收器:
Parallel 收集器:
- 定义:与吞吐量关系密切,故也称为吞吐量优先收集器
- 特点:并行的,工作于新生代,采用复制算法
Parallel Old 收集器:
- 定义:是Parallel 收集器的老年代版本
- 特点:并行的,工作与老年代,采用标记-整理算法
4-3 响应时间优先
- 多线程
- 堆内存较大,多核cpu
- 尽可能让单次的暂停时间(STW)最短
- 初始标记:标记GC Roots能直接到的对象。速度很快,存在Stop The World
- 并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行
- 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。存在Stop The World
- 并发清理:对标记的对象进行清除回收
CMS收集器:
- 定义:Concurrent Mark Sweep(并发,标记,清除)
- 特点:基于标记-清除算法的垃圾回收器。是并发的。工作在老年代。
ParNew 收集器:
- 定义:ParNew收集器其实就是Serial收集器的多线程版本
- 特点:工作在新生代,基于复制算法的垃圾回收器。
4-4 G1
定义:Garbage First
- JDK 9以后默认使用,而且替代了CMS 收集器
适用场景:
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
- 超大堆内存,会将堆划分为多个大小相等的 Region
- 整体上是 标记+整理 算法,两个区域之间是 复制 算法
相关参数:JDK8 并不是默认开启的,所需要参数开启
垃圾回收阶段:
新生代伊甸园垃圾回收—–>内存不足,新生代回收+并发标记—–>混合收集,回收新生代伊甸园、幸存区、老年代内存——>新生代伊甸园垃圾回收(重新开始)
Young Collection:
存在Stop The World
分区算法region:分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间
E:伊甸园 S:幸存区 O:老年代
Young Collection + CM:
- CM:并发标记
- 在 Young GC 时会对 GC Root 进行初始标记
- 在老年代占用堆内存的比例达到阈值时,对进行并发标记(不会STW),阈值可以根据用户来进行设定
Mixed Collection:
会对E S O 进行全面的回收
- 最终标记(Remark)会STW
- 拷贝存活(Evacuation)会STW
-XX:MaxGCPauseMills:xxx
:用于指定最长的停顿时间
问:为什么有的老年代被拷贝了,有的没拷贝?
因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)
Full GC:
- SerialGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- ParallelGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- CMS
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足
- G1
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足(老年代所占内存超过阈值)
- 如果垃圾产生速度慢于垃圾回收速度,不会触发Full GC,还是并发地进行清理
- 如果垃圾产生速度快于垃圾回收速度,便会触发Full GC
Young Collection 跨代引用:
新生代回收的跨代引用(老年代引用新生代)问题
- 卡表:老年代被划为一个个卡表
- Remembered Set:Remembered Set 存在于E(新生代)中,用于保存新生代对象对应的脏卡
- 脏卡:O被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡
- 在引用变更时通过post-write barried + dirty card queue
- concurrent refinement threads 更新 Remembered Set
Remark:
重新标记阶段
黑色:已被处理,需要保留的
灰色:正在处理中的
白色:还未处理的
但是在并发标记过程中,有可能A被处理了以后未引用C,但该处理过程还未结束,在处理过程结束之前A引用了C,这时就会用到remark
- 之前C未被引用,这时A引用了C,就会给C加一个写屏障,写屏障的指令会被执行,将C放入一个队列当中,并将C变为 处理中 状态
- 在并发标记阶段结束以后,重新标记阶段会STW,然后将放在该队列中的对象重新处理,发现有强引用引用它,就会处理它
JDK 8u20 字符串去重:
- 优点:节省大量内存
- 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加
例如:
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
- 将所有新分配的字符串(底层是char[])放入一个队列
- 当新生代回收时,G1并发检查是否有重复的字符串
- 如果字符串的值一样,就让他们引用同一个字符串对象
- 注意,其与String.intern的区别
- intern关注的是字符串对象
- 字符串去重关注的是char[]
- 在JVM内部,使用了不同的字符串表
JDK 8u40 并发标记类卸载:
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 -XX:+ClassUnloadingWithConcurrentMark
默认启用
JDK 8u60 回收巨型对象:
- JDK 8u60 回收巨型对象一个对象大于 region 的一半时,称之为巨型对象
- G1 不会对巨型对象进行拷贝
- 回收时被优先考虑回收巨型对象
- G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0的巨型对象就可以在新生代垃圾回收时处理掉
JDK 9 并发标记起始时间的调整:
- 并发标记必须在堆空间占满前完成,否则退化为 FullGC
- JDK 9 之前需要使用
-XX:InitiatingHeapOccupancyPercent
- JDK 9 可以动态调整
-XX:InitiatingHeapOccupancyPercent
用来设置初始值- 进行数据采样并动态调整
- 总会添加一个安全的空档空间
5.垃圾回收调优
5-1 调优领域
- 内存
- 锁竞争
- CPU占用
- IO
- GC
5-2 确定目标
- 【低延迟】还是【高吞吐量】,选择合适的回收器
- CMS,G1,ZGC (低延迟,响应时间优先)
- ParallelGC
- Zing
5-3 最快的 GC
最快的GC是不发生GC
查看Full GC前后的内存占用,考虑以下几个问题:
- 数据是不是太多?
resultSet = statement.executeQuery("select * from 大表")
- 数据表示是否太臃肿?
- 对象图
- 对象大小
- 是否存在内存泄漏?
5-4 新生代调优
- 新生代的特点
- 所有的new操作分配内存都是非常廉价的
- TLAB thread-local allocation buffer(可防止多个线程创建对象时的干扰)
- 死亡对象回收零代价
- 大部分对象用过即死(朝生夕死)
- MInor GC 所用时间远小于Full GC
- 所有的new操作分配内存都是非常廉价的
- 新生代内存越大越好么?
- 不是
- 新生代内存太小:频繁触发Minor GC,会STW,会使得吞吐量下降
- 新生代内存太大:老年代内存占比有所降低,会更频繁地触发Full GC。而且触发Minor GC时,清理新生代所花费的时间会更长
- 新生代内存设置为能容纳**[并发量*(请求-响应)]**的数据为宜
- 幸存区大到能保留【当前活跃对象+需要晋升对象】
- 晋升阈值配置得当,让长时间存活对象尽快晋升
- 不是
5-5 老年代调优
以 CMS 为例 :
- CMS 的老年代内存越大越好
- 先尝试不做调优,如果没有 Full GC 那么已经…,否则先尝试调优新生代
- 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
-XX:CMSInitiatingOccupancyFraction=percent
最后喜欢的小伙伴别忘了一键三连哦🎈🎈🎈
以上是关于(超详解)JVM-垃圾回收的主要内容,如果未能解决你的问题,请参考以下文章