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

常见代码场景

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

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

ini 复制代码
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加一个作用域,如:

ini 复制代码
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加大括号这部分代码抽离出一个方法。方法作为栈帧的存在,也是使用后即会出栈,该出栈后的栈帧中的变量也可以被回收。

所以优雅的写法应该是:
ini 复制代码
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也不会给它优化掉,它仍然会占用内存。所以这里又可以有优化,将无用变量和代码及时清理。

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

本篇文章,讲解的这个方法只是简单举了个例子,实际肯定不会有这么简单的代码,实际开发过程中,要注重方法的抽离。多抽离短小的方法不是什么坏事,另外无用代码的清理也要时刻进行,可以利用混淆来替代这一步。
相关推荐
qq_12498707536 分钟前
基于Java Web的城市花园小区维修管理系统的设计与实现(源码+论文+部署+安装)
java·开发语言·前端·spring boot·spring·毕业设计·计算机毕业设计
h7ml14 分钟前
查券返利机器人的OCR识别集成:Java Tesseract+OpenCV优化图片验证码的自动解析方案
java·机器人·ocr
野犬寒鸦16 分钟前
从零起步学习并发编程 || 第五章:悲观锁与乐观锁的思想与实现及实战应用与问题
java·服务器·数据库·学习·语言模型
Volunteer Technology20 分钟前
Sentinel的限流算法
java·python·算法
岁岁种桃花儿21 分钟前
SpringCloud从入门到上天:Nacos做微服务注册中心
java·spring cloud·微服务
jdyzzy22 分钟前
什么是 JIT 精益生产模式?它与传统的生产管控方式有何不同?
java·大数据·人工智能·jit
Chasmれ27 分钟前
Spring Boot 1.x(基于Spring 4)中使用Java 8实现Token
java·spring boot·spring
汤姆yu28 分钟前
2026基于springboot的在线招聘系统
java·spring boot·后端
darling3311 小时前
mysql 自动备份以及远程传输脚本,异地备份
android·数据库·mysql·adb
计算机学姐1 小时前
基于SpringBoot的校园社团管理系统
java·vue.js·spring boot·后端·spring·信息可视化·推荐算法