zoukankan      html  css  js  c++  java
  • Java虚拟机(三)垃圾标记算法与Java对象的生命周期

    前言

    这一节我们来简单的介绍垃圾收集器,并学习垃圾标记的算法:引用计数算法和根搜索算法,为了更好的理解根搜索算法,会在文章的最后介绍Java对象在虚拟机中的生命周期。

    1.垃圾收集器概述

      垃圾收集器(Garbage Collection),通常被称作GC。提到GC,很多人认为它是伴随Java而出现的,其实GC出现的时间要比Java早太多了,它是1960诞生于MIT的Lisp。 
    GC主要做了两个工作,一个是内存的划分和分配,一个是对垃圾进行回收。关于内存的划分和分配,目前Java虚拟机内存的划分是依赖于GC的的设计的,比如现在GC都是采用了分代收集算法来回收垃圾,Java堆作为GC主要管理的区域,被细分为新生代和老年代,再细致一点新生代又可以划分为Eden空间、From Survivor空间、To Survivor空间等,这样进行划分是为了更快的进行内存分配和回收。空间划分后,GC就可以为新对象分配内存空间。 
    关于对垃圾进行回收,被引用的对象是存活的对象,而不被引用的对象是死亡的对象也就是垃圾,GC要区分出存活的对象和死亡的对象,也就是垃圾标记,并对垃圾进行回收。接下来我们先来介绍垃圾标记算法。

    2.垃圾标记算法

    在对垃圾进行回收前,GC要先标记出垃圾,那么如何标记呢,目前有两种垃圾标记算法,分别是引用计数算法和根搜索算法,这两个算法都和引用有些关联,因此讲垃圾标记算法前,我们先回顾下引用的知识。

    引用

    在JDK1.2之后,Java将引用分为强引用、软引用、弱引用和虚引用。

    • 强引用:当我们new一个对象时就是创建了一个具有强引用的对象,如果一个对象具有强引用,垃圾收集器就绝不会回收它。Java虚拟机宁愿抛出OutOfMemoryError异常,使程序异常终止,也不会回收具有强引用的对象来解决内存不足的问题。
    • 软引用:如果一个对象只具有软引用,当内存不够时,会回收这些对象的内存,回收后如果还是没有足够的内存,就会抛出OutOfMemoryError异常。Java提供了SoftReference类来实现软引用。
    • 弱引用:弱引用比起软引用具有更短的生命周期,垃圾收集器一旦发现了只具有弱引用的对象,不管当前内存是否足够,都会回收它的内存。Java提供了WeakReference类来实现弱引用。
    • 虚引用:虚引用并不会决定对象的生命周期,如果一个对象仅持有虚引用,这就和没有任何引用一样,在任何时候都可能被垃圾收集器回收。一个只具有虚引用的对象,被垃圾收集器回收时会收到一个系统通知,这也是虚引用的主要作用。Java提供了PhantomReference类来实现虚引用。

    引用计数算法

    引用计数算法的基本思想就是每个对象都有一个引用计数器,当对象在某处被引用的时候,它的引用计数器就加1,引用失效时就减1。当引用计数器中的值变为0,则该对象就不能被使用成了垃圾。 
    目前主流的Java虚拟机没有选择引用计数算法来为垃圾标记,主要原因是引用计数算法没有解决对象之间相互循环引用的问题。 
    举个例子,下面代码的注释1和注释2处,d1和d2相互引用,除此之外这两个对象无任何其他引用,实际上这两个对象已经死亡,应该作为垃圾被回收,但是由于这两个对象互相引用,引用计数就不会为0,垃圾收集器就无法回收它们。

    class _2MB_Data {
        public Object instance = null;
        private byte[] data = new byte[2 * 1024 * 1024];//用来占内存,测试垃圾回收
    }
    
    public class ReferenceGC {
        public static void main(String[] args) {
            _2MB_Data d1 = new _2MB_Data();
            _2MB_Data d2 = new _2MB_Data();
            d1.instance = d2;//1
            d2.instance = d1;//2
            d1 = null;
            d2 = null;
            System.gc();
        }
    }
    View Code

    如果你使用Android Studio,就在Edit Configurations中的VM options加入如下语句来输出详细的GC日志:

    -XX:+PrintGCDetails
     1 运行程序,GC日志为: 
     2 [GC (System.gc()) [PSYoungGen: 8028K->832K(76288K)] 8028K->840K(251392K), 0.0078334 secs] [Times: user=0.06 sys=0.00, real=0.01 secs] 
     3 [Full GC (System.gc()) [PSYoungGen: 832K->0K(76288K)] [ParOldGen: 8K->603K(175104K)] 840K->603K(251392K), [Metaspace: 3015K->3015K(1056768K)], 0.0045844 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
     4 Heap 
     5 PSYoungGen total 76288K, used 1966K [0x000000076af80000, 0x0000000770480000, 0x00000007c0000000) 
     6 eden space 65536K, 3% used [0x000000076af80000,0x000000076b16bac0,0x000000076ef80000) 
     7 from space 10752K, 0% used [0x000000076ef80000,0x000000076ef80000,0x000000076fa00000) 
     8 to space 10752K, 0% used [0x000000076fa00000,0x000000076fa00000,0x0000000770480000) 
     9 ParOldGen total 175104K, used 603K [0x00000006c0e00000, 0x00000006cb900000, 0x000000076af80000) 
    10 object space 175104K, 0% used [0x00000006c0e00000,0x00000006c0e96d10,0x00000006cb900000) 
    11 Metaspace used 3046K, capacity 4496K, committed 4864K, reserved 1056768K 
    12 class space used 334K, capacity 388K, committed 512K, reserved 1048576K
    13 
    14 查看此GC日志前我们先来简单了解下各参数的含义,[GC (System.gc()和[Full GC (System.gc()说明了这次垃圾收集的停顿类型,而不是来区分新生代GC和老年代GC的。 [Full GC (System.gc() 说明这次GC发生了STW,STW也就是Stop the World机制,意思是说在执行垃圾收集算法时,只有GC线程在运行,其他的线程则会全部暂停,等待GC线程执行完毕后才能再次运行。 
    15 PSYoungGen代表新生代,ParOldGen代表老年代,Metaspace代表元空间(JDK 8中用来替代永久代PermGen)。 
    16 我们来看日志的[GC (System.gc()),内存变化为:8028K->840K(251392K),8028K代表回收前的内存大小,840K代表回收后的内存大小,251392K代表内存总大小。因此可以得知内存回收大小为(8028-840)K。这就说明JDK8的HotSpot虚拟机并没有采用引用计数算法来标记内存,它对上述代码中的两个死亡对象的引用进行了回收。
    View Code

    根搜索算法

    这个算法的基本思想就是选定一些对象作为GC Roots,并组成根对象集合,然后从这些作为GC Roots的对象作为起始点,向下进行搜索,如果目标对象到GC Roots是连接着的,我们则称该目标对象是可达的,如果目标对象不可达则说明目标对象是可以被回收的对象,如下图所示。 
    未命名文件.png

    从上图看以看出,Obj5、Obj6和Obj7都是不可达的对象,其中Obj5和Obj6虽然互相引用,但是因为他们到GC Roots是不可达的所以它们仍旧会判定为可回收的对象,这样根搜索算法就解决了引用计数算法无法解决的问题:已经死亡的对象因为相互引用而不能被回收。 
    在Java中,可以作为GC Roots的对象主要有以下几种:

    • Java栈中的引用的对象。
    • 本地方法栈中JNI引用的对象。
    • 方法区中运行时常量池引用的对象。
    • 方法区中静态属性引用的对象。
    • 运行中的线程
    • 由引导类加载器加载的对象
    • GC控制的对象

    还有一个问题是被标记为不可达的对象会立即被垃圾收集器回收吗?要回答这个问题我们首先要了解Java对象在虚拟机中的生命周期。

    3.Java对象在虚拟机中的生命周期

    当Java对象被类加载器加载到虚拟机中后,Java对象在Java虚拟机中有7个阶段。 
    1.创建阶段(Created) 
    创建阶段的具体步骤为:

    • 为对象分配存储空间。
    • 构造对象。
    • 从超类到子类对static成员进行初始化。
    • 递归调用超类的构造方法。
    • 调用子类的构造方法。

    2.应用阶段(In Use) 
    当对象被创建,并分配给变量赋值,状态就切换到了应用阶段。 
    这一阶段的对象至少要具有一个强引用,或者显式的使用软引用、弱引用或者虚引用。

    3.不可见阶段(Invisible) 
    程序中找不到对象的任何强引用,比如程序的执行已经超出了该对象的作用域。在不可见阶段,对象仍可能被特殊的强引用GC Roots持有着,比如对象被本地方法栈中JNI引用或是被运行中的线程引用等。

    4.不可达阶段(Unreachable) 
    程序中找不到对象的任何强引用,并且垃圾收集器发现对象不可达。

    5.收集阶段(Collected) 
    垃圾收集器已经发现对象不可达,并且垃圾收集器已经准备好要对该对象的内存空间重新进行分配时。这个时候如果该对象重写了finalize方法,则会调用该方法。

    6.终结阶段(Finalized) 
    当对象执行完finalize法后仍然处于不可达状态时,或者对象没有重写finalize方法,则该对象进入终结阶段,并等待垃圾收集器回收该对象空间。

    7.对象空间重新分配阶段(Deallocated) 
    当垃圾收集器对对象的内存空间进行回收或者再分配时,这个对象就会彻底消失。

    好了,我们已经了解了Java对象在虚拟机中的生命周期,再来回想我方才说的问题:被标记为不可达的对象会立即被垃圾收集器回收吗?很显然是不会的,被标记为不可达的对象会进入收集阶段,这时会执行该对象重写的finalize方法,如果没有重写finalize方法或者finalize方法中没有重新与一个可达的对象进行关联才会进入终结阶段,并最终被回收。

    JVM 深入笔记(3)垃圾标记算法 
    GC roots 
    Java GC - 监控回收行为与日志分析 
    Java:对象的强、软、弱和虚引用 
    JVM GC中Stop the world案例实战 
    Java对象的生命周期

  • 相关阅读:
    每天一道LeetCode--141.Linked List Cycle(链表环问题)
    每天一道LeetCode--119.Pascal's Triangle II(杨辉三角)
    每天一道LeetCode--118. Pascal's Triangle(杨辉三角)
    CF1277D Let's Play the Words?
    CF1281B Azamon Web Services
    CF1197D Yet Another Subarray Problem
    CF1237D Balanced Playlist
    CF1239A Ivan the Fool and the Probability Theory
    CF1223D Sequence Sorting
    CF1228D Complete Tripartite
  • 原文地址:https://www.cnblogs.com/ganchuanpu/p/6217414.html
Copyright © 2011-2022 走看看