您的位置:首页 > 编程语言 > Java开发

jvm之java垃圾回收机制详解

2018-08-13 18:58 459 查看
版权声明:版权所有 © 侵权必究 https://blog.csdn.net/m0_38075425/article/details/81633799

      传统的C/C++等编程语言,需要程序员负责回收已经分配出去的内存。显示进行垃圾回收是一件令人头疼的事情,因为程序员并不总是知道内存应该何时进行释放。如果一些分配出去的内存不能及时的回收就会引起系统运行速度下降,甚至导致程序瘫痪,这种现象称为内存泄露。

      与C/C++语言不同,Java语言不需要程序员自己去控制内存回收,Java程序的内存分配和回收都是由JVM在后台自动进行的。JRE会负责回收那些不再使用的内存,这种机制被称为垃圾回收机制(Garbage Collection,也被称为GC)。通常JVM会提供一个后台线程来进行检测和控制,一般都是在CPU空闲或者内存不足时自动进行垃圾回收。

     Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。

     1)如何确定一个对象是否可以被回收

     引用计数算法:

     引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。在这种方法中,堆中的每个对象实例都有一个引用计数器。当一个对象被创建时,且将该对象实例分配给一个引用变量,该对象实例的引用计数设置为 1。当任何其它变量被赋值为这个对象的引用时,对象实例的引用计数器加 1(a = b,则b引用的对象实例的计数器加 1,因为b被引用),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减 1。特别地,当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器均减 1。任何引用计数器为0的对象实例可以被当作垃圾收集。其优点是执行速度快,缺点是很难解决对象之间相互循环引用的问题。

     可达性分析算法

     可达性分析算法是通过判断对象的引用链是否可达来决定对象是否可以被回收。JVM把所有的引用关系看作一张图,通过一系列的名为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。如下图所示:

在上图中可以看出,红色的对象可以被回收。结合上图,此算法不难理解,GC ROOT实际上为“有用的对象”如果某一个对象没有被有用的对象间接或直接的引用,则其是无用的,可以被回收掉。

    2)如何回收垃圾

    标记清除算法

    标记-清除算法分为标记和清除两个阶段。该算法首先从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间,未被标记的对象将被回收。标记-清除算法的劣势是,标记和清除两个过程的效率都不高;标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,因此标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。如下图:

           

                

    上图为回收前,下图为回收后。在扫描一遍为,红色的为不存活对象,白色的为无数据,绿色的为存活对象,由图可见,此算法产生了大量的内存碎片。

    复制算法

    复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,比如新生代。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。且劣势为需要50%的内存空间进行垃圾回收。现在商用的虚拟机都采用这种算法来回收新生代。如下图:

           

   由上图可见,此算法将存活的对象复制到另一块内存,再将此内存全部清除(此内存便为空了),下次垃圾回收,再将另一块内存的存活对象复制到此内存,以此循环进行垃圾回收。

   标记整理算法

   复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。标记整理算法的标记过程类似标记清除算法,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,类似于磁盘整理的过程,该垃圾回收算法适用于对象存活率高的场景(老年代)。其不会产生内存碎片。

 同样是第一例的垃圾图,回收后如下:

             

可见此算法内存碎片减少。

   分代收集算法

   对于一个大型的系统,当创建的对象和方法变量比较多时,堆内存中的对象也会比较多,如果逐一分析对象是否该回收,那么势必造成效率低下。分代收集算法是:不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率。当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就用标记清除算法或者标记整理算法。Java堆内存一般可以分为新生代、老年代和永久代三个模块。

   下面做进一步说明:

   1)新生代

   新生代的目标就是尽可能快速的收集掉那些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的。新生代内存按照 8:1:1 的比例分为一个eden区和两个survivor(survivor0,survivor1)区,大部分对象在Eden区中生成。在进行垃圾回收时,先将eden区存活对象复制到survivor0区,然后清空eden区,当这个survivor0区也满了时,则将eden区和survivor0区存活对象复制到survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后交换survivor0区和survivor1区的角色(即下次垃圾回收时会扫描Eden区和survivor1区),即保持survivor1区为空,如此往复。特别地,当survivor1区也不足以存放eden区和survivor0区的存活对象时,就将存活对象直接存放到老年代。如果老年代也满了,就会触发一次FullGC,也就是新生代、老年代都进行回收。注意,新生代发生的GC也叫做MinorGC,MinorGC发生频率比较高,不一定等 Eden区满了才触发。如下图所示:

  2)老年代

  老年代存放的都是一些生命周期较长的对象,就像上面所叙述的那样,在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中。此外,老年代的内存也比新生代大很多(大概是两倍),当老年代满时会触发Full GC,老年代对象存活时间比较长,因此FullGC发生的频率比较低。

 3)永久代

  永久代主要用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如使用反射、动态代理、CGLib等bytecode框架时,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。

其中:

       Minor GC为对新生代进行回收,不会影响到年老代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。Full GC为对整个堆进行回收,包括新生代、老年代和永久代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代被写满、永久代(Perm)被写满和System.gc()被显式调用等。

关于新生代和老年代晋升的说明:

      大对象直接进入年老代

      大对象即需要大量连续内存空间的Java对象,如长字符串及数组。经常出现大对象导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来安置他们。  

      长期存活的对象将进入年老代

      JVM给每个对象定义了一个对象年龄计数器,对象在Eden创建并经过第一次Minor GC后仍然存活,并能被Suivivor容纳的话,对象年龄加1。每经历过一次Minor GC,年龄就增加1岁,当到一定年龄(默认15岁,可以通过参数-XXMaxTenuringThreshold设置),新生代就将会晋升年老代。

      动态对象年龄判定

      为了更好地适应不同程序内存状况,JVM并不硬性要求对象年龄达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入年老代。

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: