• 误区纠正:关于单例模式的内存分析


           小菜最近在读《Java与模式》一书时,发现关于单例模式的章节中有这样一段话:

           作者想表达的大意为:为了实现某个对象能够持久在内存中,以供程序在整个运行周期都可以访问,可以让对象的某个成员变量持有一个指向自身的引用,来避免被回收。

           成员变量想要被清空,需要等待对象被释放,而对象被释放需要没有引用指向它,此时成员变量恰恰指向了对象本身,这看起来很不错,形成了一个循环。

           但实际上,这种说法是不准确的,容易让读者产生误解。

           请看下边这段代码:

     1 package com.cnblogs.test;
     2 
     3 public class SingletonTest {
     4 
     5     public static void main(String[] args) {
     6         
     7         //调用测试方法
     8         test();
     9         //通知jvm回收无用资源
    10         System.gc();
    11         
    12         System.out.println("main finalize ...");
    13     }
    14     
    15     //测试方法
    16     public static void test(){
    17         //创建A类的对象
    18         A a = new A();
    19         //让对象的成员变量指向其自身
    20         a._a=a;
    21     }
    22     
    23 }
    24 
    25 class A{
    26     
    27     //定义一个成员变量,用来保存对象本身
    28     public A _a = null;
    29     
    30     //对象被销毁时执行的方法
    31     protected void finalize() throws Throwable {
    32         System.out.println("A finalize ...");
    33     }
    34 }

           简单说明一下:

           当在main方法中调用test方法时,test方法会创建一个A类的实例a,同时把实例a的堆区地址放在实例a的成员变量_a中,也就是在模拟成员变量持有指向自身对象的引用。

           当以上步骤执行完成后,test方法结束, 由于a是局部变量,保存在方法栈中,会被立即释放,不再指向A类的实例,但是我们刚刚完成了“自引用”,根据上边的理论,有引用指向A类的实例,实例便不会被释放,因此上边程序的输出结果是“main 方法执行完毕...”。

           遗憾的是,结果并不是这样,真实的输出结果为:“main 方法执行完毕...A 被回收...”。

           为什么会这样?听听小菜的解释。

           单例模式创建的对象能够一直存在于内存中不被释放,并不只是由于持有一个自身的引用,本质是因为这个引用是静态的!也就是说,如果成员变量是非静态的,它持有一个自身的引用,那么这个对象还是会被回收。

         “系统内至少保持一个对对象的引用”,这个引用指的是从栈区或方法区中发出的引用,也就是安全的引用。

         类被实例化之后,是放在堆区中的,而我们是无法直接操作堆内存的!因此需要一个引用,指向堆区的某个区域,而这个引用,必须是从栈中(或方法区中)发出的,因为我们可以直接访问栈内存,如果是从堆中发出的引用,是无意义的引用,我们根本访问不到,因此会被回收。

         类的成员变量恰恰是放在堆内存中,因此由类的成员变量持有一个对象的引用,这个引用是不安全的(不安全≠无效)!!

         再举个例子,如果栈区的a局部变量指向堆中的b对象,b对象的某个成员变量指向堆中的c对象。我们可以通过a访问b,再由b访问c。假如一旦a不再指向b,那么再也访问不到b,c也不可能被访问到,即便b此时仍然指向c,但都成了无法访问的对象,b、c均会被jvm自动回收。

         单例模式中成员变量是静态的,它并不保存在堆内存中,而是在方法区中,是一块持久的内存空间,不会被自动回收,因此指向自身的引用是安全的,自身不会被回收。

         到此,如果我们把第一个例子中的成员变量改成静态的,那么test方法执行结束后,A类的对象不会被回收,程序输出结果为:“main 方法执行完毕...”。

         最后提一句,堆区是整个程序共享的,因此可能会出现线程安全问题。

  • 相关阅读:
    spring原理
    架构师和数学
    项目经理需要注意的地方
    如何快速掌握一门新技术
    项目管理要做啥
    编程原则
    架构设计的常用思想
    聊聊编程范式
    程序员与哲学家
    IT人员如何有效规划自己时间
  • 原文地址:https://www.cnblogs.com/iyangyuan/p/3036776.html
Copyright © 2020-2023  润新知