《深入理解Java虚拟机》(第二版)学习2:垃圾收集算法
对象存活判断
在对堆进行回收之前虚拟机需要判断这些对象中那些是“存活”的。
引用计数算法(Reference Counting)
原理:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为0的对象就是不可能再被使用的。
优点:实现简单,判定效率高。
缺点:很难解决对象之间相互循环引用的问题。
JVM并没有使用这种判断方法,而是被广泛使用在FlashPlayer、Python等语言上。
可达性分析算法(Reachability Analysis)
在主流的商用程序语言(Java、C#)的主流实现中,都是使用可达性分析(Reachability Analysis)来判断对象是否存活的。
原理:通过一系列的称为“GC Root”的**引用(Reference)**作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Root 没有任何引用链相连(即从 GC Root 到这个对象不可达)时,则证明此对象是不可用的。
注:这里在书中成称 GC Root 为对象,个人看了知乎上R大的回答后,感觉引用更适合。引用是存放在栈上的变量,对象则是存放在堆中的数据,而我们平时都通过引用(即引用变量)来操作对象。
我们来理解一下,假设垃圾回收是一条河,而堆里面的对象则是这条河上的船,那么 GC Roots 就是河中的锚点。锚点肯定是不会被河水冲走的,与锚点有联系的船(也就是可达的对象)同样也不会被冲走;而与锚点断开联系的船(即不可达的对象),则会慢慢被河水冲走(即被垃圾回收)。
如上图,object5、object6、object7虽然互相有关联,但是它们到GC Roots是不可达的,所以他们会被判定为是可回收的对象。
哪些对象可以被作为 GC Roots 呢?
在Java语言中,可以作为 GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的局部变量表)中所引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
从垃圾回收谈 GC Root
在young GC中不收集 old gen,所以 old gen 里的对象此时全部都会被认为是活的,那么从这些对象出发指向 young gen 的引用就必须被认为是活的。因而把这些引用看作 root set 的一部份,所以对于 young GC 比 full GC 的 GC roots 还要更多一些。
生存还是死亡
然而,即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。
第一次标记
如果对象在进行可达性分析后发现没有 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有重写(Override)finalize()方法,或者 finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
第二次标记
如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列里面,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会触发这个方法,但并不会承诺会等待它们运行结束,原因是,如果一个对象在 finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能导致 F-Queue 队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己赋值给某个类变量或者对象的成员变量,那在第二次标记中它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
在一下次 GC 中该逃脱的对象只会再进行一次标记即会被回收。
finalize()方法的缺点
finalize()方法并不推荐大家去使用,书上介绍它说“运行代价高昂、不确定性大,无法保证各个对象的调用顺序”。
其实更直观的因素还有它使用不当的话很容易导致 OOM(内存溢出),如下:
/*** @author 小关同学* @create 2021/10/4*/
public class FinalizeTest {public static class Test{private byte[] content = new byte[1024*1024];protected void finalize(){System.out.println("finalize方法被执行");}}public static void main(String[] args) throws Exception{for (int i = 0;i<1000;i++){Test test = new Test();}}
}
# 虚拟机参数设置:
# 最大堆内存
-Xmx5m
# 最小堆内存
-Xms5m
执行结果:
如上图,发生了内存溢出,原因前面也讲到,第二次标记过程是由一个低优先级的 Finalizer 线程执行的,当主线程在疯狂创建新的Test对象占用空间时,Finalizer 线程还在慢悠悠地执行,这样就导致了清除内存空间的速度跟不上内存空间被占用的速度,进而发生了内存溢出。
垃圾收集算法
标记-清除算法(Mark-Sweep)
“标记-清除”(Mark-Sweep)算法是最基础的收集算法,后续的收集算法都是基于它进行改进而得到的,它分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象(即前面的两次标记)。
上图来自https://www.jianshu.com/p/74727c856da4
缺点:
- 效率问题,标记和清除这两个过程的效率都并不高;
- 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多会导致以后需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法(Copying)
“复制”(Copying)算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉,解决了内存碎片的问题。对象存活率越高,效率越低。
现在的商业虚拟机都是采用复制算法来回收新生代,它们将内存划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中的一块 Survivor 。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚刚使用过的 Survivor 空间。
HotSpot JVM 把年轻代分为了三部分:1个 Eden 区和2个 Survivor 区(分别叫 From 和 To),默认比例为 8:1:1,并且默认 Eden 和 From Survivor 的大小比例是 8:1 ,也就是说每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10% 的内存(To Survivor区域)会被“浪费”。
注意:当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
收集过程描述:
在 GC 开始的时候,对象只会存在于 Eden 区和 From Survivor 区,To Survivor 区是空的。
紧接着进行 GC ,Eden 区中所有存活的对象都会被复制到 To Survivor 区
而在 From Survivor 区中,仍存活的对象会根据他们的年龄值来决定去向(对象在 From Survivor 区中每熬过一次 Minor GC,年龄就会增加1岁)。年龄达到一定值(年龄阈值,可以通过
-XX:MaxTenuringThreshold
来设置)的对象会被移动到老年代中,没有达到阈值的对象会被复制到 To Survivor 区域。经过这次 GC 后,Eden 区和 From Survivor 区已经被清空。这个时候,From Survivor 和 To Survivor 会交换他们的角色,也就是新的 To Survivor 就是上次 GC 前的 From Survivor,新的 From Survivor 就是上次 GC 前的 To Survivor。不管怎样,都会都会保证名为 To Survivor 区域是空的,即永远有一块 Survivor Space 是空的。Minor GC 会一直重复这样的过程,直到 To Survivor 区被填满(即被复制到 To Survivor 区域时,To Survivor 区域一下子装不下了),To Survivor 区被填满之后,会将所有对象直接移动到年老代中。
标记-整理算法(Mark-Compact)
“标记-整理”(Mark-Compact)算法是针对老年代进行回收的算法,它的标记过程和“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。
上图来自:https://blog.51cto.com/u_4837471/2324575
分代收集算法(Generational Collection)
“分代收集”(Generational Collection)算法根据对象存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,这样就可以根据个人年代的特点采用最适当的收集算法。
新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,那就采用复制算法,只需要付出少量存活对象的复制成本就可完成收集;
而老年代中因为对象存活率过高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。
算法实现原理
枚举根节点
枚举根节点也就是查找 GC Roots,我们前面说过,在HotSpot虚拟机中,是通过可达性分析来判断此对象是否需要回收的。那可达性分析就需要找到“源头”,也就是根节点(GC Roots)。
问题
- 消耗大量时间。
从前面可达性分析知道,GC Roots主要在全局性的引用(常量或静态属性)和执行上下文中(栈帧中的本地变量表);
要在这些大量的数据中,逐个检查引用,会消耗很多时间; - GC停顿。
可达性分析期间需要保证整个执行系统的一致性,对象的引用关系不能发生变化;
导致GC进行时必须停顿所有Java执行线程 (称为 “Stop The World”,STW );
(几乎不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的)
STW 是 JVM 在后台自动发起和自动完成的;在用户不可见的情况下,把用户正常的工作线程全部停掉;
总结:要可达性分析耗费的时间,让 SWT 的时间尽可能缩短,从而减少虚拟机停顿的时间。
解决方法
为了减少可达性分析所耗费的时间,Hotspot 虚拟机使用了一组称为 OopMap 的数据结构,使虚拟机可以直接得知哪些地方存放着对象引用 ,不用每次直接遍历整个引用链,所以执行系统停顿下来后,并不需要全部、逐个检查完全局性的和执行上下文中的引用位置。
在 Hotspot 中,对象的类型信息都有自己的 OopMap ,记录了在该类型的对象内什么偏移量上是什么类型的数据。所以从对象开始向外的扫描可以是准确的,而这些数据是在类加载过程中计算得到的。
OopMap 的作用:
- 避免全栈扫描,加快枚举根节点的速度
- 帮助 Hotspot 实现准确式 GC
保守式 GC
保守式 GC 是不能识别指针(引用)和非指针的 GC
缺点:
- 不能识别引用和非引用会导致部分对象本来应该是要被回收的,但有疑似指针指向它们,使它们逃过 GC 的收集;
- 由于不知道疑似指针是否就是真的指针,所以它们的值都不能改写;移动对象就意味着修改指针,因此只能使用不移动对象的 GC 算法,如:Mark-Sweep
半保守式 GC
半保守式 GC 在栈上不记录引用信息,在对象上记录引用信息
缺点:
- 运行时,需要在对象上带有足够多的元数据
准确式 GC
虚拟机要知道内存中所有位置上的数据具体是什么类型,是不是指向 GC 堆里面的引用。
几种准确式 GC 的实现方法:
- 让数据自身带上标记(tag)(在半保守式 GC 中较为常见);
- 让编译器为每个方法生成特别扫描码;
- 从外部记录下类型信息存成映射表,如:Hotspot 中的 OopMap。
安全点(Safe Point)
有了 OopMap,HotSpot 可以快速准确完成 GC Roots 枚举。但是另一个问题来了,我们要在什么地方创建 OopMap?
程序运行期间,引用的变化在不断发生,如果每一条指令都生成 OopMap,那占用空间就太大了,所以有了安全点(Safe Point)。
只在安全点进行 GC 停顿,只要保证引用变化的记录完成于 GC 停顿之前就可以。
如何选定安全点
安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的,“长时间执行”的最明显特征就是指令序列复用,例如在方法调用、循环跳转、异常跳转等,所以具备这些功能的指令才会产生 Safe Point。
安全点如何让线程中断
有两种方案可以选择:
- 抢占式中断(Preemptive Suspension)
在 GC 发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。 - 主动式中断(Voluntary Suspension)
当 GC 需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
HotSpot 使用主动式中断。
安全区(Safe Region)
安全点保证了程序执行时,在不长的时间里就会遇到可进入 GC 的安全点,但如果线程没有分配 CPU 时间,必须线程处于 睡眠(Sleep) 或阻塞(Blocked)状态,就无法响应 JVM 的中断请求,走到安全点去挂起。安全区(Safe Region)解决了这一问题。
安全区是指在一段代码片段中,引用关系不会发生变化。在这个区域中的任意地方开始 GC 都是安全的。
当代码执行到安全区时,首先标识自己已经进入了安全区,那样如果在这段时间里 JVM 发起 GC,就不用管标识自己在安全区的那些线程了,在线程离开安全区域时,会检查系统是否已经完成了根节点枚举(或者是整个 GC 过程),如果完成了,那线程就继续执行,否则它就必须等待,直到收到可以安全离开安全区的信号为止。
PS:也可以到我的个人博客查看更多内容
个人博客地址:小关同学的博客
相关文章:

Android 知识杂记(MVP模式)
MVP的模式在于将原来activity中业务逻辑的部分剥离出来,代码示例如下: Accountpublic class Account {private String mUsername;private String mPassword;public String getmUsername() {return mUsername;}public void setmUsername(String mUsername…

玩转百度即用API(2)——身份证查询
2019独角兽企业重金招聘Python工程师标准>>> 第二个即用API,身份证查询 示例代码: #-*- coding: utf-8 -*- #version:0.1 #note:该即用API接口简单,输入身份证只能查到3个信息:大概地址,性别&am…

解决Ubuntu环境下不能QT不能输入中文
参考链接: https://blog.csdn.net/baidu_33850454/article/details/81212026?utm_mediumdistribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_sourcedistribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2…

UE5虚幻引擎5中的实时特效学习 Introduction to real time FX in Unreal Engine 5
MP4 |视频:h264,1280720 |音频:AAC,44.1 KHz,2 Ch 语言:英语中英文字幕(根据原英文字幕机译更准确) |时长:40节课(3h 36m) |大小解压后:2.65 GB 含课程文件 从零开始使用尼亚加拉在虚幻引擎中学习实时外汇…

《深入理解Java虚拟机》(第二版)学习3:垃圾收集器
垃圾收集器 如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。 我们这里讨论的收集器主要是基于JDK 1.7 Update 14之后的 Hotspot VM 。 Serial 收集器 Serial 收集器是最基本、发展历史最悠久的收集器,曾经(在 …

2016寒假训练3
题目链接 A CodeForces 362A Two Semiknights Meet 题意:在一个棋盘中有两个定义了特殊走法的棋子,同时移动他们,问是否会相遇(只能在合法的位置)。 做法:直接暴力dfs处理出这两个棋子到达各个位置的时间&a…

2015化妆品行业如何进行微信公众号营销
对比其他行业发展特点发现化妆品行业具有以下特点: 1、化妆品行业呈现节日、电商促销影响明显、口碑评价关注度高、女性网民占比高冲动消费多、整体用户年轻化等特点。 2、化妆品行业即使在各行业发展低迷期依然保持良好发展势头。 3、化妆品商家的顾客回头率高。 4…

在新建好的ROS空间里面添加功能包
第一步:创建功能包 cd catkin_ws打开src ~/catkin_ws/src新建文件夹名字 catkin_create_pkg (文件加名字) roscpp rospy std_msgs打开新建文件夹中的src ~/catkin_ws/src/(新建文件夹名字)/src输入: gedit 文件.cpp返回工作空间: 执行catkin build 编译后即可执行

Revit的Enscape基本培训(2021) Enscape Essential Training for Revit (2021)
MP4 |视频:h264,1280720 |音频:AAC,44.1 KHz,2 Ch 语言:英语中英文字幕(根据原英文字幕机译更准确) |时长:2h 53m |大小解压后:2.23 GB 含课程练习文件 如果您使用Revit,您可能需要学习Enscape&…

设计模式学习2:单例模式
单例模式 所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。 比如Hibernate的SessionFactory,它充当数据存储源的代理…

数据库插入时,标识列插入显式值
当 IDENTITY_INSERT 设置为 OFF 时,不能为表 CU_GiftExchange 中的标识列插入显式值。SET IDENTITY_INSERT [dbo].[CU_GiftExchange] ONGO转载于:https://blog.51cto.com/hezun/1631849

Ubuntu终端显示文本让选择确定,OK等等
问题1:首先按下"TAB"键,会看到选中文本"确定","OK"然后按下回车键,即可!

Unity安卓游戏开发:打造7款2D 3D游戏 Unity Android Game Development : Build 7 2D 3D Games
流派:电子学习| MP4 |视频:h264,1280720 |音频:AAC,44.1 KHz 语言:英语中英文字幕(根据原英文字幕机译更准确) |大小:15.4 GB |时长:32h 55m Unity游戏开发与设计,用C# & Unity学习Unity安卓游戏开发(更…

唯一索引和普通索引的选择
前言:最近在研究阿里的开发手册中关于 MySQL 的一些规定,所以来记录一下学习中的心得 唯一索引和普通索引的选择 【强制】业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。 说明:不要以为唯一索引影响了…

Springboot整合HBase——大数据技术之HBase2.x
Apache HBase 是以hdfs为数据存储的,一种分布式、可扩展的noSql数据库。是一个高可靠性、高性能、面向列、可伸缩的分布式存储系统,利用HBase技术可在廉价PC Server上搭建起大规模结构化存储集群。HBase使用与BigTable(BigTable是一个稀疏的、分布式的、持久化的多维排序map)非常相似的数据模型。用户将数据行存储在带标签的表中。数据行具有可排序的键和任意数量的列。该表存储稀疏,因此如果用户喜欢,同一表中的行可以具有疯狂变化的列。

一个合格的Java选手必须要掌握的并发锁知识
Java内置锁:基于Java语法层面(关键词)实现的锁,主要是根据Java语义来实现,最典型的应用就是synchronized。Java显式锁:基于JDK层面实现的锁,主要是根据基于Lock接口和ReadWriteLock接口,以及统一的AQS基础同步器等来实现,最典型的有ReentrantLock。使用方式:synchronized关键字互斥锁主要有作用于对象方法上面,作用于类静态方法上面,作用于对象方法里面,作用于类静态方法里面等4种方式。

终于有人把Web 3.0和元宇宙讲明白了
分散的数据网络使个人数据(例如个人的健康数据、农民的作物数据或汽车的位置和性能数据)出售或交换成为可能,与此同时,不会失去对数据的所有权控制、放弃数据隐私或依赖第三方平台来管理数据。Web 3.0的目标是在创作者经济中取得更好的平衡。互联网第二次迭代(Web 2.0)的缺陷,加上公有区块链技术的诞生,帮助我们朝着更加去中心化的Web 3.0 迈进,元宇宙和更广泛的去中心化网络都是关于现实世界和虚拟世界的融合。此时的网络中不再是静态内容,而是动态的内容,用户现在可以与发布在网络上的内容进行交互。

ubuntu下安装 python 常用软件
1、用于科学计算的常用包: sudo apt-get install python-numpy python-scipy python-matplotlib ipython ipython-notebook python-pandas python-sympy python-nose 包括,numpy, scipy, matplotlib, ipython, ipython-notebook, pandas, sympy, nose 2、…

《团队项目开发之三对一维环形数组的求解》
《团队项目开发之三对一维环形数组的求解》 设计思想:通过把数组的长度扩大为原来的一倍,相当于新数组是由对原来的数组重复了一遍后而组成的,这样保证了数组以环状的形式,按照数组中每个数字的位序依次对它们可能形成的最大子数组…

PX4修改线程内存大小
当编译时出现错误: 在CMakeLists.txt文件中修改内存大小 px4_add_module(#下面添加文件夹名字MODULE modules__position_control#下面添加线程名字MAIN a#线程内存大小STACK_MAIN 4000SRCS#添加文件夹里面.cpp文件main.cppDEPENDS)修改后,再次编译就不…

Blender液体烟与火VFX特效制作教程 Blender VFX Liquid Smoke Fire
流派:电子学习| MP4 |视频:h264,1280720 |音频:AAC,48.0 KHz 语言:英语|大小:6.60 GB |时长:7h 44m Blender了解三维模型如何创建模拟和动画的简单方法 你会学到什么 完成创建两个鬼魅万圣节场景的完整指南,包括模拟和全动画元素 使用Blender…

Ajax+SpringBoot+Thymeleaf使用中遇到的跳转页面问题
前言:这周在使用 AjaxThymeleaf 时遇到一个问题,折腾了我很久,在此记录一下 AjaxSpringBootThymeleaf使用中遇到的跳转页面问题 问题描述 我的目的:通过 Ajax 获取数据,并通过 Model 渲染 View ,实现跳转…

数据库分离 脱机
数据库分离:一般默认情况下数据库在联机状态下我们不能对数据库文件进行任何复制删除等操作,如果将数据库分离的话就可以对数据文件进行复制、剪切、删除等操作了。一般想直接备份数据文件,就先分离数据库,之后把数据文件复制到别…

年卡在手,城墙我走: 记葡萄城控件团队建设
上个周六,赶着春天的尾巴,《葡萄城控件业务团队》进行了一次有趣的团队建设:西安城墙半日游。 拿到《2015陕西旅游年票一卡通》,还是小小激动一下,据说有888个景点可以免费游玩呢。 从葡萄城到西安南门,这个…

PX4代码框架
src:目录是源码目录存放所有的源码,源码的查看都应该在这里。 mavlink:是MAvlink的库目录,源码要调用这个库,如果我们要修改和添加MAVLINLK消息ID也是在这个目录下面。 NuttX:是NuttX的系统库,…

Houdini魔法特效制作学习教程
大小解压后:12.1G 1920X1080 mp4 语言:英语中英文字幕 Houdini已经成为特效行业的热门工具。 谁运用它的力量,谁就势不可挡。 魔法咒语、科幻盾牌和电磁脉冲冲击波。掌握特效的艺术元素是这里的目标,我们将指导您完成它。 魔法通常…

关于SpringBoot项目通过jar包启动之后无法读取项目根路径静态资源
前言:这个是昨天晚上在部署一个项目的时候发现的,在此记录一下 关于SpringBoot项目通过jar包启动之后无法读取项目根路径静态资源 问题描述 在部署了一个项目之后,打开项目页面进行测试,发现有一个查询页面查询失败了࿰…

JavaScript高级程序设计(第3版)第六章读书笔记
第六章 面向对象的程序设计 1. 数据属性 [[Configurable]]:表示能否通过delete删除属性从而重新定义属性。默认值为true。 [[Enumerable]]:表示能否通过for-in循环返回属性。默认值为true。 [[Writable]]:表示能否修改属性的值,默…

2022-2028年中国钢丸行业市场行情监测及未来前景规划报告
【报告类型】产业研究 【报告价格】4500起 【出版时间】即时更新(交付时间约3个工作日) 【发布机构】智研瞻产业研究院 【报告格式】PDF版 本报告介绍了中国钢丸行业市场行业相关概述、中国钢丸行业市场行业运行环境、分析了中国钢丸行业市场行业的…

[MySQL]Every derived table must have its own alias
2019独角兽企业重金招聘Python工程师标准>>> 关于这条报错信息,意思是指每个派生出来的表都必须有一个自己的别名。 _mysql_exceptions.OperationalError: (1248, Every derived table must have its own alias) 如下两条select语句可以说明这个问题&am…