转载

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

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

疑问

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

// 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 ,如果是因为作用域的原因,那这两种情况下作用域应该是一样的才对。

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

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 都会被回收。

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

其他回收或不回收的情况

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

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();
}

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

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复用导致的。

结论

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

System.gc()

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

原文  https://blog.darkness463.top/2019/12/16/objects-set-null-again/
正文到此结束
Loading...