监控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