JVM 2-垃圾收集及内存分配策略

  Java虚拟机的内存模型分为五个部分,分别是:程序计数器、Java虚拟机栈、本地方法栈、堆、方法区。程序计数器、Java虚拟机栈、本地方法栈都是线程私有的,也就是每条线程都拥有这三块区域,而且会随着线程的创建而创建,线程的结束而销毁。在这几个区域内就不需要过多的考虑回收的问题。

  然而,堆和方法区中的内存清理工作就没那么容易了。 

堆和方法区所有线程共享,并且都在JVM启动时创建,一直得运行到JVM停止时。因此它们没办法根据线程的创建而创建、线程的结束而释放。

堆中存放JVM运行期间的所有对象,虽然每个对象的内存大小在加载该对象所属类的时候就确定了,但究竟创建多少个对象只有在程序运行期间才能确定。 
方法区中存放类信息、静态成员变量、常量。类的加载是在程序运行过程中,当需要创建这个类的对象时才会加载这个类。因此,JVM究竟要加载多少个类也需要在程序运行期间确定。 因此,堆和方法区的内存回收具有不确定性,因此垃圾收集器在回收堆和方法区内存的时候花了一些心思。

  判断对象的死活
  1 引用计数法:给对象添加一个引用计数器每当有一个地方引用它时,计数器就加1,当引用失效时 计数器减1,当计数器为0时表示对象不可能再被使用 表示对象已死。

这个方法效率很高,但是它无法解决对象之间的互相依赖的问题。

public class ReferenceCountingGC {

    public Object instance = null;

    private static final int _1MB = 1024 * 1024;

    /** * 这个成员属性的唯一意义就是占点内存,以便在能在GC日志中看清楚是否有回收过 */
    private byte[] bigSize = new byte[2 * _1MB];

    public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;//这两个变量 被设置为空,对象不可能再被使用
        objB = null;

        // 假设在这行发生GC,objA和objB是否能被回收?
        System.gc();
    }
    public static void main(String[] args) {
        testGC();
    }
}

  但是运行结果显示并未回收它们,间接说明jvm不是通过引用计数法来判断对象的死活。

  2 可达性分析

    通过一系列 GC Roots对象作为起点,从这些节点向下搜索 搜索的路径称为引用链 当一个对象没有任何一个引用链相连 则说明对象是不可用的。如Object 5,Object 6,Object 7 虽然有互相关联 但是GC Roots 不可达,所以他们都是可以回收的对象。

  

  java中可以作为GC Roots的对象有下面几种

    • Java虚拟机栈所引用的对象(栈帧中局部变量表中引用类型的变量所引用的对象)
    • 方法区中静态属性引用的对象
    • 方法区中常量所引用的对象
    • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

  java里面的各种引用的分类:这几种引用级别由高到低分别为:强引用、软引用、弱引用和虚引用。

    强引用:如:Object object=new Object();那object就是一个强引用了,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

    软引用:软引用就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存 空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

    弱引用:

      如果一个对象只具有弱引用,那就类似于可有可无的生活用品。它比软引用的强度更弱一些当垃圾收集器工作时 无论当前内存是否足够,那些只被弱引用关联的对象都会被回收掉。
    虚引用:虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象 仅持有虚引用,那么它就和没有任何引用一样。

   对象的死亡与拯救

    即使在可达性分析算法中不可达的对象,也不是一定会死亡的,它们暂时都处于“缓刑”阶段,要真正宣告一个对象“死亡”,至少要经历两次标记过程:

    如果对象在进行可达性分析后发现没有与 GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finaliza()方法。当对象没有覆盖finaliza()方法,或者finaliza()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

    如果这个对象被判定为有必要执行finaliza()方法,那么此对象将会放置在一个叫做F-Queue的队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发此方法,但并不承诺会等待它运行结束,原因是:如果一个对象在finaliza()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能导致F-Queue队列中的其它对象永久处于等待,甚至导致整个内存回收系统崩溃。

  

/**
 * 此代码演示了两点: 
 * 1.对象可以在被GC时自我拯救。 
 * 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
 * @author zzm
 */
public class FinalizeEscapeGC {

    public static FinalizeEscapeGC 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 mehtod executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();

        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停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();
        // 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
    }
}

  值得注意的是,如果代码中有两段一模一样的代码段,执行结果却是一次逃脱成功,一次失败。这是因为任何一个对象的finalize()方法都只会被系统调用一次,如果对象面临下一次回收,它的finalize()方法不会再被执行,因此第二次逃脱行动失败。因此这个方法是非常不确定的,连是否会被执行都不能确定 因此这个方法无意义的。

  方法区回收 

  很多人以为方法区(或者HotSopt VM中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且性价比一般较低,在对的新生代生一般能回收70%~95%的空间,而永久代远低于此。

永久代的垃圾手机主要回收两部分内容:废弃常量和无用的类。 回收废弃常量与回收Java堆中的对象非常相似。以常量池中字面量的回收为例,若字符串“abc”已经进入常量池中,但当前系统没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用该字面量,若发生内存回收,且必要的话,该“abc”就会被系统清理出常量池。常量池中其他的类(接口)、方法、字段的符号引用与此类似。

无用的类需要满足3个条件:

(1)该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例;
(2)加载该类的ClassLoader已经被回收;
(3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

虚拟机可以对满足上述3个条件的无用类进行回收,此处仅仅是“可以”,而并不是和对象一样(不使用了就必然回收)

 


原文地址:https://www.cnblogs.com/tjqBlog/p/10498120.html