Java中隱藏的this變數和區域性變數可能引發的記憶體洩露問題
阿新 • • 發佈:2019-02-08
背景
眾所周知,在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出來的值等。