JVM垃圾回收机制

文章引用:https://blog.csdn.net/antony9118/article/details/51375662
https://blog.csdn.net/antony9118/article/details/51425581

一、背景介绍

与C/C++相比,JAVA并不要求我们去人为编写代码进行内存回收和垃圾清理。JAVA提供了垃圾回收器(garbage collector)来自动检测对象的作用域),可自动把不再被使用的存储空间释放掉,也就是说,GC机制可以有效地防止内存泄露以及内存溢出。

1.JAVA 垃圾回收器的主要任务是:

分配内存
确保被引用对象的内存不被错误地回收
回收不再被引用的对象的内存空间

凡事都有两面性。垃圾回收器在把程序员从释放内存的复杂工作中解放出来的同时,为了实现垃圾回收,garbage collector必须跟踪内存的使用情况,释放没用的对象,在完成内存的释放之后还需要处理堆中的碎片, 这样做必定会增加JVM的负担。

2.为什么要了解JAVA的GC机制?

综上所述,除了作为一个程序员,精益求精是基本要求之外,深入了解GC机制让我们的代码更有效率,尤其是在构建大型程序时,GC直接影响着内存优化和运行速度。
.

二、JVM内存结构

了解JVM的垃圾回收机制,要先了解JVM内存结构。
在这里插入图片描述
其中栈内存可以再细分为java虚拟机栈和本地方法栈 , 堆内存可以划分为新生代和老年代,新生代中还可以再次划分为Eden区、From Survivor区和To Survivor区。
在这里插入图片描述
虚拟机栈和本地方法栈以及程序计数器是线程私有的。
私有内存区伴随着线程的产生而产生,一旦线程中止,私有内存区也会自动消除,因此我们在本文中讨论的内存回收主要是针对共享内存区。
在这里插入图片描述
Java 堆和方法区是线程共享的;
在这里插入图片描述
.

三、堆内存

JVM的垃圾回收机制主要发生在堆内存中,这部分我们会对堆内存进行比较详细的描述。

堆内存是干嘛的?

堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。堆是应用程序在运行期请求操作系统分配给自己的向高地址扩展的数据结构,是不连续的内存区域。用一句话总结堆的作用:程序运行时动态申请某个大小的内存空间。

堆内存又分为新生代:老年代 = 1 : 2
新生代又分为Eden区 : from Survivor(幸存区1) : to Survivor(幸存区2)= 8 : 1 : 1
.

四、堆内存中的新生代

1.为什么要有新生代

不分代不能完成他所做的事情么?其实不分代完全可以,分代的唯一理由就是优化GC性能。你先想想,如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

2.新生代中的垃圾回收机制

刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块幸存区中,先定为幸存区1(年龄加1),Eden被清空;
等Eden区再满了,就再触发一次Minor GC,Eden和幸存区1中的存活对象又会被复制送入第二块幸存区2中,幸存区1和Eden被清空;
等Eden区再满了,就再触发一次Minor GC,Eden和幸存区2中的存活对象又会被复制送入第一块幸存区1中,幸存区2和Eden被清空;
等Eden区再满了,就再触发一次Minor GC,Eden和幸存区1中的存活对象又会被复制送入第二块幸存区2中,幸存区1和Eden被清空;

每一轮幸存区1与幸存区2交换角色,如此循环往复。如果存活对象每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到16岁时,就会被移动到年老代中。

虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold(最大年龄,可以设置)才能晋升到老年代,如果在Survivor区中相同年龄(设年龄为age)的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄(age)的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄,所以并不是对象年龄到16岁才会进入老年区。
.

3.Survivor区分析

①.为什么要有Survivor区

如果没有Survivor,Eden区每进行一次Minor GC(小GC),存活的对象就会被送到老年代。老年代很快被填满,触发Major GC(大GC,因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。老年代的内存空间远大于新生代,进行一次Full GC(全GC)消耗的时间比Minor GC长得多。你也许会问,执行时间长有什么坏处?频发的Full GC消耗的时间是非常可观的,这一点会影响大型程序的执行和响应速度,更不要说某些连接会因为超时发生连接错误了。
在这里插入图片描述
老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和年老代。
Major GC 发生在老年代的GC,清理老年区,经常会伴随至少一次Minor GC,比Minor GC慢10倍以上。所以在实际运行中,Major GC会伴随至少一次 Minor GC,因此也不必过多纠结于到底是哪种GC(在有些资料中看到把full GC和Minor GC等价的说法)。

总结:Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
.

②.两个幸存区的好处**

设置两个Survivor区最大的好处就是解决了碎片化,下面我们来详细分析一下。

为什么一个Survivor区不行?

第一部分中,我们知道了必须设置Survivor区。假设现在只有一个survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC(小GC),Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化
我绘制了一幅图来表明这个过程。其中色块代表对象,白色框分别代表Eden区(大)和Survivor区(小)。Eden区理所当然大一些,否则新建对象很快就导致Eden区满,进而触发Minor GC,有悖于初衷。
在这里插入图片描述
总结:碎片化带来的风险是极大的,严重影响JAVA程序的性能。堆空间被散布的对象占据不连续的内存,最直接的结果就是,堆中没有足够大的连续内存空间,接下去如果程序需要给一个内存需求很大的对象分配内存。。。画面太美不敢看。
这就好比我们爬山的时候,背包里所有东西紧挨着放,最后就可能省出一块完整的空间放相机。如果每件行李之间隔一点空隙乱放,很可能最后就要一路把相机挂在脖子上了。

两个Survivor区又是什么样的呢?

那么,顺理成章的,应该建立两块Survivor区(幸存区),刚刚新建的对象在Eden中,经历一次Minor GC(小GC),Eden中的存活对象就会被移动到第一块幸存区1,Eden被清空;
等Eden区再满了,就再触发一次Minor GC,Eden和幸存区1中的存活对象又会被复制送入第二块幸存区2中(这个过程非常重要,因为这种复制算法保证了幸存区2中来自幸存区1和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生),幸存区1和Eden被清空。
然后下一轮幸存区1与幸存区2交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就会被送到老年代中。下图中每部分的意义和上一张图一样,就不加注释了。
在这里插入图片描述
总结:上述机制最大的好处就是,整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片。

Survivor为什么不分更多块呢?

比方说分成三个、四个、五个?显然,如果Survivor区再细分下去,每一块的空间就会比较小,很容易导致Survivor区满,因此,我认为两块Survivor区是经过权衡之后的最佳方案。

4.一个对象在堆内存中会经历哪些过程?

在这里插入图片描述
如果Major GC之后还是老年代不足,那苍天也救不了了。。。。JVM会抛出内存不足的异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值