Java 的垃圾回收策略

栏目: Java · 发布时间: 7年前

内容简介:Java 的垃圾回收策略

当程序 创建对象,数组引用类型实体 时,系统都会在 堆内存 中为之分配一块内存区,对象就保存在这块内存区,当这块内存不再被任何变量引用时,这块内存就变成垃圾,系统就要回收。

  • 只回收堆内存中对象,不会回收物理资源
  • 程序无法精确控制回收时机。
  • 在垃圾回收机制回收任何对象之前,总会先调用它的 finlize() 方法,可能导致 垃圾回收机制取消

如何判断对象是否已经死亡?

引用计数算法

古老的判断对象是否存活的算法是:给对象添加一个计算器,一旦有地方引用该对象,则计数器加一,当引用失效时,就减一。任何计数器为 0 的对象,就不能再使用了。这种算法虽然经典,但是其并不能解决一个对象之间相互循环引用的问题。

public class RefreenceCount{
  
  class GcObject{
  	public Object instance = null;
}
  
  public static void main(String[] args){
	GcObject o1 = new GcObject(); 
    GcObject o2 = new GcObject();
    
    o1.instance = o2; // 1
    o2.instance = o1; // 2
    
    o1 = null;
    o2 = null;
}
}

如上我们在最后注释一和注释二处还是无法释放对方。这样会造成堆溢出。

可达性分析算法

主流的语言中都是通过可达性算法分析对象是否存活的:通过一系列称为 “GC Roots” 对象作为起始点,从这些节点开始向下搜索,其所走过的路径称为引用链,当一个对象到 GC Roots 不可达时,则该对象是不可用的,也就是判断这些对象为可回收的。

Java 语言中,如下可作为 GC Roots 对象:

  • 虚拟机栈中引用的本地对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI 引用的对象

而可达性算法,我们可以看一个非常好的示例。 垃圾回收机制中,引用计数法是如何维护所有对象引用的

再谈引用

前面所说的两种都是跟引用有关系。在 JDK1.2 之前,引用的定义很狭隘,一个对象只有被引用和未被引用的两种状态。在 JDK1.2 之后,对引用的定义进行了扩充,我们希望:当内存空间还足够时候,则对象能够保留在内存中;如果内存空间在进行垃圾回收之后,内存空间还是非常紧张,则抛弃这些对象。适合于很多缓存功能的应用场景。分成四种强度递减的引用:

  • 强引用:这种引用很常见,类似于 :

    A a = new A()// 强引用

    只要引用存在,就不会回收引用对象。如果显示的将对象置为 null 或者其超出对象的生命范围,则被回收。当内存空间不足,Java虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题

  • 软引用:如果一个对象具有软引用,内存足够时,gc不会回收它;内存不足时,gc就会回收这个对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。 软引用可用来实现内存敏感的高速缓存。

  • 弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象, 不管当前内存空间足够与否,都会回收它的内存

    public class WeakRefrenceDemo {
    
        public static void main(String[] args) {
    
            String s = new String("hello");// 1
            WeakReference<String> wrf = new WeakReference<String>(s);
            s = null;// 2
            System.out.println(wrf.get());// 3
            System.gc();// 4
            System.runFinalization();
            System.out.println(wrf.get());// 5
        }
    }

    如上,我们逐行分析。在注释 1 处,我们不能自以为聪明的用:

    String s = "hello"// 6

    来替代 1 中的语句,因为按照 6 中的做法,系统会采用常量池来管理这个字符串直接量,会采用强引用来引用它。同时在 1 处我们用 s 引用变量引用 “hello” 字符串对象,接下来创建一个弱引用对象来引用 s 引用的同一个对象。执行到 2 处时候,切断了 s 和引用对象的之间的联系。而我们不切断的话,会发生什么呢?自然 gc 机制无法发挥出实质性的作用,导致并没回收任何引用对象,故输出的还是字符串对象。

    执行到 3 处时候,由于系统内存还足够,不会回收 wrf 对象,因此会输出 “hello” 对象。而后通知程序进行强制垃圾回收,自然弱引用对象 wrf 就会被系统回收了,此时输出的就为 null 了。

    下面再看一个例子,弱引用在 Android 中的应用,我们希望在 Activity 中新建一个线程获取数据:

    public class MainActivity extends Activity {
    
        //...
        private int page;
        private Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                if (msg.what == 1) {
                    //...
                    page++;
                } else {
                    //...
                }
            };
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            //...
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //..
                    Message msg = Message.obtain();
                    msg.what = 1;
                    //msg.obj = xx;
                    handler.sendMessage(msg);
                }
            }).start();
            //...
        }
    }

    Activity 具有自身的生命周期,Activity 中新开启的线程运行过程中,可能此时用户按下了Back键,或系统内存不足等希望回收此 Activity, 由于 Activity 中新起的线程并不会遵循 Activity 本身的什么周期,也就是说,当 Activity 执行了onDestroy,由于线程以及 Handler 的 HandleMessage 的存在, 使得系统本希望进行此Activity 内存回收不能实现,因为非静态内部类中隐性的持有对外部类的引用,导致可能存在的内存泄露问题。

    因此,在 Activity 中使用 Handler 时,一方面需要将其定义为静态内部类形式,这样可以使其与外部类(Activity)解耦,不再持有外部类的引用, 同时由于 Handler 中的 handlerMessage 一般都会多少需要访问或修改 Activity 的属性,此时,需要在 Handler 内部定义指向此 Activity 的 WeakReference, 使其不会影响到 Activity 的内存回收同时,可以在正常情况下访问到 Activity 的属性。

    google 官方推荐的一个示例写法如下:

    public class MainActivity extends Activity {
    
        //...
        private int page;
        private MyHandler mMyHandler = new MyHandler(this);
        private static class MyHandler extends Handler {
            private WeakReference<MainActivity> wrActivity;
            public MyHandler(MainActivity activity) {
                this.wrActivity = new WeakReference<MainActivity>(activity);
            }
    
            @Override
            public void handleMessage(Message msg) {
                if (wrActivity.get() == null) {
                    return;
                }
                MainActivity mActivity = wrActivity.get();
                if (msg.what == 1) {
                    //...
                    mActivity.page++;
                } else {
                    //...
                }
            }
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            //...
            new Thread(new Runnable() {
                @Override
                public void run() {
                    //..
                    Message msg = Message.obtain();
                    msg.what = 1;
                    //msg.obj = xx;
                    mMyHandler.sendMessage(msg);
                }
            }).start();
            //...
        }
    }

  • 虚引用:是一种最弱的引用关系,其存在的必要就是在该虚引用对象被垃圾回收器回收时候,系统能够接收到一个通知。

生存还是死亡

当一个对象被创建之后,根据它是否被引用变量所引用的状态,可以将其所处状态分为如下三种:

  • 可达状态:当一个变量被创建之后,有一个或者以上的变量引用它,其就处于可达状态。
  • 可恢复状态:没有任何对象来引用它,就处于可恢复状态。这种状态下,系统的垃圾回收机制准备来回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复对象的 finalize() 方法来进行资源的清理,如果系统在清理资源的同时,重新让一个引用变量引用该对象,则该对象会再次变为可达对象,反之则该对象进入不可达状态。
  • 不可达状态:没用引用变量指向该对象,并且不能被恢复,则成为不可达状态。只有一个对象在不可达状态时候,垃圾回收器才会回收该对象。

然而即使某个对象被标记为不可达的情况下,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与 GC Roots 相连接的引用链,那它会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法,或 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。如果该对象被判定为有必要执行 finalize() 方法,那么这个对象将会被放置在一个名为 F-Queue 队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize() 方法是对象逃脱死亡命运的最后一次机会(因为一个对象的 finalize() 方法最多只会被系统自动调用一次),稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果要 在 finalize() 方法中成功拯救自己,只要在 finalize() 方法中让该对象重引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。接下来,我们顺便看一下 finalize() 方法:

在垃圾回收机制回收某个对象之前,通常会调用 finalize() 方法来回收一些资源。在没有明确指定回收方法之前,调用默认的 finalize() 放法,只有在调用了该方法之后,垃圾回收机制才真正的开始执行。但是我们始终要注意该方法不一定会得到执行,故我们不要指望在该方法中去执行垃圾清理的工作。finalize() 方法具有以下特点:

  • 不要主动调用某个对象的 finalize() 方法
  • finalize() 方法是否被调用具有不确定性
  • JVM 执行可恢复对象的 finalize() 方法时候,可能会是该方法重新变为可达状态
  • JVM 执行 finalzie() 方法出现异常时,不会抛出异常

接下来,我们看一个对象自我拯救的示例:

public class FinalizeEscapeGC {

    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive(){
        System.out.println("yes i am still live");
    }

    // 留个心眼,这个方法每个对象只会被系统自动调用一次
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method called");
        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("no,i am dead");
        }

        // 对象第二次拯救自己失败
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else{
            System.out.println("no,i am dead");
        }
    }
}

输出结果:

finalize method called // 标注 finalize 被调用
yes i am still live // 逃脱成功
no,i am dead // 逃脱失败

为什么会出现对象的复活?又为什么会出现同样的代码,而对象只会复活一次?首先解决第一个问题,对象复活的奥秘完全在注释一处,我们在 finalize() 方法中执行了这样一句:

FinalizeEscapeGC.SAVE_HOOK = this;// 注释一

上面就将一个可恢复的对象变成了可达对象。而后面只能复活一次的原因是因为一个对象的 finalize() 方法只能被调用一次。

如何去回收垃圾

  • 标记-清除算法

    首先标记所有需要被清除的对象,然后进行回收,至于判定哪些对象该被回收,前面已经说过了。由于该算法是比较基础的算法,所以不可避免的带来两个问题:标记和清除效率不高的问题、空间碎片化的问题。

  • 复制算法

    复制算法是针对标记—清除算法的缺点,在其基础上进行改进而得到的,它讲 可用内存按容量 分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。其缺点很明显,将可用内存缩小到了一半。复制算法有如下优点:

    • 每次只对一块内存进行回收,运行高效。
    • 只需移动栈顶指针,按顺序分配内存即可,实现简单。
    • 内存回收时不用考虑内存碎片的出现。
  • 标记-整理算法

    其标记过程仍然跟标记-清除算法一样。但是该算法不会直接对可回收对象进行清理,而是让所有存活的对象都向一段移动,然后直接清理端边界以外的内存。

  • 分代收集算法

    当前用的比较多的垃圾收集算法都是分代收集。根据对象存活周期的不同,将内存分为几块。一般是将 Java 堆分为新生代和老生代,根据各个年代的特点采用合适的垃圾回收算法。在新生代中每次都有大批对象死去,只有少量存活,故而采用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高,没有额外空间对其进行分配担保,就必须使用标记-整理或者标记-清理算法来进行回收。

内存分配策略

  • 对象优先在 Eden 分配

    大多数情况下,对象优先在新生代 Eden 区中分配,当该区域没有足够空间时候,将发生一次 MinorGC。其中我们在复制算法中提到了:新生代 98% 对象是朝生夕死,所以没必要 1:1 分布内存空间,从而产生了一块较大的 Edge 和两块 Survivor 空间。

  • 大对象直接进入老年代

  • 长期存活的对象将进入老年代

    虚拟机采用了分代会回收的思想来管理内存,故判断新老对象的标准就很重要。虚拟机给每个对象定义了一个对象年龄,如果对象在 Eden 区 出身,并且经过第一个 Minor GC 之后仍然存活,并且能被 Survivor 区域容纳的话,将被移动到 Survivor 区域中,并将对象年龄设置为1.对象在 S 区域中每熬过一次 MInor GC,年龄就增加1.当年龄增加到一定 15岁时候,就会被存入到老年代。年龄阈值可以通过设置指定参数来确定。

  • 动态对象年龄判定

  • 空间分配担保


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Mastering Regular Expressions, Second Edition

Mastering Regular Expressions, Second Edition

Jeffrey E F Friedl / O'Reilly Media / 2002-07-15 / USD 39.95

Regular expressions are an extremely powerful tool for manipulating text and data. They have spread like wildfire in recent years, now offered as standard features in Perl, Java, VB.NET and C# (and an......一起来看看 《Mastering Regular Expressions, Second Edition》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

在线进制转换器
在线进制转换器

各进制数互转换器

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具