32

再说【不使用的对象应手动赋值为null】 | 变猴旅程 | 从人变猴的历程

 4 years ago
source link: https://blog.darkness463.top/2019/12/16/objects-set-null-again/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

再说【不使用的对象应手动赋值为null】

2019-12-16

280

前段时间发了一篇又见【不使用的对象应手动赋值为null】,之后又想了想,发现在变量会不会被回收这个问题上还是有一些地方说不清楚,于是再次研究了一下。

还是用上篇文章的例子,我们来看看字节码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// placeHolder不会被回收
public static void main(String[] args) {
if (true) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
}
System.gc();
}
// 字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: ldc #2 // int 67108864
2: newarray byte
4: astore_1
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: aload_1
9: arraylength
10: sipush 1024
13: idiv
14: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
17: invokestatic #5 // Method java/lang/System.gc:()V
20: return
LocalVariableTable:
Start Length Slot Name Signature
5 12 1 placeHolder [B
0 21 0 args [Ljava/lang/String;


// placeHolder会被回收
public static void main(String[] args) {
if (args.length == 0) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
}
System.gc();
}
// 字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: aload_0
1: arraylength
2: ifne 22
5: ldc #2 // int 67108864
7: newarray byte
9: astore_1
10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
13: aload_1
14: arraylength
15: sipush 1024
18: idiv
19: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
22: invokestatic #5 // Method java/lang/System.gc:()V
25: return
LocalVariableTable:
Start Length Slot Name Signature
10 12 1 placeHolder [B
0 26 0 args [Ljava/lang/String;

LocalVariableTable里虽然标明了placeHolder的作用范围,第一个例子是从偏移5到17,第二个例子是从10到22,但是终止的偏移都是invokestatic #5 // Method java/lang/System.gc:()V,如果是因为作用域的原因,那这两种情况下作用域应该是一样的才对。

另外如果真的是我之前想的作用域的原因,那下面这样的代码为什么也不会被回收掉呢?

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
if (args.length == 0) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
} else {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
}
System.gc();
}

这让我产生了怀疑?

解释执行与编译执行

我在测试上面两种代码的时候,都是通过IDE运行的,在这种情况下是通过解释执行的方式运行。
可以在运行时加上-Xcomp-Xjit:count=0的参数,让JVM强制只用编译执行的方式运行,加上参数后,会发现上面两种代码中的placeHolder都会被顺利回收。
即使是下面这样的代码,加上上述参数后placeHolder都会被回收。

1
2
3
4
5
public static void main(String[] args) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
System.gc();
}

其他回收或不回收的情况

在不添加JVM参数的情况下,除了上面提到的那些例子,以下几种代码placeHolder都不会被回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static void main(String[] args) {
{
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
}
System.gc();
}

public static void main(String[] args) {
do {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
} while(false);
System.gc();
}

public static void main(String[] args) {
do {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
} while(args.length != 0);
System.gc();
}

public static void main(String[] args) {
if (true) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
} else {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
}
System.gc();
}

而下面的例子则可以被回收:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static void main(String[] args) {
int i = 0;
if (i == 0) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
}
System.gc();
}

public static void main(String[] args) {
for (int i = 0; i == 0; i++) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
}
System.gc();
}

public static void main(String[] args) {
int i = 0;
while (i++ == 0) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
}
System.gc();
}

// 下面这种代码稍微有点不同
public static void main(String[] args) {
if (true) {
byte[] placeHolder = new byte[64 * 1024 * 1024];
System.out.println(placeHolder.length / 1024);
}
int i = 0;
System.gc();
}

这两种类型的代码有什么区别呢?
在第一类的代码中,placeHolder这个变量是一定会被初始化并赋值的。
而在第二类代码中,虽然在目前的参数或代码下placeHolder会被初始化赋值,但存在placeHolder没有初始化的情况或分支。不过最后一种代码不太一样,最后一种是因为局部变量表slot复用导致的。

根据上面我看到的现象,在此我作出大胆猜测:

  • JIT及时编译后的代码做了大量优化,并且推测某个变量是否还可用的方式也与解释执行时有差别。会让不再使用了的变量都被顺利回收掉。
  • 如果到达System.gc()时,变量存在没有被初始化的路径,那就表明这个变量是可以被回收的,JVM可能会根据这个来判断变量是否可以被回收。
  • 当slot被其他变量复用,因为不再持有对原有变量的引用,因此也可以被回收。

以上猜测还需要之后深入研究下JVM的代码来验证,不过有一点可以肯定的是,我们没必要在所有变量使用完后都设置为null,毕竟绝大部分情况下运行的都是JIT编译之后的代码,即便手动设置为null,我猜也是会在及时编译时被优化掉的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK