JVM学习笔记(三)

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


java 与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外的人想进去,墙内的内想出来。。。书中的一块好有哲♂学的话。。

 

垃圾收集(Garbage Collection)历史久远,其实在很久之前人们就开始思考GC需要完成的三件事情。
1.那些内存需要回收?
2.什么时候回收?
3.如何回收?

到现在,内存的动态分配与内存回收技术已经十分成熟了。一切看起来已经进入了“自动化”的时代了,为什么还要去了解与内存分配?
答案就是:
当需要排查各种内存溢出,内存泄露问题时,当垃圾收集成为系统更高并发量的瓶颈时,我们需要对这些“自动化”的技术实施必要的监控和调节。
在java内存运行时划分的数据区域中,程序计数器,虚拟机栈,本地方法栈都是线程私有的,生命周期随着线程的启动而开始,也随着线程的销毁而结束。
栈中的栈桢随着方法的进入与退出,有条不紊地进行着入栈出栈。每一个栈桢的内存大小基本是在类结构确定下来的时候就已经知道了(尽管在运行期间,JIT编译器会进行一些优)。
这几个区域的内存分配与回收都具备确定性,不需要过多考虑回收的问题。因为随着方法的结束,线程的销毁,内存自然就跟着回收了。
但是java堆、方法区不一样,一个接口的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存可能也不一样。只有在程序运行期间才会知道创建那些对象,这部分的内存分配与回收
都是动态的。垃圾收集器所关注的也是这部分内存。

如何判断对象存活还是死去?
   堆中几乎存放着所有的对象实例,垃圾回收器对堆回收之前,需要确认对象是存活,还是死去(即不可能被任何途径使用的对象)。
1.引用计数算法:作为最早的算法,它是这样的:给对象添加一个引用计数器,每当有一个地方引用他的时候,计数器的值就+1,当引用失效的时候,计数器的值就-1;
                            任何时刻计数器都为0的对象就是不可能再被使用的了
引用计数算法简单,判断效率也高。但是java并没有使用引用计数算法来管理内存,原因是它很难解决对象之间的相互循环引用的问题

2.根搜索算法通过的一系列的名为“GC Roots ” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径,称为引用链(Reference Chain),当一个对象到GC Roots没有任何
                        引用链相连(用图论的话来说就是从“GC Roots 到这个对象不可达)时,则证明此对象是不可用的。

 

 

 在java里面。可作为GC Roots 的对象有下面几种
1.虚拟机栈(栈桢中的本地变量表)中的引用的对象。
2.方法区中的类静态属性引用的对象。
3.方法区中的常量引用的对象。
4.本地方法栈中JNI(即一般说的 Native方法)的引用的对象。

再谈引用:
    无论是通过引用计数算法判断对象的引用数量,还是通过根搜索算法判断对象的引用链是否可达。判断对象是否存回都与引用有关。
在JDK1.2之前,java中的引用的定义很传统:如果引用类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。
但是这定义虽然纯粹,但是很狭隘。
在JDK1.2之后,java对引用的概念进行了扩充,将引用分为 强引用,软引用,弱引用,虚引用。这四种引用强度依次递减。。

强引用:在程序代码中普遍存在的,类似“Object object =  new Object()”这类的引用,只要强引用还在,垃圾收集器永远不会回收被引用的对象。
软引用:一些还有用,但并非必须的对象。对于软引用的对象,在系统将要发生内存溢出的异常之前,将会把这些对象列入回收范围之中并进行第二次回收。
              如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在jdk1.2之后。提供了SoftReference来实现软引用。
弱引用:也是描述非必须对象的。但是它的强度比软引用更弱,被软引用关联的对象,只能活到下一次垃圾收集发生之前。当垃圾收集器工作室,无论当前内存
              是否足够,都会回收掉被弱引用关联的对象。jdk1.2之后,提供 WeakReference类来实现弱引用。
虚引用:也成幽灵引用或者幻影引用。他是引用关系最弱的一种。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得对象实例。
              为一个对象设置虚引用唯一的目的就是希望它被回收时收到一个系统通知。在jdk1.2之后,提供了PhantomReference类来实现虚引用。

生存还是死亡?这是个问题。。。。(哈哈哈哈,突然想到了扁鹊的台词)
   在根搜索算法中不可达的对象,也不是说一定就会被回收,这是他们暂时处于“缓刑”阶段,要真正宣告一个对象的死亡,至少要经历两次标记过程。
如果对象在进行根搜索后,发现没有与GC Roots 相连接的引用链。那么他就会被第一次 标记并且进行一次筛选,筛选的条件是此对象是否有必要执行
finalize() 方法。对象没有覆盖finalize() 方法,或者finalize() 方法已经被虚拟机调用过,虚拟机将这种情况视为“没有必要执行”。
   如果这个对象被判定为有必要执行finalize() 方法,那么这个对象就会被放置在一个名为 F-Queue 的队列中,并在稍后由一条由虚拟机自行建立的,优先级低的
Finalizer 线程去执行。注意,这里的执行指的是虚拟机会触发这个方法,但不是说会等它运行结束。这么做的原因是:如果一个对象在finalize() 方法中执行缓慢,
或者发生了死循环,将可能会到期 F-Queue队列中的其他对象永久处于等待状态,甚至导致系统崩溃。finalize() 方法的对象逃脱死亡的最后一次机会。
稍候GC 将会对 F-Queue队列中的对象进行第二次小规模标记,如果对象想要活命,主要重新与引用链上的任意一对象建立关联即可,譬如把自己(this关键字)复制给某个类变量
或对象的成员变量,在第二次标记时会把他移出去。
代码小案例

public class FinalizeEscapeGC {

    public  static   FinalizeEscapeGC  SAVE_HOOK = null;

    public  void  IsAlive(){

        System.out.println("嘿嘿嘿。。。。我还活着");
    }

    @Override
    public  void  finalize() throws Throwable{

        super.finalize();
        System.out.println("finalize Method executed!");
        FinalizeEscapeGC.SAVE_HOOK=this;
    }

    public static void main(String[] args) throws Throwable{

        SAVE_HOOK = new FinalizeEscapeGC();

        SAVE_HOOK=null;
        System.gc();
        Thread.sleep(500);
        // 第一次救了自己
        if(SAVE_HOOK !=null){
            SAVE_HOOK.IsAlive();
        }else {
            System.out.println("什么?朕已经凉了???");
        }

        SAVE_HOOK=null;
        System.gc();
        Thread.sleep(500);
        // 第二次凉了
        if(SAVE_HOOK !=null){
            SAVE_HOOK.IsAlive();
        }else {
            System.out.println("什么?朕已经凉了???");
        }

    }


}

以上输出结果是

finalize Method executed!
嘿嘿嘿。。。。我还活着
什么?朕已经凉了???

   从上面可以看出,SAVE_HOOK 对象的 finalize()方法确实被GC收集器触发过,而且在收集之前成功逃脱了。
但是,上述代码中有两个代码片段是一模一样的,执行结果却是一次成功,一次死亡,这是因为任意一个对象的
finalize()方法只会被系统自动调用一次,如果对象面临下一次回收,他的finalize()方法不会被执行,所以自救失败了。
需要注意的是finalize()方法 运行代价高,不确定性大,无法保证各个对象的调用顺序。它能做的所有工作,使用try-finally  或者其他方式
都可以做的更好,更及时。。。其实大家可以忘了他。。。我偷电瓶车养你。。。咳咳。。不好意思。。跑题了。。。。

 


   

 

 

posted @ 2020-03-04 17:48  小羊小恩  阅读(166)  评论(0)    收藏  举报