这是我参与「第四届青训营 」笔记创作活动的第 3 天,今天自主学习了Java系统的GC垃圾回收机制,因为面试常常会问到,所以系统整理了一遍相关概念。
如何判断是否是垃圾
引用计数法
主要:判断对象的引用数量
方式:
给对象添加一个引用计数器,当有引用时计数器加1,当引用失效时计数器减1。当某个对象的计数器变为 0 ,就会被回收
但这种JVM并没有采用,因为不能解决对象之间的循环引用问题。 如:A和B为两个相互引用的对象(A对象有一个属性为B,B对象有一个属性为A,这种互相引用的情况,GC无法识别)
可达性分析法
主要:通过对象引用链判断对象是否需要被引用
方式:通过一些类GC Roots对象作为起始点,从这些根节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明对象不可用的,需要回收该对象。
回收时间
取决于垃圾回收器何时运行(与C++不同,C++是可以明确确定何时确定)
- CPU空闲的时候自动回收
- 堆内存满了以后
- 主动调用System.gc()后尝试回收 (在调用时,垃圾收集器会回收未使用的内存空间,尝试释放被丢弃对象占用的内存。但会带一个免责声明,无法保证对垃圾收集器的调用)
回收算法
标记-清除算法
步骤
- 标记:标记所有需要回收的对象
- 清除:进行统一回收带标记对象占据的内存空间。
缺点
- 回收速度慢,效率低
- 会产生大量不连续的内存碎片。(导致需要分配内存给较大对象时,无法找到足够的连续内存)
复制算法
步骤
- 划分分配:将内存划分为大小相等的两块,每次只使用其中一块。
- 复制转移:当一块内存使用完后,将还存活的对象复制到另一块上
- 清除:把已使用空间一次性清除。
优缺点
- 优点:不需要考虑内存碎片问题
- 缺点:存储缩小到原来一半
标记-整理算法
类似于标记—清除
与标记-清除区别
- 标记-清除:仅对不存活对象进行处理,剩余不存活对象不做任何处理
- 标记-整理:对不存活对象进行清除,对存活对象重新整理
优缺点
- 不会产生内存不连续现象
分代收集算法
是JVM使用最多的一种算法,在具体场景下自动选择以上三种算法 不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率
- 当代商用虚拟机使用的都是分代收集算法
- 新生代对象存活率低,就采用复制算法;
- 老年代存活率高,就用标记清除算法或者标记整理算法。
相关知识
堆内存分为以下三个模块:
-
新生代:一般情况下新生成的或者朝生夕亡的对象一般都是首先存放在新生代里面。
新生代将内存按照8:1:1分为一个Eden和so,s1三个区域;
- 大部分对象都在Eden区域生成,
- 在垃圾回收时,先将Eden存活的对象复制到s0区,然后清除Eden区
- 当这个s0区满了,则将Eden区和s0区的存活对象复制到s1,然后将Eden和s0区清空,此时s0是空的,
- 然后交换s0和s1的角色(即下次回收会扫描eden和s1区),即保持s0为空,
- 如此往复; (当s1不足以存放Eden和s0存放的对象时,则将对象直接放到老年代)
适用回收算法:复制算法 在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成垃圾收集
-
老年代:存放生命周期比较长的对象,比如新生代中经历来了n次垃圾回收后仍然存活的对象都进入了老年代。 适用算法:标记整理 和 标记清除 在老年代中因为对象存活率较高,没有额外的空间对它分配担保,就必须使用标记清除或标记整理
-
永久代:存放静态文件(java类、方法。对GC没有显著影响)
垃圾回收类型
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。垃圾回收有两种类型,Minor GC 和 Full GC。
Minor GC:
不影响老年代:对新生代进行回收,不会影响到年老代。
因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成
Full GC:
也叫 Major GC,对整个堆进行回收,包括新生代和老年代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数
导致Full GC的原因包括:
- 老年代,永久代被写满
- System.gc()被显式调用等。
垃圾回收器
CMS垃圾回收器
全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器,对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。
基础算法
标记——清除
过程
- 初试标记
- 并发标记
- 并发预清理
- 重新标记
- 并发清理
- 并发重置
- 初始标记: 在这个阶段,需要虚拟机停顿正在执行的任务。从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。
- 并发标记: 在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以 用户不会感受到停顿 。
- 并发预清理:并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段"重新标记"的工作,因为下一个阶段会Stop The World。
- 重新标记:这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从"根对象"开始向下追溯,并处理对象关联。
- 并发清理: 清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。
- 并发重置: 这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收。
相关概念
Garbage Collection GC
垃圾回收器并不总是工作,工作时间:只有当内存资源告急时,垃圾回收器才会工作;即使垃圾回收器工作,finalize方法也不一定得到执行,这是由于程序中的其他线程的优先级远远高于执行finalize()函数线程的优先级。(这是楼下的答案)
- 垃圾回收机制优先级很低:保证不再使用的内存会被及时回收。程序运行时至少会有两个线程,一个是主线程,一个就是GC
- GC不能允许开发者明确指定释放拿一个元素:java的内存回收是自动在后台运行的,只能调用 System.gc() 或 finalize() 建议回收,但只会在JVM执行时进行(也不一定执行),具体时间未定。
- GC对内存溢出有一定防止作用:当程序严重时还是会溢出
- 不会回收Dead状态的线程:Dead线程可以回复
- finalize() :一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法, 并且在下一次垃圾回收动作发生时,才会真正的回收对象占用的内存
\