【JVM】垃圾回收机制

【垃圾回收机制(GC)】

「垃圾回收机制」让Java程序员在写代码时可以放心大胆地new对象,JVM会自动识别,哪些new完的对象再也不用了,就会把这样的对象给释放掉

GC虽然方便,但也有代价

1.GC会让JVM中引入额外的逻辑,消耗不少CPU开销,进行垃圾的扫描和释放,从而影响效率

2.进行GC时,可能会触发STW问题,导致程序卡顿,STW对于性能要求高的场景,就会影响很大

垃圾回收可以回收内存,但JVM中有好几个内存区域,GC负责回收哪里呢?

1.程序计数器,不需要GC,程序计数器只是存一个地址,所占据的空间是固定的,而且这个空间也并非程序员所能干预的

2.栈,和程序计数器差不多

3.堆,是GC的主战场,GC主要针对堆来回收内存,因为new出的对象都存放在堆中

4.元数据区,里面通过类加载的形式存放类对象,一个程序里面要加载的类都是有上限的,因此不会出现无限增长这样的情况,也就不用担心内存泄露

因此,垃圾回收在回收内存上,是以对象为维度进行回收的(回收对象)

【堆上的回收机制】

可以认为堆上有以上这三类内存,中间的部分是需要回收的对象

如果有一个对象,这个对象一部分在使用,一部分不再使用,是否会回收?

不回收 等到整个对象都不再使用,才整个回收

【GC具体的回收步骤】

1.先找出谁是垃圾(不再使用的对象)

需要针对每个对象分别判定是否是垃圾

如何判定?

方案一:引用计数

Java中使用一个对象,一般是通过“引用”来使用的

如果一个对象没有引用指向了,就可以认为这个对象是垃圾了

(对于一些无需通过引用指向的对象,有其他的额外判定方法,如Thread的匿名内部类写法调用start后视为没有指向)

举个例子:

这个代码就是在整个内存空间上创建了“Test”对象,然后在对象中留着它的地址“0x100”,且安排了一个内存空间保存一个“引用计数器”,初始值为0

“a”作为引用变量被创建在内存空间中,a中存放的内容就是引用的地址“0x100”

a指向Test对象,Test对象的引用计数器+1,从0变成了1

此时又来了一个新的引用,且将a这个引用赋给b

意思是内存空间中又创建了一个引用变量“b”,b中存放的地址是“0x100”

通过a和b都能找到Test对象,因此该对象的“引用计数器”就是“2”

现在,将a置null,这样一来只有通过b才能找到Test对象了,因此该对象的“引用计数器”-1,变成了“1”

同理,b置null,该对象的“引用计数器”-1,变成了“0”,该对象的引用指数器变为0了,可以作为垃圾被回收了

综上所述,可以给每个对象分配一个计数器,衡量有多少个引用指向,每次增加一个引用,计数器+1,每次减少一个引用,计数器-1,当计数器减为0,此时对象就是垃圾了

但上述方案存在两个问题:

1.消耗额外的空间

假设test类只有一个int成员(4个字节),此时为了引入引用计数,少说得搞个short(两个字节),这相当于内存多占用了50%,让本来不富裕的内存空间变得更加雪上加霜

2.引用计数可能导致「循环引用」令上述的判定出错

「循环引用」:和死锁类似,举个例子:

此时,执行“a.t=b”,这意味着把b中的地址赋值给a中的Test

这意味着b又有了一个对Test对象的指向,0x200的计数器+1

此时,执行“b.t=a”,这意味着把a中的地址赋值给b中的Test

这意味着a又有了一个对Test对象的指向,0x100的计数器+1

接下来,将a,b置null,这意味着两个指向都没有了

此时发现了问题,这两个对象的引用计数都不是0,因此不能被释放

但这两个对象又无法被使用

方案二:可达性分析

比较粗暴,用时间去换空间

在JVM中,专门搞了一波线程,周期性地扫描代码中所有的对象,来去判定某个对象是否是“可达”(可以被访问到)的,与之对应,不可达的对象就是「垃圾」

换句话说,老师(JVM)手中有一个所有学生(对象)的总名单,针对当前教室(内存空间)内的学生(对象)进行点名操作,被点到的回答到(可以被访问),没有回答到的,就是缺课(不可被访问,垃圾)

可达性分析的起点,称为「GC root」

一个程序中,GC root不是只有一个,而是有很多,比如:

1.栈上的局部变量(引用类型)

2.方法区中,静态的成员(引用类型)

3.常量池引用指向的对象把所有的GC root都遍历一遍,针对每一个尽可能地往下延伸,因此「可达性分析」很耗时间,尤其是在对象很多的情况下

2.释放垃圾的内存空间

知道了谁是垃圾,就需要回收,回收也有一些回收方式

1.标记清楚

直接针对内存中的对应对象进行释放

但这样的做法,会引入「内存碎片问题」

哪些对象要释放,这是一个随机的过程,很可能要释放的多个内存不是连续的,虽然把内存释放掉,但整体这些空闲空间并没有连在一起(是离散的),后续申请内存时很可能申请不了(申请的内存一定是连续的)

这就导致了虽然空间够,但尝试申请空间时会失败

2.复制算法

把整个内存空间一分为二,同一时刻只使用其中的一半

需要回收时,把不是垃圾的对象拷贝到另一侧(确保这些拷贝的数据是连续的),然后把原来这一半空间整个都释放

但这也有缺点

1.内存空间利用率低(只用到了50%)

2.如果存活下来的对象比较多,此时复制成本也比较大

3.标记整理

比较像顺序表中的删除中间元素

假设打对号的元素是垃圾,此时回收时会触发搬运操作,把3搬运到2,把5搬运到4……

这样就构成了“1357”这样的所有有效对象,随后把另一半释放掉,但搬运的开销也是不小的

这三个方案都有缺点,JVM中真实的解决方案,是把上述几个方案综合一下,取长补短,称为「分代回收」

JVM根据对象的“年龄”,把对象进行区分

年轻的对象是「新生代」,年老的对象是「老年代」

衡量年龄的方式,是通过周期性的可达性分析,每次经过一轮扫描,该对象仍然存活(不是垃圾),该对象的年龄就+1

整个堆内存空间就化为了两部分:

新生代区域采取「复制算法」的方式来回收,老年代区域采取「标记整理」的方式来回收

新生代区域中有细致划分:

1.刚new出来的对象放在「伊甸区」

根据经验规律,绝大部分的新对象,活不过第一轮GC

留存下来的对象,拷贝到幸存区,其他整个伊甸区内对象就全部清空了

2.「幸存区」,是两个相等的空间,也是按照复制算法来组织的

新生代中,真正要拷贝的对象不多(经验规律)

3.如果一个对象在幸存区,已经被反复拷贝多次,也不是垃圾,年龄不断增长达到一定限度后,就会把它放到「老年代」区域中

4.根据经验规律:老年代中的对象,生命周期都会比较长(要死早死了,一个对象迟迟没死,说明生命周期很长,大概率会存在很久)

老年代的对象当然还是要可达性分析,只不过GC的频率会降低

老年代通过标记整理进行,需要整理的次数不多,自然开销也降低

此处按照这么一个「分代回收」的机制,不同特点的对象按照不同的方式进行操作,扬长避短,构成了一个综合性地策略,通过这样的策略来更好地解决垃圾回收的机制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值