JVM -- 垃圾回收

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM -- 垃圾回收相关的知识,希望对你有一定的参考价值。

前一篇大致讲解了JVM的内存结构,在对JVM有一定了解的基础上,接下来进行JVM垃圾收集的学习

垃圾收集器与内存分配策略

1.概述

  内存的动态分配与内存回收技术已经很成熟了,了解GC和内存分配:一方面为了当出现内存溢出,内存泄漏的时候排查问题,另一方面垃圾收集会成为实现更高并发量的瓶颈,所以我们需要对这些“自动化”的技术实施必要的监控和调节。

        对于程序计数器、虚拟机栈、本地方法栈这三个内存区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着进栈和出栈的操作,线程结束则内存被回收。所以线程私有的内存区域是不需要额外控制的。Java堆和方法区就不一样了,我们只有程序处于运行期间时才知道(多态导致)创建哪些对象,这部分的内存分配和回收都是动态的。内存的分配和回收指的就是Java堆和方法区部分的内存
 

3.2 回收堆中死亡对象

     堆中几乎存放着Java世界中所有的对象实例,垃圾收集器回收的内存就是那些已经死了的对象实例的内存。对象已死表示不可能再被任何途径使用的对象
  下面介绍几种判断对象是否存活的算法:

        3.2.1  引用计数法

            引用计数法判断对象存活的通过给对象添加一个引用计数器(在new Object时候初始化计数器为0),每当有一个地方引用(例如,在Object obj = new Object()时,计数器加1)它时,计数器就加1;当引用失效时(例如在方法中局部变量引用了对象,方法执行完成时,就是引用失效),计数器值就减1;任何时刻计数器都为0的对象就是不可能再被使用的
            引用计数法实际上是一个效率很高的算法,实现也简单,但是却存在一个隐患,对象之间的相互循环引用会导致内存泄漏【内存泄漏这里指的是该内存应该被但没有被回收,导致内存一直被占用】
如下情况:Java 代码
  
/**
* testGC 方法执行后,objA和objB会不会被GC回收呢?
*/
public class TestGC{
    public Object instance = null;
    
    public static void testGC(){
        TestGC objA = new TestGC();
        TestGC objB = new TestGC();
        
        objA.instance = objB;
        objB.instance = objA;
        //图一
        objA = null;
        objB = null;
        //图二
    }
    //图三
    
}
如下的几张图正是上面Java代码的内存,在testGC方法执行时,objA和objB为局部变量分别进栈,并且指向堆中的对象实例。
            图一  实际上应该Java栈到objB为止,没有别的变量进栈,下同
            图中的引用计数器为2
技术分享
            
                                            图二 图中的引用计数器为1
技术分享
                                                                                 图三 图中的引用计数器为1
技术分享
上面的图示解释了,引用计数法的缺陷,会导致内存泄漏。
 

3.2.2 根搜索算法

            根搜索算法是目前的流行的判断对象算法存活的算法。
            此算法的思路是通过一系列的名为“GCRoots”的对象作为起始点,从这些根节点开始向下搜索,搜索所走过的路径成为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(其实就是数据结构中的图来解释就是,GC Roots到该对象不可达)时。则证明此对象是不可用的
下图中的Object5,6,7会被回收,在上一例中的图二和图三中的objA和objB就会被回收
    技术分享
 

3.2.3 Java对引用的改进

           在JDK 1.2以前,引用定义很传统;如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就成这块内存代表着一个引用
           JDK1.2之后对引用类型进行了扩充,将引用分为强引用,软引用,弱引用,虚引用
           强引用是指类似于 Object obj = new Object()这类的引用,其实也就是直接存在于Java栈中的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象;
           软引用是指一些还有用但是不是必需的对象,这些对象会进行二次回收,也就是在内存块溢出的时候,将软引用进行标记,然后进行第一次回收,但是软引用对象没有被回收,如果第一次回收完成后,还是没有足够的内存,那么才会回收标记的软引用对象
          弱引用是指一些非必需的对象,在下一次垃圾回收时就会被回收。
          虚引用更弱,回收他们不会造成任何影响。

3.2.4 根搜索的生存还是死亡(覆盖finalize方法可以拯救一次)

          对于根搜索算法,当在发现对象不可达GC Roots时,并不是立即把他们回收了,先判断是否需要执行finalize()方法【注:如果对象没有覆盖finalize()方法,或者finalize方法已经被虚拟机执行过一次了,那么就视为没必要执行finalize()方法了,直接进行回收】。
          如果判断需要执行finalize()方法,那么这个对象会被放在一个F-Queue的队列中,处于F-Queue队列中的对象有一次逃脱死亡命运的机会,如果finalize()方法中只要把对象重新和reference chain(GC Roots所在的的图)链接上,那么就成功拯救了自己。譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量。
请用javac -className.java
       java  -className
来执行下面的文件,查看根搜索算法中的自我拯救。
/**
    下面的代码演示了两点:
    1.对象可以在GC时自我拯救
    2.这种自救的机会只有一次,因为一个对象的finalize方法最多只会被系统自动的执行一次
*/
public class EscapeGC{
    public static EscapeGC SAVE_HOOK = null;
    
    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");
        EscapeGC.SAVE_HOOK = this;//在这里吧自己赋值给了类变量,拯救了自己
    }
    
    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new EscapeGC();
        
        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        //因为Finalize方法优先级很低,暂停0.5秒,等待它
        Thread.sleep(500);
        if(SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no, i am dead :(");
        }
        
        //下面这段代码与上面的完全相同,但是这次自救却失败了
        SAVE_HOOK = null;
        System.gc();//一个对象的finalize方法最多会执行一次
        //因为Finalize方法优先级很低,暂停0.5秒,等待它
        Thread.sleep(500);
        if(SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no, i am dead :(");
        }
        
    }
    
    
    
    
}

在介绍完根搜索算法后,堆中对象的死亡判断算法已经介绍完了。接下来介绍一下方法区中的垃圾回收。

3.2.5 回收方法区

            方法区(HotSpot虚拟机中的永久代)在Java虚拟机规范中提到,不要求虚拟机在方法区实现垃圾收集,而且方法区的垃圾收集“性价比”很低而在堆中,尤其是新生代中,进行一次垃圾收集一般可以回收70%~95%的空间,而方法区的垃圾收集效率远低于此。
            方法区的垃圾收集主要回收两部分内容:废弃常量无用的类。回收废弃的常量和回收Java堆中的对象很类似【例如,字符串“abc”已经进入常量池,在内存回收时,发现没有任何的String对象引用常量池中的“abc”常量,那么这个时候“abc”会被系统清理出常量池】,常量池中包括类(接口)、方法、字段的符号引用【参考后面的类文件结构篇】
           判定一个类是否是“无用的类”相对复杂,需要满足如下条件:
   1:该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例【因为对象实例的数据有两部分组成:对象实例数据(Java堆中),对象类型数据(方法区中)】。
技术分享
   2:加载该类的Classloader已经被回收【这个还不知道为什么,需要在学习类加载机制之后来补充】。
        3:该类对应的java.long.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法【也就是该类的方法都是不能通过反射机制访问的】。
        就算满足上面的条件,HotSpot也可以选择不回收这些“无用的类”,要想回收无用的类,需要-Xnoclassgc参数进行控制,如前面所说的,方法区的垃圾回收是可以选择的
        而对于大量使用反射、动态代理、GCLib等bytecode框架的场景以及动态生成JSP的频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代(方法区)不会溢出
 

3.3 垃圾收集算法

3.3.1 标记-清除算法

待补充

3.3.2 复制算法

待补充

3.3.3 标记-整理算法

待补充

3.3.4 分代收集算法

待补充

 

 

以上是关于JVM -- 垃圾回收的主要内容,如果未能解决你的问题,请参考以下文章

多线程二:jvm中的主线程&垃圾回收线程

GC垃圾回收机制

GC垃圾回收机制

java中的垃圾回收机制是怎么回事?

JVM : 4 JVM的垃圾回收机制

垃圾回收开篇之作,带你走进 JVM 垃圾回收机制