【GC】真实代码层面来分析内存优化(场景1)
常见代码场景
存在下边两个方法,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中的内存分步情况:
- JVM结构中,每个线程会起一个栈,而线程中运行的每个方法都会起一个栈帧存放到栈中。
- 运行outMethod时候,会为outMethod起一个栈帧放入到栈中,栈帧中包含局部变量b,bigArray引用,unused和ss引用以及当前对象EQ的引用this,这5个引用存在于局部变量表中,局部变量表深度为5
- bigArray数组实例和ss数组实例存在于java堆中。b和unused由于是常量,其值存在于类文件的常量池中,类文件存在于元空间中。
- 当运行到innerMethod时候,会又新建一个栈帧,存放到栈中,该栈帧在outMethod栈帧的上方,innerMethod的栈帧中存在两个变量,this引用和s引用。局部变量表深度为2,
- 其中s也是指向堆中的数组实例{"1","2"}
上述分析基于的是JVM的内存结构,接下里回忆一下GC相关的基本知识点:
GC root包含哪些呢?
- 当前正在运行的栈帧
- 元空间中被引用的静态变量和对象
- 元空间中,常量引用的对象
- 本地方法中,JNI引用的对象
- Exception相关对象
- 类加载器
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,发现这些变量没有向上的根在引用,直接回收了。
我们从这个方法的字节码来进一步验证下:
- 没加大括号的字节码
- 加了大括号后的字节码
对比是否加大括号的字节码可以看到,当没加大括号时,每一个变量都会往变量槽存储时候,store后的位置会加1,对于没加大括号的,bigArray对应变量槽的位置是2,unused对应变量槽的位置是3,局部变量表槽大小是5。如图:
对于加了大括号,可以看到,对于变量bigArray对应的变量槽为2,unused对应槽也是2,说明运行到unused时候,复用了bigArray的变量槽,此时bigArray的引用被覆盖消失,java堆中的实例没有被root引用,从而可以被GC回收,此时局部变量表深度是4.
所以对于这种大数据占用内存的情况,我们最好定义其作用域,限制其生命周期,这样可以及时让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,我们可以看到,实际上这个变量并没有被用到,但是我们没有删除,放到了这里。可能我们大部分时候不会在意这种无用代码的清理,觉得放在这里,万一以后有用。
我们看下字节码:
会看到,即使这个变量无用,JVM也不会给它优化掉,它仍然会占用内存。所以这里又可以有优化,将无用变量和代码及时清理。
注意的是:我们代码可以混淆,混淆一般可以帮助我们去掉无用的代码,不用我们手动清理,所以混淆也可以减少内存占用,提升性能。
本篇文章,讲解的这个方法只是简单举了个例子,实际肯定不会有这么简单的代码,实际开发过程中,要注重方法的抽离。多抽离短小的方法不是什么坏事,另外无用代码的清理也要时刻进行,可以利用混淆来替代这一步。
转载自:https://juejin.cn/post/7278509192507965474