[Java] 监控java对象回收的原理与实现

监控Java对象回收的原理与实现

一.监控Java对象回收的目的

监控Java对象是否回收的目的是:为了实现内存泄露报警。

内存泄露是指程序中对象生命周期(点击查看详情)已经进入不可见阶段,但由于编码错误或系统原因,仍然存在着GC roots持有或间接持有该对象的引用,导致该对象的生命周期无法继续向下流转,也就无法释放的现象。简单的来说即是:已实例化的对象长期被持有且无法释放或不能按照对象正常的生命周期进行释放。(点击这里查看《[Android]内存泄露排查实战手记》)

实现内存泄露报警,可以发现并解决程序上编码的错误,降低应用的内存使用,减少应用OOM的机率。

在本人Android开发中,监控的对象为Activity。

二.监控Java对象回收的原理

下图1中。对象X在失去了所有的强引用后(普通Java对象为在失去了所有的强引用,在Android中如Activity执行了onDestroy()方法),往listGCLog中添加该对象X的特征日志,然后listGCLog进入黄色的等待时间区域,如果在该等待时间内,对象X正常被终结,则从listGCLog中删除该对象的特征日志;如果在等待时间内仍然未被终结,则时间一过,程序检查listGCLog是否为空,并在不为空时做出内存泄露的报警。

图1. 对象的监控示意图

三.监控Java对象回收的时机

如果判定Java对象已经被回收呢?可以有3种办法:

1.          Java对象执行了finalize()方法

这个方法的实现依据每个对象在被垃圾回收器回收之前,都会调用该对象的finalize()方法。在该finalize()方法内执行图1中从listGC中删除X特征日志的操作,即不会引起内存泄露的报警了。

但这并不是一种好的实现方式。在分配该对象时,JVM需要在垃圾回收器上注册该对象,以便在回收时能够执行该重载方法;在该方法的执行时需要消耗CPU时间且在执行完该方法后才会重新执行回收操作,即至少需要垃圾回收器对该对象执行两次GC。

见下图2。回收重写finalize()方法的对象和正常的对象相比,前者所花费的回收时间比后者多了好多倍。当测试数量是10000时,前者消耗433ms是后者95ms的将近5倍;当数量越多时,时间差距则越来越大;当测试数量达到50000个时,前者消耗7553ms已经是后者217ms的35倍了!!

图2. 对象回收的时间消耗对比图

2.          利用WeakReferences(弱引用),当WeakReferences.get()返回为null时,即表示该弱引用的对象已经或处于垃圾回收器回收阶段。

与强引用和软引用相比,弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

当弱引用的对象已经或处于垃圾回收器回收阶段时,通过get()方法返回的值为null,此时执行图1中从listGC中删除X特征日志的操作,即不会引起内存泄露的报警了。

3.          利用PhantomReferences(虚引用)和ReferenceQueue(引用队列),当PhantomReferences被加入到相关联的ReferenceQueue时,则视该对象已经或处于垃圾回收器回收阶段了。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。

如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动,即执行图1中从listGC中删除X特征日志的操作,即不会引起内存泄露的报警了。

第2、3种方式的实现中,弱引用的get()是否返回null及虚引用是否被添加到引用队列中,系统都没有提供回调接口,所以在代码实现上,需要一起开启着一个子线程去检查。

以上三种方式的实现,都可以通过多次执行System.gc()操作,催促VM尽快对已经失去强引用的对象执行回收操作。但“催促”的意思是尽可能早,并不是立即就执行的意思。

其实也方法3中的PhanomReferences也可以使用WeakReferences代替实现。但两者还是有些差别的。见下图3。

弱引用的对象被清除后才会对执行finalize()方法,finalize()方法执行完毕后才是清除虚引用的对象。

由于执行finalize()方法时,该对象再次被赋值给某个强引用,所以从更加细密的角度上来看,使用虚引用+引用队列的方法来判断对象是否回收是最准确的。

图3. 对象的状态转换流程

四.监控Java对象回收的代码实现

本文中的代码是在Android上实现的,可以加QQ群Code2Share(363267446),从群共享文件中去下载获得。不想加群的“勤()劳()小蜜蜂”也可以通过下文中的描述自己编码实现吧。
本文属sodino原创,发表于博客:http://blog.csdn.net/sodino,转载请注明出处。
相关代码可以从QQ群Code2Share(363267446)中的群文件中下载。

下图4是示例代码操作界面效果图。点击“New”按钮,将会生成指定数量的Java对象,点击“Release”则将开始对象回收操作,对象全部回收后,所消耗的时间将会反馈在界面上。

图4. 示例代码操作界面效果图

1.          Java对象执行了finalize()方法

在示例代码中,FinalizeActivity、WeakReferencesActivity、PhantomReferencesActivity三个类中创建对象的代码都是差不多的方式,这里给下FinalizeActivity下的newObject()方法吧。

创建的对象都会被添加到listBusiness中,为了得到尽可能准确的创建时长,这里把添加特征日志的操作独立在创建代码的后面,每个对象都会有特征日志添加到listGCLog中,等待回收时检验。

   private void newObject(){
        txtResult.setText("");
        FinalizeObjectobj = null;
        long startNewTime= System.currentTimeMillis();
        for (int i = 0;i < number;i ++) {
            obj= new FinalizeObject(i);
            listBusiness.add(obj);
        }
        final long consume =System.currentTimeMillis() - startNewTime;
        runOnUiThread(new Runnable() {

            @Override
            public void run() {
                txtResult.setText("New "+ number +" objs,\nconsume:" + consume +"ms");
                btnNew.setEnabled(false);
                btnRelease.setEnabled(true);
            }
        });
        for (int i = 0;i < number;i ++) {
            obj= listBusiness.get(i);
            listGCLog.add(obj.idStr);
        }
        Log.d("ANDROID_LAB", "newObject" + number +"consume=" + consume);
    }

与上面newObject()方法相对应的是触发对象释放的方法为releaseObject()实现如下:

    private voidreleaseObject() {
        btnRelease.setEnabled(false);
        startGCTime = System.currentTimeMillis();
        listBusiness.clear();
        //清除操作并告诉VM有一大坨对象可以吃啦..
        System.gc();
    }

最重要的是得定义一个重写了finalize()方法的类,该类FinalizeObject的一个成员变量idStr表示一个该类对象特有的特征日志;在finalize()方法中,通过判断listGCLog是否包含该idStr来执行listGCLog的删除操作。当listGCLog的size()为0时,表示所有的对象已经被回收完毕,这时去计算所有对象的回收耗时与通知UI刷新界面。

代码如下:

  classFinalizeObject {
        int id = -1;
        // 特征日志
        StringidStr = null;
        publicFinalizeObject(int id) {
            this.id = id;
            this.idStr = Integer.toString(id);
        }

        @Override
        public void finalize() {
            boolean contains = listGCLog.contains(FinalizeObject.this.idStr);
            if (contains) {
                // 删除特征日志:isStr
                listGCLog.remove(idStr);
            }
            if (listGCLog.size() == 0){
                // 已经全部回收完毕了
                final long consume =(System.currentTimeMillis() - startGCTime);
                Log.d("ANDROID_LAB", "finalizesize=0, consumeTime=" + consume +" name=" +Thread.currentThread().getName());
                runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        StringnewObjStr = txtResult.getText().toString();
                        txtResult.setText(newObjStr+ "\n\nGC "+ number +"objs,\nconsume:" + consume +" ms");
                        btnNew.setEnabled(true);
                        btnRelease.setEnabled(false);
                    }
                });
            }
        }
    }

2.      利用WeakReferences(弱引用)

newObject()方法中,listGCLog直接记录与对象相应的WeakReferences即可。

  classFinalizeObject {
        int id = -1;
        // 特征日志
        StringidStr = null;
        publicFinalizeObject(int id) {
            this.id = id;
            this.idStr = Integer.toString(id);
        }

        @Override
        public void finalize() {
            boolean contains = listGCLog.contains(FinalizeObject.this.idStr);
            if (contains) {
                // 删除特征日志:isStr
                listGCLog.remove(idStr);
            }
            if (listGCLog.size() == 0){
                // 已经全部回收完毕了
                final long consume =(System.currentTimeMillis() - startGCTime);
                Log.d("ANDROID_LAB", "finalizesize=0, consumeTime=" + consume +" name=" +Thread.currentThread().getName());
                runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        StringnewObjStr = txtResult.getText().toString();
                        txtResult.setText(newObjStr+ "\n\nGC "+ number +"objs,\nconsume:" + consume +" ms");
                        btnNew.setEnabled(true);
                        btnRelease.setEnabled(false);
                    }
                });
            }
        }
    }

这里需要开启子线程去判断弱引用get()是否返回null。当返回值为null时就把listGCLog中删除相应的特征日志。当listGCLog.size()为0时,则表示VM已经把一大坨对象吃掉了。

  classFinalizeObject {
        int id = -1;
        // 特征日志
        StringidStr = null;
        publicFinalizeObject(int id) {
            this.id = id;
            this.idStr = Integer.toString(id);
        }

        @Override
        public void finalize() {
            boolean contains = listGCLog.contains(FinalizeObject.this.idStr);
            if (contains) {
                // 删除特征日志:isStr
                listGCLog.remove(idStr);
            }
            if (listGCLog.size() == 0){
                // 已经全部回收完毕了
                final long consume =(System.currentTimeMillis() - startGCTime);
                Log.d("ANDROID_LAB", "finalizesize=0, consumeTime=" + consume +" name=" +Thread.currentThread().getName());
                runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        StringnewObjStr = txtResult.getText().toString();
                        txtResult.setText(newObjStr+ "\n\nGC "+ number +"objs,\nconsume:" + consume +" ms");
                        btnNew.setEnabled(true);
                        btnRelease.setEnabled(false);
                    }
                });
            }
        }
    }

3.          利用PhantomReferences(虚引用)和ReferenceQueue(引用队列)

newObject()方法中需要关注的是虚引用对象的创建,如下代码中注释:

   private void newObject(){
        txtResult.setText("");
        PFObjectobj = null;
        long startNewTime= System.currentTimeMillis();
        for (int i = 0;i < number;i ++) {
            obj= new PFObject(i);
            listBusiness.add(obj);
        }
        long consume =System.currentTimeMillis() - startNewTime;
        showResult(true, consume);
        for (int i = 0;i < number;i ++) {
            obj= listBusiness.get(i);
            // 将对象传入构造虚引用,并与引用队列关联
            PhantomReference<PFObject>phantomRef  = newPhantomReference<PFObject>(obj, refQueue);
            listGCLog.add(phantomRef);
        }
        Log.d("ANDROID_LAB", "newObject" + number);
    }

与WeakReferences一致,虚引用是否被添加到引用队列也需要去开启线程不断查询状态。当ReferenceQueue.poll()返回不为null时,表示有虚引用已经被添加到引用队列中了。这时可以执行listGLog.remove()清除对象的特征日志。最后调用showResult()显示时间。

private void releaseObject() {
    btnRelease.setEnabled(false);
    newThread() {
        publicvoid run() {
            startGCTime= System.currentTimeMillis();
            listBusiness.clear();
            //清除操作并告诉VM有一大坨对象可以吃啦..
            System.gc();
            intcount = 0;
            while(count!= number) {
                Reference<?extends PFObject> ref = (Reference<? extends PFObject>)refQueue.poll();
                if(ref != null) {
                    booleanbool = listGCLog.remove(ref);
                    //清除一下虚引用。
                    ref.clear();
                    count++;
                }
            }
            longconsume = System.currentTimeMillis() - startGCTime;
            Log.d("ANDROID_LAB","releaseObject() consume=" + consume);
            showResult(false,consume);
        }
    }.start();
}

[Java] 监控java对象回收的原理与实现,布布扣,bubuko.com

时间: 2024-10-08 03:41:36

[Java] 监控java对象回收的原理与实现的相关文章

java虚拟机的垃圾回收机制原理

1.常用的算法: a.引用计数法:为每一个对象配置一个整形计数器,当有一个引用时,计数器+1,引用失效时,计数器-1.计数器为0,进行垃圾回收 存在的问题:A对象引用B,B对象引用A.循环引用,无法清除,引起内存泄漏 java的垃圾回收器没有使用该算法 b.标记-清除算法 分标记阶段和清除阶段. 存在问题:释放后的空间不是连续的.内存分配不连续的空间,工作效率较低 c.复制算法 将内存空间分为两块,每次只使用其中一块,垃圾回收时,将正在使用的内存中存活的对象复制到未使用的内存中,然后,清除使用的

成为JavaGC专家(2)—如何监控Java垃圾回收机制

本文作者: ImportNew - 王晓杰 未经许可,禁止转载! 本文是成为Java GC专家系列文章的第二篇.在第一篇<深入浅出Java垃圾回收机制>中我们学习了不同GC算法的执行过程,GC是如何工作的,什么是新生代和老年代,你应该了解的JDK7中的5种GC类型,以及这5种类型对于应用性能的影响. 在本文中,我将解释JVM到底是如何执行垃圾回收处理的. 什么是GC监控? 垃圾回收收集监控指的是搞清楚JVM如何执行GC的过程,例如,我们可以查明: 1.        何时一个新生代中的对象被移

成为JavaGC专家(3)—如何监控Java垃圾回收机制(转载)

原文:http://www.importnew.com/3146.html 为什么需要优化GC 或者说的更确切一些,对于基于Java的服务,是否有必要优化GC?应该说,对于所有的基于Java的服务,并不总是需要进行GC优化,但前提是所运行的基于Java的系统,包含了如下参数或行为: 已经通过 -Xms 和–Xmx 设置了内存大小 包含了 -server 参数 系统中没有超时日志等错误日志 换句话说,如果你没有设定内存的大小,并且系统充斥着大量的超时日志时,你就需要在你的系统中进行GC优化了. 但

Java对象回收流程

一.可回收对象判断 引用计数器算法:为对象放置一个引用计数器,当对象被引用时则计数器加一,如果一个对象的计数器标识为零的时候,则表明该对象可被回收.这种方法比较简单,但无法解决对象之间互相引用的情况. GC Roots算法:该方法可以解决上面算法中的对象互相引用无法判断可回收状态的问题.顾名思义,GC Root指的是程序使用对象,总会有一个"入口",这里可以是栈里的本地变量引用,也可以是方法区中的类静态属性引用.常量引用.JNI的Java引用.当一个对象,如果所有的GC Root沿着对

Java中字符串对象

Java中字符串对象创建有两种形式,一种为字面量形式,如String str = "droid";,另一种就是使用new这种标准的构造对象的方法,如String str = new String("droid");,这两种方式我们在代码编写时都经常使用,尤其是字面量的方式.然而这两种实现其实存在着一些性能和内存占用的差别.这一切都是源于JVM为了减少字符串对象的重复创建,其维护了一个特殊的内存,这段内存被成为字符串常量池或者字符串字面量池. 工作原理 当代码中出现字

Java内存的原型及工作原理理解

一.java虚拟机内存原型 寄存器:我们在程序中无法控制. 栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中 堆:存放用new产生的数据 静态域:存放在对象中用static定义的静态成员 常量池:存放常量 非RAM存储:硬盘等永久存储空间. 二.常量池(constant pool) 常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据.除了包含代码中所定义的各种基本类型(如int.long等等)和对象型(如String及数组)的常量值(final)

Java虚拟机(一)结构原理与运行时数据区域

前言 本来计划要写Android内存优化的,觉得有必要在此之前介绍一下Java虚拟机的相关知识,Java虚拟机也并不是三言两语能够介绍完的,因此开了Java虚拟机系列,这一篇文章我们来学习Java虚拟机的结构原理与运行时数据区域. 1.Java虚拟机概述 Oracle官方定义的Java技术体系主要包括以下几个部分: Java程序设计语言 各种平台的Java虚拟机 Class文件格式 Java API类库 第三方Java类库 可以把Java程序设计语言.Java虚拟机和Java API类库这三部分

Java内存模式以及回收模式

1.Java内存模型 Java虚拟机在执行程序时把它管理的内存分为若干数据区域,这些数据区域分布情况如下图所示: 程序计数器:一块较小内存区域,指向当前所执行的字节码.如果线程正在执行一个Java方法,这个计数器记录正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,这个计算器值为空. Java虚拟机栈:线程私有的,其生命周期和线程一致,每个方法执行时都会创建一个栈帧用于存储局部变量表.操作数栈.动态链接.方法出口等信息. 本地方法栈:与虚拟机栈功能类似,只不过虚拟机栈为虚拟机执行J

java基础之 垃圾回收机制

1. 垃圾回收的意义 在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象:而在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾.JVM的一个系统级线程会自动释放该内存块.垃圾回收意味着程序不再需要的对象是"无用信息",这些信息将被丢弃.当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用.事实上,除了释放没用的对象,垃圾回收也可以清除内存记录碎片.由于创建对象和垃圾回收器释放丢弃对象所占的内存空间,