JVM~垃圾收集机制

垃圾:在java中指的是已经不需要使用的对象,它们占用着内存空间,却已经失去了作用,如果不进行回收,在运行一段时间后虚拟机将会因为内存不足而终止运行。

由于java不像C/C++,分配内存后每次都需要手动free掉,java将这项工作交给了虚拟机来做,带来了很大的便利。

判定对象是否需要回收需要一个严格合理的机制进行筛选,既不能过分影响程序执行性能,也要高效率的将“垃圾”进行辨识。

常用的2种判别方法:

引用计数法、可达性分析

引用计数法:

        这种方法为每个对象添加一个引用计数器,用来统计指向该对象的引用的个数,该对象每被引用一次,计数值+1,指向它的引用每次被指向其他对象值,计数-1,如果该对象引用计数=0,那么标识该对象已经死亡,无法再被使用,随即将会在下一轮垃圾收集过程中被回收释放内存。

例如:

String a = new String(); 
String b = a; 
String c = b;

此时 new String() 产生的这个对象已经被 a , b , c 所引用,该对象的引用计数值为3,如果此时将 a = null,则只有b , c对其有引用关系,计数值为2,当 b =null   c = null,此时该对象计数值为0,已经没有任何引用指向它,无法被使用了,于是可以被回收掉。

但是该方法存在弊端,

class TestA{
    TestB b = new TestB();
}

class TestB{
    TestA a = new TestA();
}

一旦2个对象TestA  TestB互相持有对方的引用,但是外部并没有任何引用指向该两个对象,实际上这两个对象无法使用,已经死亡,应该被回收,但是由于内部相互引用,该2个对象引用计数值仍然不为0,引用计数法无法判定。

可达性分析:

当前主流垃圾回收器采用的是可达性分析算法,该方法创建一个对象的合集起始合集为GC Root,从GC Root访问,能够被GC root引用到的对象为存活,不可到达的为死亡。

GC Root:可以理解成为堆外指向堆内对象的引用集合,上面引用计数法对象之间相互引用的弊端即可被解决,对象之间相互引用,但是没有被外部变量引用,该对象即被判定为从GC Root不可达,应该被回收。

GC Root中包括:

事物总是有缺陷的,修复旧Bug产生新Bug,可达性分析在多线程环境时坑产生2中坏的情况。

TestA a = new TestA();

​误​​​​​​:线程A刚访问过引用a指向的对象,此时GC线程进行扫描,从引用a出发能够到达TestA的实例对象,于是该对象标记为存活,但是同时线程B将a = null了,此时应该被回收,但是由于错过了时机,只能等待下一轮标记回收。此外无其他影响。

漏报:线程A将 a = null ,此时该对象已经不可达,该TestA实例对象被GC标记为可回收,然后线程B随即将a = new TestB(),由于在GC进行存活对象标记时,未将TestB的实例对象标记为存活,故在进行回收时会将它也回收掉,也就是分配给它的内存也会回收,回收后如果通过引用 a 访问了这块(不存在的)内存,将会导致虚拟机崩溃!

(标记和回收是2个过程,先将对象进行存活标记也就是加入GC Root,回收时将从 GC Root 中不可访问到的对象回收,所以会出现以上情况)

 

针对此弊端,进而又出现了一种解决方案:Stop-the-world

误报和漏报既然是由于多线程的时候发生的,那么就来个简单粗暴的方法,在进行标记和回收时,将所有工作线程挂起,直到该轮回收工作完成(GC pause)。

从逻辑上来分析,这个将所有线程突然性的暴力暂停明显是一种粗暴的做法,可能导致一些更坏的影响,实际上虚拟机工作时,这个stop-th-world是通过safe-point安全点机制来实现的,当需要开始垃圾回收时,虚拟机会收到stop-th-world请求,然后虚拟机会等待工作线程到达一个安全点时将该线程 “停掉”。当所有工作线程到达安全点后,才会允许垃圾回收相关线程独占工作。

到达安全点后也并非是将该线程完全挂起了,安全点可以是一段,只要执行的代码不影响虚拟机堆栈状态(不影响对象的引用关系),那么在垃圾回收的同时该线程可以继续运行,例如执行不访问java对象的native本地方法。

 

在标记完存活对象后,下一步就是回收未被标记的 “死亡” 对象。

三种回收方式:

空闲列表、压缩、区域复制

空闲列表:使用一张内存映射表来维护和记录内存使用情况,把死亡的对象所占用的内存空间标记为空闲,于是下一次的内存分        配便可以在此区域进行覆盖。但是由于这种方法灵活性较差,比如

如果对象B被回收了,那么在分配内存大于对象B的对象时就只能在对象C后面寻找空闲区域,在多次分配和回收之后,内存区域就会“千疮百孔”,出现很多小块空闲的内存碎片,无法被使用,内存利用率极低。

压缩:在对象B被回收后,为避免出现碎片化内存,将对象C往前移动,占据掉B之前的空间,这样就不存在碎片内存,利用率提升,但是由于频繁的进行内存数据迁移,容易造成性能下降。

区域复制:将内存划分为2块等区域,使用2个指针from  to 来维护,当进行内存分配时在from区域分配内存,当进行垃圾回收时,将标记为存活的对象放到 to 区域,那么此时from区域可以看成是空的,所有位置可写入。然后交换from和to指针,这样也可以解决碎片化内存,但是由于将内存划分2份,导致实际内存最多只能使用一半,极大浪费。

要实现一个优秀的垃圾处理机制必然是取百家之长,弃其糟粕。

分代收集:

就像现实世界存在的某种规律,大部分被小部分所掌握。java对象也是,大部分java对象只能存活一小段时间,而小部分对象将会存在很长一段时间,于是出现分代收集机制。

将堆空间划分2块区域:新生代和老年代。

新创建的对象放入新生代,在经历几轮回收仍然存活的对象放入老年代。这样就可以采用合适的算法快速进行回收,新生代由于存活时间短,回收频繁,于是可以采用耗时较短的回收方法,而老年代存活时间长,需要收集时表示内存可能不足了,将不计成本和效率触发全堆对象扫描。

例如,新new出来的对象将被放入Eden区,当Eden区空间不足时将触发一次Minor GC,将Eden区的死亡对象回收,存活下来的对象将进入Survivors的from区,这批对象将使用区域复制法进行回收,from 和 to区的对象交换超过一定次数时(默认15),存活下来的对象将进入老年代。如果单个EdenSurvivors区占用超过50%,复制次数较高的对象也会进入老年代。Eden和Survivors的比例默认由虚拟机根据对象生成速率动态调配,由参数 -XX:+UsePSAdaptiveSurvivorSizePolicy控制,固定比例的话由 参数 -XX:SurvivorRatio控制。

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值