zoukankan      html  css  js  c++  java
  • JVM 基础:回收哪些内存/对象 引用计数算法 可达性分析算法 finalize()方法 HotSpot实现分析

    转自:https://blog.csdn.net/tjiyu/article/details/53982412

    1-1、为什么需要了解垃圾回收

    目前内存的动态分配与内存回收技术已经相当成熟,但为什么还需要去了解内存分配与GC呢?

           1、当需要排查各种内存溢出、内存泄漏问题时;

           2、当垃圾收集成为系统达到更高并发量的瓶颈时;

           我们就需要对这些"自动化"技术实话必要的监控和调节;

    1-2、垃圾回收需要了解什么

    思考GC完成的3件事:

           1、哪些内存需要回收?即如何判断对象已经死亡;

           2、什么时候回收?即GC发生在什么时候?需要了解GC策略,与垃圾回收器实现有关;

           3、如何回收?即需要了解垃圾回收算法,及算法的实现--垃圾回收器;

          第一点就是本文下面的主题,这是垃圾回收的基础,如:可达性分析算法是后面垃圾回收算法的基础,而判断哪些对象可以回收是垃圾回收的首要任务。

    2、判断对象可以回收

    垃圾收集器对堆进行回收前,首先要确定堆中的对象哪些还"存活",哪些已经"死去";

           下面先来了解两种判断对象不再被引用的算法,再来谈谈对象的引用,最后来看如何真正宣告一个对象死亡。

    2-1、引用计数算法(Recference Counting)

    1、算法基本思路

           给对象添加一个引用计数器,每当有一个地方引用它,计数器加1;

           当引用失效,计数器值减1;

           任何时刻计数器值为0,则认为对象是不再被使用的;    

    2、优点

          实现简单,判定高效,可以很好解决大部分场景的问题,也有一些著名的应用案例;

    3、缺点

    (A)、很难解决对象之间相互循环引用的问题

           如:

    ReferenceCountingGC objA = new ReferenceCountingGC();
    ReferenceCountingGC objB = new ReferenceCountingGC();
    objA.instance = objB;
    objB.instance = objA;
    objA = null;
    objB = null;

    当两个对象不再被访问时,因为相互引用对方,导致引用计数不为0;

           更复杂的循环数据结构,如图(《编译原理》7-18):

    B)、并且开销较大,频繁且大量的引用变化,带来大量的额外运算;

          主流的JVM都没有选用引用计数算法来管理内存;

    2-2、可达性分析算法(Reachability Analysis)

    也称为传递跟踪算法;

          主流的调用程序语言(Java、C#等)在主流的实现中,都是通过可达性分析来判定对象是否存活的。

    1、算法基本思路

          通过一系列"GC Roots"对象作为起始点,开始向下搜索;

          搜索所走过和路径称为引用链(Reference Chain);

          当一个对象到GC Roots没有任何引用链相连时(从GC Roots到这个对象不可达),则证明该对象是不可用的;

    2、GC Roots对象

          Java中,GC Roots对象包括:

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

          (2)、方法区中类静态属性引用的对象;

          (3)、方法区中常量引用的对象;

          (4)、本地方法栈中JNI(Native方法)引用的对象;

          主要在执行上下文中和全局性的引用;

    3、优点

          更加精确和严谨,可以分析出循环数据结构相互引用的情况;

    4、缺点

          实现比较复杂;

          需要分析大量数据,消耗大量时间;

          分析过程需要GC停顿(引用关系不能发生变化),即停顿所有Java执行线程(称为"Stop The World",是垃圾回收重点关注的问题);

    后面会针对HotSpot虚拟机实现的可达性分析算法进行介绍,看看是它如何解决这些缺点的。

    2-3、再谈对象引用

    在《Java对象在Java虚拟机中的引用访问方式》曾详细介绍过对象的引用问题,这与对象回收算法有很大关系,下面再来了解下。

          java程序通过reference类型数据操作堆上的具体对象;

    1、JVM层面的引用

          reference类型是引用类型(Reference Types)的一种;

          JVM规范规定reference类型来表示对某个对象的引用,可以想象成类似于一个指向对象的指针;

          对象的操作、传递和检查都通过引用它的reference类型的数据进行操作

    2、Java语言层面的引用

    (i)、JDK1.2前的引用定义

          如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用;

          这种定义太过狭隘,无法描述更多信息;

    (ii)、JDK1.2后,对引用概念进行了扩充,将引用分为:

    (1)、强引用(Strong Reference)

          程序代码普遍存在的,类似"Object obj=new Object()";

          只要强引用还存在,GC永远不会回收被引用的对象;

    (2)、软引用(Soft Reference)

          用来描述还有用但并非必需的对象;

          直到内存空间不够时(抛出OutOfMemoryError之前),才会被垃圾回收;

          最常用于实现对内存敏感的缓存;

          SoftReference类实现;

    (3)、弱引用(Weak Reference)

          用来描述非必需对象;

          只能生存到下一次垃圾回收之前,无论内存是否足够;

           WeakReference类实现;

    (4)、虚引用(Phantom Reference)

          也称为幽灵引用或幻影引用;

          完全不会对其生存时间构成影响;

          唯一目的就是能在这个对象被回收时收到一个系统通知;

          PhantomRenference类实现;                

          更多请参考JDK相关API说明;

          对于软引用,可以使用命令行选项"-XX:SoftRefLRUPolicyMSPerMB = <N>"来控制清除速率;

          更多请参考:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/considerations.html#sthref65

    2-4、判断对象生存还是死亡

     要真正宣告一个对象死亡,至少要经历两次标记过程。

    1、第一次标记

          在可达性分析后发现到GC Roots没有任何引用链相连时,被第一次标记;

          并且进行一次筛选:此对象是否必要执行finalize()方法;

    (A)、没有必要执行

          没有必要执行的情况:

          (1)、对象没有覆盖finalize()方法;

          (2)、finalize()方法已经被JVM调用过;

          这两种情况就可以认为对象已死,可以回收;

    (B)、有必要执行

          对有必要执行finalize()方法的对象,被放入F-Queue队列中;

          稍后在JVM自动建立、低优先级的Finalizer线程(可能多个线程)中触发这个方法;

    2、第二次标记

          GC将对F-Queue队列中的对象进行第二次小规模标记;

          finalize()方法是对象逃脱死亡的最后一次机会:

          (A)、如果对象在其finalize()方法中重新与引用链上任何一个对象建立关联,第二次标记时会将其移出"即将回收"的集合;

          (B)、如果对象没有,也可以认为对象已死,可以回收了;                    

          一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;

    2-5、finalize()方法

    上面已经说到finalize()方法与垃圾回收第二次标记相关,下面了解下在Java语言层面有哪些需要注意的。

          finalize()是Object类的一个方法,是Java刚诞生时为了使C/C++程序员容易接受它所做出的一个妥协,但不要当作类似C/C++的析构函数;

          因为它执行的时间不确定,甚至是否被执行也不确定(Java程序的不正常退出),而且运行代价高昂,无法保证各个对象的调用顺序(甚至有不同线程中调用);

          如果需要"释放资源",可以定义显式的终止方法,并在"try-catch-finally"的finally{}块中保证及时调用,如File相关类的close()方法;

          此外,finalize()方法主要有两种用途:

    1、充当"安全网"

          当显式的终止方法没有调用时,在finalize()方法中发现后发出警告;

          但要考虑是否值得付出这样的代价;

          如FileInputStream、FileOutputStream、Timer和Connection类中都有这种应用;

    2、与对象的本地对等体有关

          本地对等体:普通对象调用本地方法(JNI)委托的本地对象;

          本地对等体不会被GC回收;

          如果本地对等体不拥有关键资源,finalize()方法里可以回收它(如C/C++中malloc(),需要调用free());

          如果有关键资源,必须显式的终止方法;

          一般情况下,应尽量避免使用它,甚至可以忘掉它。

    更多请参考:

          《How to Handle Java Finalization's Memory-Retention Issues》:http://www.devx.com/Java/Article/30192

          《Effective Java》第二版 第2章 第7条:避免使用终结方法;

          《Thinking in Java》第四版 5.5 清理:终结处理和垃圾回收;

          《Java语言规范》12.6 类实例的终结;

  • 相关阅读:
    记一道乘法&加法线段树(模版题)
    2021CCPC网络赛(重赛)题解
    Codeforces Round #747 (Div. 2)题解
    F. Mattress Run 题解
    Codeforces Round #744 (Div. 3) G题题解
    AtCoder Beginner Contest 220部分题(G,H)题解
    Educational Codeforces Round 114 (Rated for Div. 2)题解
    Codeforces Global Round 16题解
    Educational Codeforces Round 113 (Rated for Div. 2)题解
    AtCoder Beginner Contest 182 F
  • 原文地址:https://www.cnblogs.com/alsf/p/9484790.html
Copyright © 2011-2022 走看看