透过OutOfMemoryError看GC

本文通过一个具体的Java程序示例,探讨了Java内存管理中对象可达性和垃圾回收机制的细节。通过对字节码的分析,解释了为何在特定条件下加入循环能够避免出现内存溢出错误。

 

javaspecialists173期 出了一道很有意思的题目,例子很直白,行为很诡异,原因很简单.这是关于一个OOM的问题,涉及了GC, 对象的可触及, 作用域等等,官方答案可以在174期 找到,这个解释其实还是有点抽象,这里我加上点图片解释下。(以下代码摘录于javaspecialists173期 )

 

先看看程序做了什么, 先分配当前可用堆栈的60%,然后再分配60%,抛OOM错误是合情合理的,因为堆内存不够用了.但是为什么程序加上了一个System.out.print的循环调用就能正常运行了? 直观的想法是GC这种情况下起作用了,那他又是如何起作用的呢?

我们先来回顾一下Java 内存模型的一些基本原理:

  1. GC触发的时机:我们知道Java虚拟机会有一个线程在后台等待,帮我们释放不再需要的内存(即所谓托管式运行时)。对于堆中不可触及的对象,GC在适当的时候会被唤醒,整理堆空间,将这些不可触及的对象清空。当JVM无法从堆中分配所需的连续 内存时(如果零碎内存总量足够,但是不连续也有可能触发GC,这时GC需要进行碎片整理,从而分配连续的大内存),GC就会被触发,释放内存,如果内存还是不够,OOM即被抛出。
  2. 不可触及:所有引用都有一“起点”,从这些起点开始能传递的被强引用的对象都是可触及对象,也就是不能被GC释放的对象(所谓“垃圾对象”就是不再被使用的对象,我们总不会扔掉还能使用的东西吧?)。这些“起点”包括:所有方法栈中的帧上的引用(包括局部变量和操作数栈上的引用);已加载类的引用(注:已被解析过的才算);JNI的局部和全局引用,如果通过这都无法找到的对象,就是所谓的不可触及对象。
  3. 局部变量:Java方法调用是基于栈的,虚拟机调用方法时,会创建一个方法帧,并为这个帧分配一个元素大小为字长的数组,用于存放局部变量(字长足够存放所有引用,而primitive则直接贮存),他也是"起点"的一部分。

再回过头来看看我们的例子,没有注释掉for循环时,程序正常运行,这意味着上述代码成功的触发了GC(在第二次分配60%内存时,堆内存不够了), 并且GC成功的释放了第一次分配的内存,即byte [] data被成功释放了。好,问题归结到了为什么没了这个for循环, byte [] data就无法被释放了?

光从Java代码上我们是看不出什么了,透过现象看本质,这时候字节码就发挥功效了.让我们用JDK提供的javap工具看看这两个f()函数有什么不同!

 

  • 正常运行(有for循环)

解释下这里的字节码

4: newarray_byte  // 分配byte [] data

6: astore_1 // 将data引用写到局部变量数组的1位置 (0位置放什么?this!)

7: iconst_0 // 常量0压栈,用来初始化局部变量i

8: istore_1 // 将7压栈的常量0写入局部变量数组1位置

(而后面的几行其实可以忽略,即System.out不起任何作用)

 

通过方法栈中f()方法帧的快照能更容易理解这个过程。

 

第6行和第8行重复使用了局部变量的1位置,8将6的引用覆盖了,第33行将在堆中分配byte [] data2,堆内存不够,GC被触发并释放了byte [] data (因为这个时候byte [] data已经不可触及了!),程序正常运行。

 

  • 运行失败

 

再看看把for循环注释掉后的字节码,第0~6行同上例一样,分配了byte [] data并放在局部变量1位置,第11行分配byte [] data2,这是个时候堆内存不够,于是GC被触发。但是同时GC又发现byte [] data仍在使用(localvariable[1]依旧指向他),GC无法释放他又无法分配更多的内存,于是OOM被抛出,程序被终止。

 

### 原因分析 `java.lang.OutOfMemoryError: GC overhead limit exceeded` 是 JVM 在垃圾回收(GC)上花费了过多时间(超过 98% 的 CPU 时间)却只能回收很少的内存(少于 2% 的堆空间)时抛出的错误。这种错误通常表明应用存在内存泄漏、堆内存不足、处理大数据集或不当的数据结构选择等问题 [^2]。 ### 解决方案 #### 1. 增加堆内存 可以通过增加 JVM 的最大堆大小(`-Xmx`)和设置初始堆大小(`-Xms`)来解决内存不足的问题。例如: ```bash java -Xmx1024m -Xms512m -XX:+UseG1GC YourApplication ``` 考虑使用 G1 垃圾收集器(`-XX:+UseG1GC`)来优化垃圾回收性能 [^2]。 #### 2. 优化代码 - **检查内存泄漏**:特别是静态集合、缓存等。 - **及时关闭资源**:如数据库连接、文件流等。 - **避免在循环中创建大量临时对象**。 - **使用更高效的数据结构** 。 #### 3. 分析内存使用 使用工具分析内存转储: ```bash jmap -dump:format=b,file=heap.hprof <pid> ``` 然后使用 MAT(Eclipse Memory Analyzer)或 VisualVM 分析。使用 JConsole 或 VisualVM 实时监控 [^2]。 #### 4. 调整 GC 策略 根据应用特点选择合适的 GC 算法,例如: ```bash -XX:+UseConcMarkSweepGC -XX:+UseG1GC -XX:+ExplicitGCInvokesConcurrent ``` #### 5. 其他配置 - `-XX:-UseGCOverheadLimit`(不推荐,这只是绕过错误而非解决问题) 。 ### 预防措施 - 实施合理的内存监控。 - 进行定期的性能测试和负载测试。 - 代码审查时关注内存使用模式。 - 对于缓存实现设置大小限制和过期策略 [^2]。 ### IDEA 启动项目或 Maven 构建时的解决方案 如果是在 IDEA 启动项目或 Maven 构建时遇到此问题,可以增加 IDEA 的启动进程堆内存值: 1. 打开 **Settings**。 2. 导航到 **Build, Execution, Deployment** -> **Compiler**。 3. 设置 **Build process heap size** 为更大的值,例如 2000 MB [^3]。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值