2015年07月13日
背景
众所周知,在Java中,成员方法内可以使用this来引用当前对象,使用起来特别方便。但是在JVM中方法是在方法区中,所有的类的对象都共用了一个方法区,那么JVM是怎么知道this是指向哪个对象的呢?
其实为了实现这一功能,Java的处理方式很简单,在编译时,为每个成员方法都默默的添加了一个参数this,当调用这个方法时,把当前对象以参数的形式传进去即可。
本文重点不在this是怎么来的,因此简单验证下:
一个简单的java类:
import java.util.Arrays;
public class Test {
public Test() {
}
public void hi() {
}
public static void staticHi() {
}
}
编译后,使用javap来查看class文件
~ javap -verbose Test.class
Classfile /Users/Johnny/Downloads/Test.class
Last modified 2015-7-13; size 280 bytes
MD5 checksum 5f8b96a983d277edb1974e472cf0b62a
Compiled from "Test.java"
public class Test
SourceFile: "Test.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()V
#2 = Class #13 // Test
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 hi
#9 = Utf8 staticHi
#10 = Utf8 SourceFile
#11 = Utf8 Test.java
#12 = NameAndType #4:#5 // "<init>":()V
#13 = Utf8 Test
#14 = Utf8 java/lang/Object
{
public Test();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
line 7: 4
public void hi();
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 11: 0
public static void staticHi();
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 15: 0
}
看Test方法和hi方法的args_size=1,源码中是0参数的,因此可以验证确实是偷偷的添加了一个参数,当然这里肯定是this参数了。
Memory Leak
回归正题,上面验证了成员方法中会默默的添加一个this参数,而参数其实是放在方法局部变量表中的。因此如果参数没有被明确赋值为null的话,那么这个参数就一直是以强引用的形态指向了该对象。在一般情况下,方法和对象的生命周期是一样的,也就是对象不存在了,方法也不会被调用,不会存在泄露。但是有些情况下,比如一些异步的操作,导致对象本来应该被回收掉时,方法还在被调用,因此这期间就会存在短暂的泄露,当然如果是耗时的方法,泄露会表现的比较明显。 下面是在有异步任务的情况下的泄露测试。
import java.lang.ref.WeakReference;
/**
* 测试隐藏的this引用带来的Memory Leak
*/
public class Test {
/**
* 主要用来标记test方法是否被调用,避免Test实例提前被gc
*/
public static boolean sHasRun = false;
private static class MyThread extends Thread {
protected WeakReference<Test> mTestRef;
protected boolean mTestLeak;
public MyThread(Test t, boolean testLeak) {
mTestRef = new WeakReference<Test>(t);
mTestLeak = testLeak;
}
@Override
public void run() {
if (mTestLeak) {
// 泄露点1:局部变量,如果执行的方法是耗时的,并且在同一个线程执行的话,那么在这个方法执行完成前,局部变量是不会被回收的。
Test t = mTestRef.get();
if (t != null) {
// 泄露点2:成员方法,因为在编译时,默认会给每个成员方法加上this参数,this指向了当前类的实例。
// 方法参数本身就是一个局部变量,因此类此泄露点1,必须等该方法执行完成后,这个局部变量的强引用才会清除。
t.testLeak();
}
} else {
// 解决泄露1:不保存局部变量
if (mTestRef.get() != null) {
// 解决泄露2:调用静态方法而不是成员方法
// 当然这里可能会发生NPE,因为可能在判空后test调用前Test实例被gc了,因此try catch下
try {
mTestRef.get().test();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
/**
* 模拟耗时方法
*/
private static void test() {
sHasRun = true;
System.out.println("time-consuming method start");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("time-consuming method end");
sHasRun = false;
}
void testLeak() {
test();
}
public void start(boolean testLeak) {
new MyThread(this, testLeak).start();
}
public static void main(String[] args) {
WeakReference<Test> testRef;
boolean testLeak = true;
{
Test t = new Test();
testRef = new WeakReference<Test>(t);
t.start(testLeak);
t = null;
}
while (testRef.get() != null) {
if (sHasRun) {
System.gc();
} else {
System.out.println("thread not run.");
}
}
System.out.println("Test instance has cleaned.");
}
}
当testLeak为true时,打印结果为
thread not run.
time-consuming method start
time-consuming method end
Test instance has cleaned.
当testLeak为false时,打印结果为
thread not run.
time-consuming method start
Test instance has cleaned.
time-consuming method end
从打印结果可以看出,在测试泄露时,必须等到耗时方法执行结束后,该对象才能被gc,改进后的测试,在耗时方法未执行结束前,对象就已经可以被gc了。
解决方案
- 为了解决隐藏的this参数这个问题,可以从根源上尽可能去除方法中this局部变量的存在,比如将方法改成static的。
- 对于局部变量,比如弱引用的调用时,不要用局部变量保存从弱引用中get出来的值等。