java虚拟机和垃圾回收机制(二)

    距离上篇文章已经过了半个多月了,这期间自己也有不断的进一步研究jvm的特性,那么这篇文章将继续深入了解更多的jvm的内容

前言

    上一篇文章主要聊了关于,java的编译和加载的原理,例如java编译成class文件后运行时的加载方式,以及jvm的内存结构,最后聊了关于jvm堆和方法区中的分代机制。 java之所以叫做java正是因为其特有完美兼容一次编译遍地运行的特性,python和c#虽然也效仿java但是由于其平台的并没有java的兼容性好所以难以大面积推广,比如python的2.X版本和3.X版本是一个较大的跨域,但是并不完全兼容,而且其字节码转码效率并没有java高,而C#更加不用说了,几乎是windows独占,对linux并不是很合适,也间接注定这两门语言的能力发挥上收到了限制


为什么要分代?

   首先我说明一点,并不是因为分代java才能进行GC垃圾回收,没有分代机制只要能判断哪些对象需要回收,jvm一样可以回收,那么为什么要分代呢?凡是都是有原因的很明显,就是为了效率,分代能让GC更加高效

    大家可以试着想想,如果没有分代所有的对象都在内存里面,那么jvm每次回收对象都要从头扫描一遍内存,这样的效率会有多低,而且又加上很多对象其实存活时间并不长,执行完就死了,那么频繁的大规模扫描内存必然让jvm的效率低下,所以分代有利于高效的垃圾回收


怎么分代?

    jvm里面主要把对象分为了三代(可以阅读我上篇文章:java虚拟机和垃圾回收机制(一)):年轻代,老年代,持久代

年轻代:

    所有新生成的对象首先都是放在年轻代的。年轻代大部分都是生命周期比较短的对象。年轻代里面有三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时触发垃圾回收,还存活的对象将被复制到Survivor区,当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。


年老代:

    在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。


持久代(永久代):

    用于存放静态文件,常量池,如今Java类、方法等。基本上垃圾回收不会参与到,不影响垃圾回收(jdk7开始已经废除这个分类,其实可以说从jdk8开始没有永久代了,jdk8常量池放入到堆区中参与垃圾回收)


如何回收?

    垃圾回收原理上分为两步,第一步筛选出需要回收的对象,第二步回收这些对象。你要回收这个对象,首先必须要能确定这个对象是不在需要的在海量的对象面前,一个个遍历判断固然很低效,所以我们需要算法去解决这个问题

    判断垃圾对象:1、引用计数法 2、可达性分析

    传统垃圾回收算法:1、标记清除法 2、复制算法 3、标记-整理算法

    分代垃圾回收算法:1、分代收算法


引用计数法:

    在java里面最直接判断一个对象是否还需要的方法就是,判断这个对象有没有被引用。如果一个对象基本上没有被任何关联引用,证明这个对象不太可能再次被引用到,那么这个对象十有八九就是需要回收的对象,当对象被引用一次,那么对象的引用计数就加1,当对象的引用计数为0的时候这个对象在gc的时候就会被认定为垃圾对象,但是这个算法 有个缺陷,就是如果两个对象互相引用可能会导致这个两个对象永远不会被回收例如:

public static void main(String[] args){
    Object object1=new Object();
    Object object2=new Object();
    object1.object=object2;
    object2.object=object1;
    object1=null;
    object2=null;
}
上面的对象已经不可能再被使用到,但是他们的引用计数永远不为0,所以永远不会被回收


可达性分析:

    为了解决如上面出现的循环引用的情况,gc改良后加入了一种新的算法,可达性算法,java会构建一个逻辑上的对象根节点GC ROOT 通过判断对象当前是否能包含在根节点的引用树种,如果没有则证明当前对象也是垃圾对象

    

从图中可以看出,对象四和五都不包含在ROOT的引用树里面,所以判断这两个对象是需要被回收的


标记-清除算法:

    当对象被标记出来后,下一步就是执行了,当然执行如果直接遍历,效率也是很低下的,标记-清除算法是jvm里面最基础的垃圾回收算法,通过上面的方式,标记出来对象后,直接回收如图:


    从图中可以看出来,回收后的内存空间参差不齐,如果多回收几次后,可能内存碎片化过于严重导致大型对象的内存无法分配的情况


复制算法:

    为了解决上面出现碎片化的情况,最初的解决方法就是采用内存分块的方式,jvm将内存平均分为两片,每次对其中一片空间进行回收,存活的对象复制到另一片内存,这样交替回收,可以一定程度上的避免大面积的内存碎片的情况


但是这个算法有一个很严重的问题,就是他会导致内存空间直接减少一半,对于靠内存而活的程序员来说,这个是无法容忍的


标记整理算法:

    为了避免内存大量浪费的情况,并结合了上面的两种算法,而提出的新方式。与标记-清除算法不同的是,标记后不是清理垃圾对象,而是将认定为存活对象移向内存的一端。然后清除端边界外的对象。如图:



如果说以上都是传统的垃圾回收思路,那么下面这个就是jvm目前最流行的垃圾回收方式,也就是使用了分代机制后的gc模式


分代收集算法:

    分代收集是目前大部分jvm所采用的方法,其思想是根据对象的存活的不同生命周期将内存划分为不同的区域,一般情况下将GC堆划分为年轻代老年代。老年代的特点是,每次触发垃圾回收机制的时候,老年代中只有少量的对象需要被回收,因此可以根据不同不同的区域选择以上的不同算法进行垃圾回收。

    目前大部分jvm的GC对年轻代都采用复制算法,因为年轻代中每次GC都要回收大量的对象,按照上面的例子年轻代对象创建在Eden区经过多轮GC,仍然存活的对象将进入老年代,例:

    老年代因为需要回收的对象较少所以采用标记整理算法,一般来说对象创建基本上都是在年轻代,很少一部分是直接到老年代中,当新生代的Eden区内存不足的使用就会触发一次GC,存活的对象每存活一次年龄就会加一,并复制到存活的新区中,直到转入老年区


未完待续


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值