likes
comments
collection
share

【GC】真实代码层面来分析内存优化(场景1)

作者站长头像
站长
· 阅读数 25

常见代码场景

    存在下边两个方法,outMethod和innerMethod,其中outMethod方法中调用了innerMethod。

    其中bigArray特别大,占用内存10M,我们运行这个方法,会发现方法运行结束前内存一直大于10M,很容易产生溢出,那么如何优化呢?

public class EQ {

    public void outMethod() {
        int b = 2 + 3;
        System.out.println(b);
        byte[] bigArray = new byte[1024 * 1024 * 10];
        System.out.println(bigArray.length);
        int unused = 3 + 4;
        String[] ss = new String[]{"1", "2"};
        innerMethod(ss);
    }

    public void innerMethod(String[] s) {
        System.out.println(s[0]);
    }
}

先来分析下这个类中这两个方法在JVM中的内存分步情况:

  1. JVM结构中,每个线程会起一个栈,而线程中运行的每个方法都会起一个栈帧存放到栈中。
  2. 运行outMethod时候,会为outMethod起一个栈帧放入到栈中,栈帧中包含局部变量b,bigArray引用,unused和ss引用以及当前对象EQ的引用this,这5个引用存在于局部变量表中,局部变量表深度为5
  3. bigArray数组实例和ss数组实例存在于java堆中。b和unused由于是常量,其值存在于类文件的常量池中,类文件存在于元空间中。
  4. 当运行到innerMethod时候,会又新建一个栈帧,存放到栈中,该栈帧在outMethod栈帧的上方,innerMethod的栈帧中存在两个变量,this引用和s引用。局部变量表深度为2,
  5. 其中s也是指向堆中的数组实例{"1","2"}

上述分析基于的是JVM的内存结构,接下里回忆一下GC相关的基本知识点:

GC root包含哪些呢?

  1. 当前正在运行的栈帧
  2. 元空间中被引用的静态变量和对象
  3. 元空间中,常量引用的对象
  4. 本地方法中,JNI引用的对象
  5. Exception相关对象
  6. 类加载器

JVM GC回收流程

    GC处理器会从根ROOT开始,遍历各个对象,对仍在使用的对象打上标记,然后清除掉内存中没有被标记的区域。

现在我们知道了内存中的分步情况,那么接下来分析下可以优化的点:

对于代码中bigArray的优化

    最好的回收时机就是用完就可以立即回收。而该变量定义在outMethod的栈帧中的局部变量表中,只有方法outMethod执行完后,局部变量表才可以别回收。

    但是JVM结构有一个知识点,JVM的变量槽是可以复用的,复用的原理就是:当一个变量生命周期在其作用域外,那么作用域外的变量便可以复用其变量槽。所以,这里我们可以给bigArray加一个作用域,如:

public void outMethod() {
    int b = 2 + 3;
    System.out.println(b);
    {
        byte[] bigArray = new byte[1024 * 1024 * 10];
        System.out.println(bigArray.length);
    }
    int unused = 3 + 4;
    String[] ss = new String[]{"1", "2"};
    innerMethod(ss);
}
讲解下为啥这样修改:

    当运行到 int unused = 3 + 4; 的时候,已经处于bigArray作用域外了,之后bigArray的变量槽就可以被复用了,此时unused就直接复用了bigArray的变量槽,而bigArray数组在java堆中的实例对象没有了引用,GC回收时候就可以通过遍历根ROOT,发现这些变量没有向上的根在引用,直接回收了。

我们从这个方法的字节码来进一步验证下:
  • 没加大括号的字节码

【GC】真实代码层面来分析内存优化(场景1)

  • 加了大括号后的字节码

【GC】真实代码层面来分析内存优化(场景1)

    对比是否加大括号的字节码可以看到,当没加大括号时,每一个变量都会往变量槽存储时候,store后的位置会加1,对于没加大括号的,bigArray对应变量槽的位置是2,unused对应变量槽的位置是3,局部变量表槽大小是5。如图:

【GC】真实代码层面来分析内存优化(场景1)

    对于加了大括号,可以看到,对于变量bigArray对应的变量槽为2,unused对应槽也是2,说明运行到unused时候,复用了bigArray的变量槽,此时bigArray的引用被覆盖消失,java堆中的实例没有被root引用,从而可以被GC回收,此时局部变量表深度是4.

【GC】真实代码层面来分析内存优化(场景1)

所以对于这种大数据占用内存的情况,我们最好定义其作用域,限制其生命周期,这样可以及时让GC回收内存。
该场景最优优化方案

    类似上述方法代码,在代码中直接加个大括号,其实写法不是很优雅,除了一些特殊情况,比如为了表示出流程思路可以这样写。其他情况还是建议直接将bigArray加大括号这部分代码抽离出一个方法。方法作为栈帧的存在,也是使用后即会出栈,该出栈后的栈帧中的变量也可以被回收。

所以优雅的写法应该是:
public void outMethod() {
        int b = 2 + 3;
        System.out.println(b);
        logBigArray();
        int unused = 3 + 4;
        String[] ss = new String[]{"1", "2"};
        innerMethod(ss);
    }

    private void logBigArray(){
        byte[] bigArray = new byte[1024 * 1024 * 10];
        System.out.println(bigArray.length);
    }
我们接着分析这个方法中的代码

    对于unused,我们可以看到,实际上这个变量并没有被用到,但是我们没有删除,放到了这里。可能我们大部分时候不会在意这种无用代码的清理,觉得放在这里,万一以后有用。

    我们看下字节码:

【GC】真实代码层面来分析内存优化(场景1)

    会看到,即使这个变量无用,JVM也不会给它优化掉,它仍然会占用内存。所以这里又可以有优化,将无用变量和代码及时清理。

注意的是:我们代码可以混淆,混淆一般可以帮助我们去掉无用的代码,不用我们手动清理,所以混淆也可以减少内存占用,提升性能。

本篇文章,讲解的这个方法只是简单举了个例子,实际肯定不会有这么简单的代码,实际开发过程中,要注重方法的抽离。多抽离短小的方法不是什么坏事,另外无用代码的清理也要时刻进行,可以利用混淆来替代这一步。