java 垃圾回收机制的理解

1.垃圾回收的意义

在java体系中内存的分和回收是自动化管理的,从而程序员不需要操心内存的分配和是否浪费。javaGC机制能帮我们把不需要的对象占用的内存回收,保证程序高效的运行。由于我们创建的对象都是放在JVM堆中,所以主要是对堆中的内存进行回收。

2.垃圾回收使用是算法

JVM在发生GC时会使用一定算法判断哪些对象是需要回收,即JVM垃圾的检测方式。那么一般采用的算法是引用计数法和根搜索法。

引用计数法:这个算法的实现是,给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1;当引用失效时,计数器值-1。任何时刻计数值为0的对象就是不可能再被使用的。这种算法使用场景很多,但是,Java中却没有使用这种算法,因为这种算法很难解决对象之间相互引用的情况。看一段代码:

/**
 * 虚拟机参数:-verbose:gc
 */
public class ReferenceCountingGC
{
    private Object instance = null;
    private static final int _1MB = 1024 * 1024;
    
    /** 这个成员属性唯一的作用就是占用一点内存 */
    private byte[] bigSize = new byte[2 * _1MB];
    
    public static void main(String[] args)
    {
        ReferenceCountingGC objectA = new ReferenceCountingGC();
        ReferenceCountingGC objectB = new ReferenceCountingGC();
        objectA.instance = objectB;
        objectB.instance = objectA;
        objectA = null;
        objectB = null;
        
        System.gc();
    }
}

根搜索算法:

根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

java中可作为GC Root的对象有

 1.虚拟机栈中引用的对象(本地变量表)

 2.方法区中静态属性引用的对象

 3. 方法区中常量引用的对象

 4.本地方法栈中引用的对象(Native对象)

对于根搜索算法而言,未到达的对象并非是“非死不可”的,若要宣判一个对象死亡,至少需要经历两次标记阶段。如果对象在进行可达性分析后发现没有与GCRoots相连的引用链,则该对象被第一次标记并进行一次筛选,筛选条件为是否有必要的执行该对象的finalize方法,若对象没有覆盖finalize方法或者该finalize方法是否已经被虚拟机执行过了,则均视为不必要执行该对象的finalize方法,即该对象将会被回收。反之,若对象覆盖了finalize方法,并且该finalize方法并没有被执行过,那么,这个对象会被放置在一个叫做F-Queue的队列中,之后会由虚拟机自动建立的、优先级低的Finalizer线程去执行,而虚拟机不必要等待该线程执行结束,即虚拟机只负责建立线程,其他的事情交给线程去处理。

对F-Queue中对象进行第二次标记,如果对象在finalize方法中拯救了自己,即关联上了GCRoots引用链,,如把this关键字赋值给其他变量,那么在第二次标记的时候该对象将从“即将回收”的集合中移除,如果对象还是没有拯救自己,那就会被回收。
如下代码演示了一个对象如何在finalize方法中拯救了自己,然而,它只能拯救自己一次,第二次就被回收了。具体代码如下:

/**
* 此代码演示了两点:
* 1.此对象可以再被GC时自我拯救
* 2.这种自救的机会只有一次,因为一个对象的finalize方法最多只会被系统自动调用一次
*/
public class FinalizeEscapeGC {
   public String name;
   public static FinalizeEscapeGC SAVE_HOOK = null;

   public FinalizeEscapeGC(String name){
       this.name = name;
   }

   public void isAlive(){
       System.out.println("yes,i am still alive :)");
   }

   @Override
   protected void finalize() throws Throwable {
       super.finalize();
       System.out.println("finalize method executed!");
       System.out.println(this);
       FinalizeEscapeGC.SAVE_HOOK = this;
   }

   @Override
   public String toString() {
       return name;
   }

   public static void main(String[] args) throws InterruptedException {
       SAVE_HOOK = new FinalizeEscapeGC("leesf");
       System.out.println(SAVE_HOOK);
       //对象第一次拯救自己(第一次标记)
       SAVE_HOOK = null;
       System.out.println(SAVE_HOOK);
       System.gc();//第一次回收
       //因为finalize方法优先级很低,所以暂停0.5秒等待
       Thread.sleep(500);//此处调用了finalize方法,对象拯救了自己,this引用赋值
       if (SAVE_HOOK != null) {//此处判断非空,对象还处于存活状态
           SAVE_HOOK.isAlive();
       } else {
           System.out.println("no, i am dead :(");
       }

       //下面这段代码与上面的完全相同,这是这一次却自救失败
       //一个对象的finalize方法只会被调用一次
       SAVE_HOOK = null;
       System.gc();
       //因为finalize方法优先级很低,所以暂停0.5秒等待
       Thread.sleep(500);//此处并没有调用finalize方法,因为finalize方法只会执行一次
       if (SAVE_HOOK != null) {//此处判断为null
           SAVE_HOOK.isAlive();
       } else {
           System.out.println("no, i am dead :(");
       }
   }
}

运行结果如下:

在这里插入图片描述

 由结果可知,该对象拯救了自己一次,第二次没有拯救成功,因为对象的finalize方法最多被虚拟机调用一次。

除了使用这两种算法判断哪些对象需要回收外,还可以通过对象的引用状态来识别是否需要做回收。有如下四种引用:

  1. 强引用
    代码中普遍存在的类似“Object obj = new Object()”这类的引用, 只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
  2. 软引用
    描述有些还有用但并非必须的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。Java中的类SoftReference表示软引用。
  3. 弱引用
    描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java中的类WeakReference表示弱引用。
  4. 虚引用
    这个引用存在的唯一目的就是在这个对象被收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间完全没关系。Java中的类PhantomReference表示虚引用

在这里插入图片描述

3、垃圾收集算法(方式)

1. 标记-清除(Mark-Sweep)算法(方式)
这是最基本的算法,标记-清除算法就如同它的名字一样,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。这种算法的不足主要体现在效率和空间,从效率的角度讲,标记和清除两个过程的的效率都不高;从空间的角度讲,标记清楚后会产生大量不连续的内存碎片,内存碎片太多可能会导致以后程序运行过程中在需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾回收动作。标记清除算法执行过程如图:
在这里插入图片描述

2. 复制(coping)算法(方式)
复制算法是为了解决效率问题而出现的,它将可用的内存分为两块,每次只用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存一次性清理掉。这样每次只需要对整个半区进行内存回收,内存分配的执行过程如图:
在这里插入图片描述
不过这种算法有个缺点,内存缩小为原来的一半,这样代价太高了。现在的商用模拟机都采用这种算法来回收新生代,不过研究表明1:1的比例非常不科学,因此新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。每次回收时,将Eden和Survivor。每次回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才和刚才使用过的Survivor空间。HotSpot虚拟机默认Eden区和Survivor区的比例为8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。当然,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖老年代进行分配担保(Handle Promotion)。

3. 标记-整理(Mark-Compact)算法(方式)
复制算法在对象存活率较高的场景下要进行大量的复制操作,效率很低。万一对象100%存活,那么需要额外的空间进行分配担保。老年代都是不易被回收的对象,对象存活率高,因此一般不能直接选用复制算法。根据老年代的特点,有人提出了另外一种标记-整理算法,过程v与标记-清除算法一样,不过不是直接对可回收对象进行整理,而是让所有存活对象都向一端移动,然后清理掉边界以外的内存。标记-整理算法的工作过程如图:
在这里插入图片描述
4. 分代收集算法(方式)
根据上面的内容,用一张纸概括一下堆内存的布局。
在这里插入图片描述
现代商用虚拟机基本都采用分代收集算法来进行垃圾回收。这种算法结合了以上的内容,根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担当的(老年代),采用标记-清理算法或者标记-整理算法。

4、垃圾收集器

垃圾收集器就是上面讲的理论知识的具体体现了。不同虚拟机所提供的垃圾收集器可能会有很大差别,我们使用的是HotSpot,HotSpot这个虚拟机所包含的所有收集器如图:

在这里插入图片描述上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,那说明他们可以搭配使用。虚拟机所处的区域说明它是属于新生代收集器还是老年代收集器,只能选择对具体应用最合适的收集器。这也是HotSpot为什么要实现这么多收集器的原因。

1. Serial收集器
最基本、发展历史最久的收集器,这个收集器采用复制算法的单线程的收集器,单线程一方面意味着他只会使用一个CPU或者一条线程去完成垃圾收集工作,另一方面也意味着他进行垃圾收集时必须暂停其他线程的所有工作,直到它收集结束为止。不过实际上到目前为止,Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器,因为它简单而高效。
在这里插入图片描述
2. Parnew收集器
Parnew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为和Serial收集器完全一样,但是他却是Server模式下的虚拟机首选的新生代收集器。除了Serial收集器外,目前只有它能与CMS收集器配合工作。CMS收集器第一次实现了让垃圾收集器与用户线程基本上同时工作。Parnew收集器默认开启的收集线程数与CPU数量相同,在CPU数量非常多的情况下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。Parnew收集器运行过程如下图所示:
在这里插入图片描述
3. Parallel Scavenge收集器
Parallel Scavenge收集器也是一个新生代收集器,也采用了复制算法,也是并行的多线程收集器。CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。Parallel Scavenge收集器是虚拟机运行在Server模式下的默认垃圾收集器。

虚拟机提供了-XX:MaxGCPauseMills和-XX:G从TimeRatio两个参数来精确控制最大垃圾收集停顿时间和吞吐量大小。不过不要以为前者越小越好,GC停顿时间的缩短是以牺牲吞吐量和新生代空间换取的。由于与吞吐量关系密切,Parallel Scavenge收集器也被称为“吞吐量优先收集器”。

Parallel Scavenge收集器有一个参数-XX:UseAdaptiveSizePolicy参数,这是一个开关参数,这个参数打开之后,就不需要手动指定新生代大小、Eden区和Survivor参数等细节参数了,虚拟机会根据当前系统的运行情况以及性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。如果对于垃圾收集器运作原理不太了解,以至于在优化比较困难的时候,可以使用 Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成。

4. Serial Old收集器
Serial收集器的老年代版本,同样是一个单线程收集器,使用== “标记-整理算法”,这个收集器的主要意义也是在于给Client模式==下的虚拟机使用。

5. Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用多线程和== “标记-整理算法”==。这个收集器在JDK 1.6之后的出现,“吞吐量优先收集器”终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge收集器+Parallel Old收集器的组合。运行过程如下图所示:
在这里插入图片描述
6. CMS收集器

CMS(Conrrurent Mark Sweep,连续标记扫描)收集器是以获取最短回收停顿时间为目标的收集器。使用标记-清除算法,收集过程分为如下四步:
(1)初始标记,标记GCRoots能直接关联到的对象,时间很短。
(2)并发标记,进行GCRoots Tracing(可达性分析)过程,时间很长。
(3)重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长。
(4)并发清除,回收内存空间,时间很长。

其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。运行过程如下图所示:
在这里插入图片描述
7. G1收集器
G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1收集器具有以下特点:
(1)并发和并行。使用多个CPU来缩短Stop The World停顿时间,与用户线程并发执行。
(2)分代收集。独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次GC的旧对象,以获取更好的收集效果。
(3)空间整合。基于标记-整理算法,无内存碎片产生。
(4)可预测的停顿。能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间不得超过N毫秒。

在G1之前的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分(可以不连续)Region的集合。

5、Java有了GC同样会出现内存泄露问题

1.静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。

Static Vector v = new Vector(); 
for (int i = 1; i<100; i++) 
{ 
    Object o = new Object(); 
    v.add(o); 
    o = null; 
}

在这个例子中,代码栈中存在Vector 对象的引用 v 和 Object 对象的引用 o 。在 For 循环中,我们不断的生成新的对象,然后将其添加到 Vector 对象中,之后将 o 引用置空。问题是当 o 引用被置空后,如果发生 GC,我们创建的 Object 对象是否能够被 GC 回收呢?答案是否定的。因为, GC 在跟踪代码栈中的引用时,会发现 v 引用,而继续往下跟踪,就会发现 v 引用指向的内存空间中又存在指向 Object 对象的引用。也就是说尽管o 引用已经被置空,但是 Object 对象仍然存在其他的引用,是可以被访问到的,所以 GC 无法将其释放掉。如果在此循环之后, Object 对象对程序已经没有任何作用,那么我们就认为此 Java 程序发生了内存泄漏。

2.各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。

3.监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

不同垃圾收集器设置方式

XX:+UseSerialGC
启⽤串⾏收集器
-XX:+UseParallelGC
启⽤并⾏垃圾收集器,配置了该选项,那么-XX:+UseParallelOldGC默认 启⽤
-XX:+UseParNewGC
年轻代采⽤并⾏收集器,如果设置了-XX:+UseConcMarkSweepGC选 项,⾃动启⽤
-XX:ParallelGCThreads
年轻代及⽼年代垃圾回收使⽤的线程数。默认值依赖于 JVM 使⽤的 CPU 个

-XX:+UseConcMarkSweepGC ( CMS )
对于⽼年代,启⽤ CMS 垃圾收集器。 当并⾏收集器⽆法满⾜应⽤的延迟需 求是,推荐使⽤CMS 或 G1 收集器。启⽤该选项后,
-XX:+UseParNewGC ⾃动启⽤
-XX:+UseG1GC
启⽤ G1 收集器。 G1 是服务器类型的收集器, ⽤于多核、⼤内存的机器。 它在保持⾼吞吐量的情况下,⾼概率满⾜GC 暂停时间的⽬标

CMS,G1 垃圾回收器中的三色标记
三色标记法是一种垃圾回收法,它可以让 JVM 不发生或仅短时间发生 STW(Stop The World),从而达到清除 JVM 内存垃圾的目的。三色标记法将对象的颜色分为了黑、灰、白,三种颜色。

黑色:该对象已经被标记过了,且该对象下的属性也全部都被标记过了。(程序所需要的对 象);

灰色:对象已经被垃圾收集器扫描过了,但是对象中还存在没有扫描的引用(GC 需要从此对象中去寻找垃圾); 
白色:表示对象没有被垃圾收集器访问过,即表示不可达。 
CMS 解决办法:增量更新在应对漏标问题时,CMS 使用了增量更新(Increment Update)方法来做,在一个未被标记的对象(白色对象)被重新引用后,引用它的对象若为黑色则要变成灰色,在下次二次标记时让 GC 线程继续标记它的属性对象(但还是存在漏标的问题)。

CMS 另两个致命缺陷 CMS 采用了 Mark-Sweep 算法,最后会产生许多内存碎片,当到一定数量时,CMS 无法 清理这些碎片了,CMS 会让 Serial Old 垃圾处理器来清理这些垃圾碎片,而 Serial Old 垃圾处理器是单线程操作进行清理垃圾的,效率很低。 所以使用 CMS 就会出现一种情况,硬件升级了,却越来越卡顿,其原因就是因为进行Serial Old GC 时,效率过低。 


解决方案:使用 Mark-Sweep-Compact 算法,减少垃圾碎片 调优参数(配套使用):-XX:+UseCMSCompactAtFullCollection 开启 CMS 的压缩 -XX:CMSFullGCsBeforeCompaction 默认为 0,指经过多少次 CMS FullGC 才进行压缩当 JVM 认为内存不够,再使用 CMS 进行并发清理内存可能会发生 OOM 的问题,而不得不进行 Serial Old GC,Serial Old 是单线程垃圾回收,效率低解决方案:降低触发 CMS GC 的阈值,让浮动垃圾不那么容易占满老年代调优参数: -XX:CMSInitiatingOccupancyFraction 92% 可以降低这个值,让老年代占用率达到该值就进行 CMS GC 
G1 解决办法:SATB SATB(Snapshot At The Beginning), 在应对漏标问题时,G1 使用了 SATB 方法来做,具体流程:在开始标记的时候生成一个快照图标记存活对象在一个引用断开后,要将此引用推到 GC 的堆栈里,保证白色对象(垃圾)还能被 GC线程扫描到(在**write barrier(写屏障)**里把所有旧的引用所指向的对象都变成非白的)配合 Rset,去扫描哪些 Region 引用到当前的白色对象,若没有引用到当前对象,则回收G1 会不会进行 Full GC?会,当内存满了的时候就会进行 Full GC;且 JDK10 之前的 Full GC,为单线程的,所以 使用 G1 需要避免 Full GC 的产生。解决方案:加大内存;提高 CPU 性能,加快 GC 回收速度,而对象增加速度赶不上回收速度,则 Full GC 可 以避免;降低进行 Mixed GC 触发的阈值,让 Mixed GC 提早发生(默认 45%) 

开启GC日志

一般来说,JDK8及以下版本通过以下参数来开启GC日志:

1 ‐XX:+PrintGCDetails

‐XX:+PrintGCDateStamps

‐Xloggc:gc.log

如果是在JDK9及以上的版本,则格式略有不同:

‐Xlog:gc*=info:file=

gc.log:time:filecount=0

Java8默认使用的垃圾收集器是什么?

Java8版本的Hotspot JVM,默认情况下使用的是并行垃圾收集器(Parallel GC)。其

他厂商提供的JDK8基本上也默认使用并行垃圾收集器。

Java9之后,官方JDK默认使用的垃圾收集器是G1。 。

总结:java垃圾回收机制首选是通过根搜索算法或者引用计数算法以及引用状态方式筛选出哪些对象是需要回收的。然后通过不同的回收方式或者几种不同方式组合进行会垃圾回收(分代收集算法是目前大部分JVM的垃圾收集器采用的收集方式(算法))。而垃圾收集器就是这些底层算法和方式的上层实现。其中G1垃圾收集器是当前最好的垃圾收集器。


 

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值