监控Java对象回收的原理与实现
一.监控Java对象回收的目的
监控Java对象是否回收的目的是:为了实现内存泄露报警。
内存泄露是指程序中对象生命周期(点击查看详情)已经进入不可见阶段,但因为编码错误或系统原因,仍然存在着GC roots持有或间接持有该对象的引用,导致该对象的生命周期无法继续向下流转,也就无法释放的现象。
简单的来说即是:已实例化的对象长期被持有且无法释放或不能依照对象正常的生命周期进行释放。
实现内存泄露报警。能够发现并解决程序上编码的错误,降低应用的内存使用,降低应用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”button,将会生成指定数量的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();
}