JVM原理及调优(4)–JVM垃圾回收机制

1.GC的概念

Garbage Collection 垃圾收集(联想下日常生活中垃圾的处理),这里所谓的垃圾指的是在系统运行过程中所产生的一些无用对象,而这些对象是需要占用一定的内存空间和系统资源的,如果这些无用的对象长期不被释放掉,那么会导致内存耗尽,即所谓的内存溢出。因此这些无用的对象必须要能够在一定的范围内及时被回收掉,以确保整个系统有足够的内存可用。
在C/C++中是由程序员自己来申请释放内存空间,因此并没有GC的概念。而Java为了解放程序员,使用了GC来管理内存的回收和垃圾的释放,用后台线程不停的监控和扫描,将一些无用的内存自动释放,防止由程序员引入的人为的内存泄漏。Java中,GC的对象是堆空间和永久区。

2.GC的算法

2.1 如何判断垃圾对象?

垃圾收集的第一步就是先需要算法来标记哪些是垃圾,然后再对垃圾进行处理。通常的编程语言都会用以下两种垃圾收集算法来进行判断:

  • 引用计数(ReferenceCounting)算法

  • 根搜索(GC Roots Tracing)算法

2.2 引用计数(ReferenceCounting)算法

这种方法比较简单直观,通过引用计算来收集垃圾。核心思路是,给每个对象添加一个被引用计数器,被引用时+1,引用失效-1,等于0时就表示该对象没有被引用,可以被回收。

使用者:

  • COM

  • ActionScript3

  • Python

JVM原理及调优(4)–JVM垃圾回收机制

JVM原理及调优(4)–JVM垃圾回收机制

缺点:
无法解决对象相互循环引用的问题。即当两个对象相互引用且不被其它对象引用时,各自的引用计数为1,虽不为0,但仍然是可被回收的垃圾对象。还有一个问题是如何解决精准计数。因此Java/C#并不采用该算法。

2.3 根搜索(GC Roots Tracing)算法

基本原理是GCRoot对象作为起始点(根)。如果从根到某个对象是可达的,则该对象称为可达对象(存活对象,不可回收对象)。否则就是不可达对象,可以被回收。

在Java语言中GC Roots包括:

  • 虚拟机栈中引用的对象

  • 方法区中类静态属性实体引用的对象

  • 方法区中常量引用的对象

  • 本地方法栈中JNI引用的对象。

JVM原理及调优(4)–JVM垃圾回收机制

3.JVM垃圾回收算法

3.1 标记清除算法(Mark-Sweep)

标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

JVM原理及调优(4)–JVM垃圾回收机制JVM原理及调优(4)–JVM垃圾回收机制JVM原理及调优(4)–JVM垃圾回收机制

标记清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记清除算法直接回收不存活的对象,因此会造成内存碎片!这显然是难以接受的。

3.2 标记整理压缩算法(Mark-Compac)

标记-压缩算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间

JVM原理及调优(4)–JVM垃圾回收机制JVM原理及调优(4)–JVM垃圾回收机制JVM原理及调优(4)–JVM垃圾回收机制

标记整理算法是在标记清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题,有利于内存的使用和分配。

3.3 复制算法(Copying)

将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收

JVM原理及调优(4)–JVM垃圾回收机制JVM原理及调优(4)–JVM垃圾回收机制JVM原理及调优(4)–JVM垃圾回收机制

复制算法简单高效,但是内存代价极高,有效内存只为总内存的一半,会浪费掉50%的空间。所以这种算法只是纸面算法,不适用于存活对象较多的场合(如老年代),不具备可用性,一般来说都会使用优化的复制算法。所谓优化的复制算法,即在复制算法的基础上,使用三个分区(Eden/S0/S1)进行处理(如新生代)

4.分代思想

分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代。根据不同代的特点,选取合适的收集算法,延缓对象衰老

  • 少量对象存活,适合复制算法

  • 大量对象存活,适合标记清理或者标记压缩JVM原理及调优(4)–JVM垃圾回收机制

虚拟机中的共划分为三个代:年轻代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

年轻代:

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

年老代:

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

持久代:

用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize进行设置。

4.1 新生代如何清理垃圾

新生代的垃圾收集器通常会假设大部分的对象的存活时间都非常短,只有少数对象的存活时间比较长。根据这个假设,新生代清理垃圾的算法主要是复制算法(Copying)。通过复制算法,可以将没有被引用的对象清理掉,并且可以将经过若干次(可配置)清理仍然存活的对象放入老生代。

新生代采用"空闲指针"的方式来控制GC触发,指针保持最后一个在新生代分配的对象位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。新生代的GC通常叫做young GC,有时候也叫minor GC

在连续分配对象过程中,对象会按照复制算法逐渐从Eden区Survivor区,最后到老生代

常用配置:

  • 新生代的大小:-Xmn

  • Eden区和Survivor区的比值:-XX:SurvivorRatio

4.2 老生代如何清理垃圾

老生代与新生代不同,对象存活的时间比较长,比较稳定,因此采用标记/整理(也叫标记-紧凑,Mark-Compact)算法

老生代的GC,通常叫做full GC,也叫major GC。老生代有多情况会触发GC,不过一般来说发生频率不高:

  1. 旧生代空间不足
    调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在老生代创建对象。

  2. Pemanet Generation空间不足
    增大Perm Gen空间,避免太多静态对象。

  3. GC后晋升到老生代的平均大小大于老生代剩余空间
    控制好新生代和旧生代的比例。

  4. 手动调用System.gc()
    垃圾回收不要手动触发,尽量依靠JVM自身的机制。

常用配置:

  • 堆的初始空间:-Xms,可以推算出老生代的大小为-Xms减去-Xmn

  • 堆的最大空间:-Xmx

  • 最大年龄阈值:-XX:MaxTenuringThreshold,即新生代转入老生代的存活次数

  • 老生代和新生代的比值:-XX:NewRatio,例如该值为3,则表示新生代与老生代比值为1:3

参考:

anzhihe 安志合个人博客,版权所有 丨 如未注明,均为原创 丨 转载请注明转自:https://chegva.com/2960.html | ☆★★每天进步一点点,加油!★★☆ | 

您可能还感兴趣的文章!

2 评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注