当前位置: 首页 > article >正文

【JVM详解三】垃圾回收机制

一、对象是否存活

  • 强引用:Object obj = new Object(); 只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。在不用对象的时将引用赋值为 null,能够帮助垃圾回收器回收对象。比如 ArrayList 的 clear() 方法实现。
  • 软引用(SoftReference):用来描述一些非必须但还有用的对象。在系统将要发生内存溢出异常时回收,若还没有足够的内存才会OOM。
  • 弱引用(WeakReference):也是用来描述非必须对象,强度比软引用更弱;被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器开始工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用:也被称为幽灵引用或幻影引用。该引用最弱,一个对象是否有虚引用的存在完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象。为一个对象设置虚引用关联的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统通知。

1.1 引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就+1;当引用失效时,计数器值就-1;任何时刻计数器为0的对象就是不可能再被使用的。

优点:实现简单,判定效率高。

缺点:假设对象objA和objB都有字段instance,赋值令objA.instance = objB 及 objB.instance = objA,除此之外这两个对象再无任何引用,实际上这两个对象不可能再被访问,但是因为它们互相引用着对方,导致它们的引用计数器都不为0,于是引用计数器无法通知GC收集器回收它们。

1.2 可达性分析算法

基本思路是 通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,它们将会被判定为可回收对象。

GC Roots:一组必须活跃的对象
可作为GC Roots的对象包括以下几种:
  • java虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

finalize()赋予对象重生 :

在可达性分析算法中被标记为不可达的对象,也不一定是一定会被回收,它还有第二次重生的机会。每一个对象在被回收之前要进行两次标记,一次是没有关联引用链会被标记一次,第二次是判断该对象是否覆盖finalize()方法,如果没有覆盖则真正的被定了“死刑”。

如果这个对象被jvm判定为有必要执行finalize()方法,那么这个对象会被放入F-Queue队列中,并在稍后由一个由虚拟机自动创建的、低优先级的finalizer线程去执行它。但是这里的“执行”是指虚拟机会触发这个方法,但是**并不代表会等它运行结束。虚拟机在此处是做了优化的,因为如果某个对象在finalize方法中长时间运行或者发送死循环,将可能导致F-Queue队列中其他对象永远处于等待,甚至可能会导致整个内存回收系统崩溃。

如果要在finalize方法中重生这个对象你可以按照下面代码做:

 注意!finalize()方法只会被系统调用一次,多次被gc只有第一次会被调用,因此只有一次的重生机会。

二、垃圾回收算法

2.1 标记-清除算法

算法分 “标记”和“清除”两个阶段:首先标记出所有需要所有需要回收的对象(可达性分析算法),在标记完成后统一回收所有被标记的对象。之所以说是最基础的算法,是因为后续的收集算法基于这种思路并对其不足进行改进而得到的。
不足之处:
  • 效率问题,标记和清除两个过程效率都不高
  • 空间问题,标记清除后会产生大量不连续的内存碎片,可能会导致后续程序运行过程需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾回收操作。

2.2 复制算法(年轻代GC)

缺点:对象存活率较高时就要进行更多的复制操作,效率将会变低。

最开始是将可用内存分为大小相等的两块,每次只使用其中一块,当这一块内存用完了,就将还存活的对象复制到另一块上,然后再把已使用的那块内存空间清理掉。

因这种算法代价太大,将可用内存缩小到了原来的一半,所以后续IBM公司研究发现年轻代98%的对象都是“朝生夕死”的,并不需要按1:1划分,所以对算法进行了优化。

上图是优化后的划分。这种划分每次年轻代可用内存达到90%。当我们没有办法保证每次回收都只有不多于10%的对象存活时,需要依赖老年代进行分配担保。

空间分配担保:

     在发生Minor GC(Yong GC)之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间。

           如果大于,则此次Minor GC(Yong GC)是安全的。

           如果小于,jdk1.6之前:则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果 HandlePromotionFailure=true ,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。如果大于,则尝试进行一次 Minor GC(Yong GC),但这次Minor GC(Yong GC)依然是有风险的,失败后会重新发起一次 Full GC;如果小于或者HandlePromotionFailure=false,则改为直接进行一次 Full GC。

但是在jdk1.6 update 24之后 -XX:-HandlePromotionFailure 不起作用了,只要老年代的连续空间大于新生代对象的总大小或者历次晋升到老年代的对象的平均大小就进行MinorGC,否则FullGC。

2.3 标记-整理算法

首先标记出所有需要所有需要回收的对象(可达性分析算法),然后让所有存活对象都向一端移动,然后直接清理端边界以外的内存。

2.4 分代收集算法

根据对象的存活周期将不同的内存划分成几块,一般是把 java 堆划分成年轻代和年老代,这样可以根据各个年代的特点分别采用最适当的收集算法。新生代一般选用复制算法,年老代一般选用“标记---清除”或者“标记---整理”算法。

 核心参数:

  • -XX:NewSize :指定新生代初始大小。
  • -XX:MaxNewSize :指定新生代最大大小。
  • -XX:NewRatio :是年老代 新生代相对的比例,比如NewRatio=2,表明年老代是新生代的2倍。老年代占了heap的2/3,新生代占了1/3。
  • -XX:SurvivorRatio :配置的是在新生代里面Eden和一个Survivor比例。
-XX:SurvivorRatio=8表示新生代的Eden占8/10,S1和S2各占1/10.
  • -XX:MaxTenuringThreshold用来定义年龄的阈值,达到阈值进入年老代。默认15。
  • -XX:PretenureSizeThreshold 的意思是大小超过这个值的时候,对象直接在old区分配内存。
  • -XX:HandlePromotionFailure 设置值是否允许担保失败。jdk1.6后失效。
GC过程:
new 的对象直接进入 Eden 区(对象太大直接进入年老代),Eden 区没有足够空间分配时,发起一次 minor gc :Eden 区和From Survivor空间还幸存的对象拷贝到To Survivor空间,然后清空Eden区和From Survivor空间,然后把To Survivor空间和From Survivor空间名字对换,幸存对象age+1。
当执行 minor gc 时To Survivor空间满了,则Eden区中的对象将进入老年代;当对象age=15时也将进入老年代;当Survivor空间age相同的对象(假设为ageA)总和 >= survivor空间的一半时,Survivor空间中age > ageA 的对象也将进入年老代。虚拟机进行minor gc时,当要进入年老代的对象 > 年老代剩余空间大小,将发生 Full GC(整个堆GC,包括年轻代和年老代)。

三、垃圾收集器

3.1 Serial收集器

新生代收集器,采用复制算法;单线程;GC时需要暂停用户线程。最高的单线程收集效率,对于运行在 Client 模式下的虚拟机来说是一个很好的选择。

Serial Old收集器:是Serial收集器的老年代版本;单线程;使用“标记---整理”算法。

3.2 ParNew收集器

新生代收集器,采用复制算法,Serial收集器的多线程版本;GC时需要暂停用户线程。

3.3 Parallel Scavenge收集器

 新生代收集器,采用复制算法;多线程;GC时需要暂停用户线程。和ParNew区别在于Parallel ScaVenge收集器目标是达到一个可控制的吞吐量吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

  • -XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间。大于0的毫秒数,改值缩短需要牺牲吞吐量和新生代空间。
  • -XX:GCTimeRadio 直接设置吞吐量大小。0到100之间的整数,垃圾收集时间占总时间的比例。
  • -XX:+UseAdaptiveSizePolicy 一个开关参数,打开后不需手动指定新生代大小(-Xmn)、Eden区域Survivor空间的比例(-XX:SurvivorRadio)、晋升老年代对象大小(-XX:PretenureSizeThreshold) 等细节参数,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整以提供最合适的停顿时间或者最大的吞吐量,这种条件被称为GC的自适应调节策略(GC Ergonomics)。
Parallel Old收集器:是Parallel Scavenge收集器的老年代版本;多线程;使用“标记---整理”算法;jdk1.6开始提供。

3.4 CMS收集器(重点)

标记-清除算法;以获取低停顿为目标。适合互联网网站或者 B/S(Brower/Server 浏览器/服务器模式)系统的服务端。运作过程分为4个步骤:

  • 初始标记:非并行,需要停止用户线程,标记GC Roots能关联到的对象。很快。
  • 并发标记:并行,不停止用户线程,进行GC Roots Tracing(追踪)的过程。慢。
  • 重新标记:并行,需要停止用户线程,修正并发标记期间因用户程序继续运作而导致标记产生变动的那些对象的标记记录。快
  • 并发清除:并行,不停止用户线程

优点:并发收集,低停顿。

缺点:

  • 对CPU资源非常敏感:并发设计的程序对CPU资源都敏感;在并发阶段会因为占用了一部分CPU资源而导致用户应用程序变慢,总吞吐量会降低。
  • 无法处理浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾产生,这一部分垃圾出现在标记过程之后,所以CMS在本次并发清理过程无法处理掉它们,只好留待下一次GC时再清理。这部分垃圾称为浮动垃圾。由于在垃圾收集阶段用户线程还在运行,所以需要预留足够的内存空间给用户线程使用,因此CMS不能等老年代几乎被填满时再进行收集,需要预留部分空间提供给并发收集时用户线程运行使用。jdk1.5默认老年代使用了68%激活Full GC,jdk1.6改为92%,可通过-XX:CMSInitiatingOccupancyFraction设置。
  • 基于“标记---清除”算法,产生大量空间碎片

3.5 G1收集器(重点)

面向服务端应用,追求低停顿(还能建立可预测的停顿时间模型),基于“标记---整理”算法。

在G1之前的其他收集器进行GC的范围都是整个新生代或者老年代,而G1对java堆的内存布局和其他收集器有很大差别。它将整个java堆分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都是一部分Region的集合。

G1可以有计划的避免在整个java堆中进行Full GC:G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以获取尽可能高的收集效率。

G1中每个Region都有一个与之对应的Rememberd Set,用于避免全堆扫描:虚拟机在发现程序对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region中(在分代的例子就是检查是否是老年代中的对象引用了年轻代的对象),如果是 则通过CardTable把相关引用信息记录到被引用对象所属的Region的Rememberd Set中。当进行内存回收时,在GC Roots的枚举范围内加入Rememberd Set即可保证不对全堆扫描也不会有遗漏。

区域 (Region) 的大小,可以通过 -XX:G1HeapRegionSize 参数指定,大小区间最小 1M 、最大 32M ,总之是 2 的幂次方。默认是将堆内存按照 2048 份均分。

运作过程如下:

  • 初始标记 :非并行,需要停止用户线程,标记GC Roots能直接关联到的对象。
  • 并发标记 :并行,从GC Roots开始对堆中对象进行可达性分析,找出存活对象。
  • 最终标记 :并行,需要停止用户线程,修正并发标记期间因用户程序继续运作而导致标记产生变动的那些对象的标记记录,并将这些变化记录在Rememberd Set Logs中,最后将Rememberd Set Logs数据合并到Rememberd Set中。Rememberd Set : 记录对象在不同分区的引用关系
  • 筛选回收 :并行,需要停止用户线程,首先对各个Region的回收价值和成本进行排序,然后根据用户期望的GC停顿时间来制定回收计划。G1 中提供了 Young GCMixed GC 两种垃圾回收模式,这两种垃圾回收模式,都是 Stop The World (STW) 的。
优点如下:
  • 并行与并发
  • 分代收集
  • 空间整合
  • 可预测的停顿
在以下场景中,G1 更适合:
  • 服务端多核 CPU、JVM 内存占用较大的应用(至少大于 4G);
  • 应用在运行过程中,会产生大量内存碎片、需要经常压缩空间;
  • 想要更可控、可预期的 GC 停顿周期,防止高并发下应用雪崩现象。

3.6 JVM垃圾收集相关参数

3.7 各个版本jdk默认的垃圾收集器

各个版本JDK默认的垃圾回收器:
  • JDK1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
  • JDK1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
  • JDK1.9 默认垃圾收集器G1
//查看当前使用的垃圾收集器
java -XX:+PrintCommandLineFlags -version
-XX:+UseSerialGC :设置串行收集器
-XX:+UseParallelGC :设置并行收集器
-XX:+UseParalledlOldGC :设置并行年老代收集器
-XX:+UseConcMarkSweepGC :设置并发收集器

四、内存分配与回收策略

  • 对象优先在Eden分配
  • 大对象直接进入老年代(-XX:PretenureSizeThreshold 配置对象大小阈值)
  • 长期存活的对象进入老年代(-XX:MaxTenuringThreshold 配置年龄大小阈值,默认15岁)
  • 动态对象年龄判定:在Survivor空间中相同年龄所有对象大小的总和 > Survivor空间的一半时,年龄 > 该年龄的对象就可以直接进入老年代。
  • 空间分配担保:在发生minor gc之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么minor gc可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置的值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次minor gc,尽管此次minor gc有风险;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

http://www.kler.cn/a/540153.html

相关文章:

  • 分布式kettle调度平台- web版转换,作业编排新功能介绍
  • 数据结构与算法(test3)
  • 多头自注意力中的多头作用及相关思考
  • 【系统架构设计师】嵌入式系统之JTAG接口
  • 学习数据结构(6)单链表OJ上
  • 计算机视觉的研究方向、发展历程、发展前景介绍
  • 嵌入式硬件篇---OpenMV的硬件流和软件流
  • 使用Chisel建立端口转发与SOCKS5代理隧道
  • [含文档+PPT+源码等]精品大数据项目-Django基于大数据实现的心血管疾病分析系统
  • 使用OpenGL自己定义一个button,响应鼠标消息:掠过、点击、拖动
  • 深度学习-利用预训练的 ResNet 和 DenseNet 模型进行医学影像诊断
  • HiveQL命令(二)- 数据表操作
  • 自动驾驶数据集三剑客:nuScenes、nuImages 与 nuPlan 的技术矩阵与生态协同
  • [LVGL] 在VC_MFC中移植LVGL
  • linux基础命令1
  • 【紫光同创PG2L100H开发板】盘古676系列,盘古100Pro+开发板,MES2L676-100HP
  • Layui树节点添加level属性
  • 【Linux】31.Linux 多线程(5)
  • Python+Flask搭建属于自己的B站,管理自己电脑里面的视频文件。支持对文件分类、重命名、删除等操作。
  • 日志统计(acWing,蓝桥杯)
  • PLSQL: 存储过程,用户自定义函数[oracle]
  • python-leetcode-组合总和
  • win10 llamafactory模型微调相关① || Ollama运行微调模型
  • 【论文阅读】Comment on the Security of “VOSA“
  • 并查集知识整理、蓝桥杯修改数组
  • 【vue】高德地图AMap.Polyline动态更新画折线,逐步绘制