嗨,您好 我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者
推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代
文章所在专栏:JVM
我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识
向我询问任何您想要的东西,ID:vnjohn
觉得博主文章写的还 OK,能够帮助到您的,感谢三连支持博客
代词: vnjohn
⚡ 有趣的事实:音乐、跑步、电影、游戏
目录
在 Java 中,判定对象是否存活指的是哪些不再被程序所引用,也无法通过任何方式访问的对象;具体来说,当一个对象不再被任何活动线程所引用,并且没有被其他对象所引用时,它就被认为是 “死亡” 对象;“死亡” 对象占用内存空间但不再有任何实际的用途,因此需要通过垃圾收集机制将其从内存中释放,以便重新利用内存资源;Java 垃圾收集机制会自动识别、回收这些 “死亡” 对象,无需程序员手动管理内存释放的过程
没有引用指向或根对象不可达的引用对象,被称之为垃圾
Java 与 C++ 对垃圾的处理方式有所不同,如下:
Java:自动回收垃圾,由 GC 回收机制去自动回收,开发效率高,执行效率低
C++:手动处理垃圾,若忘记回收时,容易发生内存泄漏,回收多次,会出现非法访问问题,一般手动回收的代码(delete)会写在析构函数中;开发效率低,执行效率高
在堆里面存放着 Java 世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象哪些还 “存活” 着,哪些对象已经 “死亡” 了
引用计数算法(Reference Counting):在对象中添加一个引用计数器,每当有一个其他对象引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的
引用指向一个对象,在它脑袋上写一个数字,有几个对象指向它就在它脑袋上写一个几,当这个数字变为 0 时,就说明没有任何对象指向它,即为 “垃圾”
在 Java 领域中,至少主流的 Java 虚拟机都没有选用引用计数算法来管理内存,主要原因:一个看似很简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能正确地工作
譬如单纯的引用计数算法就很难解决对象之间相互引用的问题,例如:A->B、B->A,A、B 计数器的值都为 1,所以在当前算法来说不是垃圾,但从此看来,没有其他的引用会使用到它们,按理来说这几个都应该是为 “垃圾”
以上会出现的对象之间互相引用问题,通过代码来演示,看 Java JVM 中是否使用到了引用计数算法来回收垃圾,如下:
/**
* @author vnjohn
* @since 2023/6/29
*/
public class ReferenceCountingGC {
public Object instance = null;
private byte[] bigSize = new byte[2 * 1024 * 1024];
public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
调整 JVM Options 参数,增加打印 GC 回收详情信息,如下:
# 打印 GC 回收时间、GC 回收详情
-XX:+PrintGCTimeStamps -XX:+PrintGCDetails
控制台打印结果如下:
0.107: [GC (System.gc()) [PSYoungGen: 6717K->608K(76288K)] 6717K->616K(251392K), 0.0044845 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
0.112: [Full GC (System.gc()) [PSYoungGen: 608K->0K(76288K)] [ParOldGen: 8K->378K(175104K)] 616K->378K(251392K), [Metaspace: 3100K->3100K(1056768K)], 0.0026966 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 76288K, used 3277K [0x000000076ab00000, 0x0000000770000000, 0x00000007c0000000)
eden space 65536K, 5% used [0x000000076ab00000,0x000000076ae334d8,0x000000076eb00000)
from space 10752K, 0% used [0x000000076eb00000,0x000000076eb00000,0x000000076f580000)
to space 10752K, 0% used [0x000000076f580000,0x000000076f580000,0x0000000770000000)
ParOldGen total 175104K, used 378K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000)
object space 175104K, 0% used [0x00000006c0000000,0x00000006c005e9e8,0x00000006cab00000)
Metaspace used 3130K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 343K, capacity 388K, committed 512K, reserved 1048576K
重点看前面两行日志信息
年轻代:0.107: [GC (System.gc()) [PSYoungGen: 6717K(回收前大小)->608K(回收后大小)(76288K)] 6717K->616K(251392K), 0.0044845 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
老年代:0.112: [Full GC (System.gc()) [PSYoungGen: 608K(回收前大小)->0K(回收后大小)(76288K)] [ParOldGen: 8K->378K(175104K)] 616K->378K(251392K), [Metaspace: 3100K->3100K(1056768K)], 0.0026966 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
以上标注黄色背景的日志内容,可以看出 Java 虚拟机并没有因为这两个对象循环引用而放弃回收它们,这也从侧面说明了 Java 虚拟机并不是通过引用计数算法来判断对象是否存活的
JDK 8 默认使用的垃圾收集器:Parallel Scavenge、Parallel Old
Java中常见的垃圾收集器,如:Serial、Parallel、CMS、G1 等,并不使用引用计数算法,而是采用基于可达性分析的算法来进行垃圾回收,这个也是后面要讲到的算法
可达性分析算法(Reachability Analysis):它是一个更广泛的概念,它是一类以可达性作为判断对象是否存活基础的算法,除了根可达算法外,还有其他的可达性分析算法,如:可达性分析与复制算法、可达性分析与标记-清除算法等
根可达算法是可达性分析算法中的一种具体实现方式,根可达算法是从一组称为 “GC Roots” 根对象开始,通过引用关系向下搜索,搜索过程所走过的过程称为 “引用链”;若某个对象到 GC Roots 间没有任何引用链相连或者用图论的方式来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的
如上图,在 Java 技术体系中,固定可作为 GC Roots 对象包括以下几种:
比如:main 方法主线程开始运行,主线程栈中变量调用了其他的方法,主线程栈中的方法访问到的对象叫根对象
静态变量初始化,能够访问到的对象称之为根对象
若一个 class 能够用到其他的 class 对象称之为根对象
GC Roots 是垃圾收集器判断对象是否存活的起点,不同的垃圾收集器会根据 GC Roots 选择合适的垃圾回收算法来进行垃圾回收、内存管理,它们的共同协作以确保内存的有效利用和程序的正常执行
常见的垃圾回收算法:复制(Copying)算法、标记-清除 (Mark-Sweep)算法、标记-整理(Mark-Compact)算法
至于很多人说还有分代算法,在我看来,分代模型应该是最准确的说法,分代算法不是指具体的一种算法,而是一种垃圾回收的策略或模型
由于对象的生命周期大部分是朝生夕死的,只有少数对象是长期存活的,基于此,垃圾收集器将堆内存划分为不同的代,分代模型将堆内存主要划分为新生代(Young Generation)和老年代(Old Generation)
G1 垃圾收集器逻辑分代,物理不分代
ZGC、Shenandoah 垃圾收集器没有物理分代,也没有逻辑分代
其他的垃圾收集器一般要么作用于新生代要么作用于老年代,例如:Parallel Scavenge-新生代、Parallel Old-老年代
关于垃圾回收算法、垃圾收集器后续文章见分晓,这里不过多展开~
该篇博文讲解判定对象是否存活的条件通过什么方式去做的,引用计数器算法、根可达算法,在引用计数器算法中,通过简单的案例来演示在 Java 程序中并未通过该算法来判定对象是否存活,而是通过根可达算法去作判别的,罗列了 Java GC Roots 不同的种类,简要阐述了为下文作铺垫的垃圾回收算法、垃圾收集器,希望能先带你一起了解这方面的前置知识!
参考文献:《深入理解 Java 虚拟机》周志明著
博文放在 JVM 专栏里,欢迎订阅,会持续更新!
如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!
推荐专栏:Spring、MySQL,订阅一波不再迷路
大家的「关注❤️ + 点赞 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!