【JVM原理】内存溢出分析

ゝ一纸荒年。 2022-05-21 12:05 367阅读 0赞

前言

Github:https://github.com/yihonglei/jdk-source-code-reading(java-jvm)

JVM内存结构

JVM类加载机制

JVM内存溢出分析

HotSpot对象创建、内存、访问

JVM垃圾回收机制(1)—如何判定对象可以回收

JVM垃圾回收机制(2)—垃圾收集算法

JVM垃圾回收机制(3)—垃圾收集器

JVM垃圾回收机制(4)—内存分配和回收策略

一 内存溢出概述

在Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时数据

区域都有发生内存溢出异常(OutOfMemoryError,简称OOM)的可能。

内存溢出就是在申请内存的时候,没有足够的内存,这个时候就会抛出内存溢出异常。

内存溢出和内存泄漏的区别:

内存泄漏是由于使用不当,把一部分内存“丢掉了”,导致这部分内存不可用。

当在堆中创建了对象,后来没有使用这个对象了,又没有把整个对象的相关引用设为 null。

此时垃圾收集器会认为这个对象是需要的,就不会清理这部分内存。

这就会导致这部分内存不可用。所以内存泄漏会导致可用的内存减少,进而会导致内存溢出。

二 运行时数据区

运行时数据区除了程序计数器外,还包括Java堆、虚拟机栈和本地方法栈、方法区。

关于jvm内存模型可以参考: JVM内存模型

除了运行时数据区会出现内存溢出外,还有一个本地直接内存也会发生内存溢出。

在对每个区域内存溢出分析前,需要先认识几个以下分析中会遇到的 JVM 参数:

-XX:+HeapDumpOnOutofMemoryError dump 的时候转储堆快照

-Xms 堆最小容量(heap min size)

-Xmx 堆最大容量(heap max size)

-Xss 栈容量(stack size)

-XX:PermSize=size 永生代最小容量

-XX:MaxPermSize=size 永生代最大容量

三 内存溢出实例分析

注意: 以下代码实例执行前,需要先配置好相应的 JVM 参数在运行程序!

1、Java 堆溢出

Java 堆用于存储对象的实例,只要不断地创建对象,并保证 GC Roots 到对象之间有

可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后

就会产生内存溢出异常。

  1. package com.lanhuigu.jvm.oom;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. /**
  5. * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
  6. * 描述: -XX:+HeapDumpOnOutOfMemoryError 生成hprof文件,该文件在项目目录下
  7. */
  8. public class HeapOOM {
  9. static class OOMObject {
  10. }
  11. public static void main(String[] args) {
  12. List<OOMObject> list = new ArrayList<>();
  13. while (true) {
  14. // 疯狂创建对象
  15. list.add(new OOMObject());
  16. }
  17. }
  18. }

运行结果:

70

代码分析:

该段代码将 Java 堆的大小限制为 20MB,不可以扩展(将堆的最小值 -Xms 参数与最大值 -Xmx

参数设置为一样避免堆自动扩展),通过 -XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机

在出现内存溢出异常时 Dump 出当前的内存堆转储快照用于分析。

这段代码疯狂的创建对象,虽然对象没有声明变量名引用,但是将对象添加到队列 list 中,

这样队列l就持有了一份对象的引用通过可达性算法(jvm 判断对象是否可被收集的算法)分析,

队列 list 作为 GC Root,每一个对象都是list的一个可达的节点,所以疯狂创建的对象不会

被收集,这就是内存泄漏,之后就会导致内存溢出。

程序运行完后,可以看到 java_pid1216.hprof 文件生成了。然后通过 visualVM 工具进行

快照分析。visualVM 是 jdk 自带的可视化监视工具 visualVM,位置在 jdk 安装目录下的

bin 目录中,双击直接运行即可。

分析依据,如果发生内存泄漏:

1)找出泄漏的对象

2)找到泄漏对象的 GC Root

3)根据泄漏对象和 GC Root 找到导致内存泄漏的代码

4)想法设法解除泄漏对象与 GCRoot 的连接

如果不存在泄漏:

1)检查虚拟机堆参数,与物理集群内存对比,看下是否能增大jvm堆的最大容量。

2)检查代码是否存在某些对象生命周期过长、持有状态时间过长的情况,

  1. 优化程序,尝试减少程序运行期的内存消耗,减小对象的生命周期。

通过工具打开 hprof 文件:

70 1

从概述中可以看到异常原因,以及异常导致的线程 main,点击 main 链接进入:

70 2

异常对象详情(实例数据):

70 3

2、虚拟机栈和本地方法栈溢出

调用方法的时候,会在栈中入栈一个栈帧,如果当前栈的容量不足,

就会发生栈溢出 StackOverFlowError 那么只要疯狂的调用方法,并且有意的不让

栈帧出栈就可以导致栈溢出了。

  1. package com.lanhuigu.jvm.oom;
  2. /**
  3. * VM Args: -Xss200k
  4. */
  5. public class JavaVMStackSOF {
  6. private int stackLength = 1;
  7. public void stackLeak() {
  8. stackLength++;
  9. stackLeak();
  10. }
  11. public static void main(String[] args) {
  12. JavaVMStackSOF oom = new JavaVMStackSOF();
  13. try {
  14. oom.stackLeak();
  15. } catch (Throwable e) {
  16. System.out.println("stack length:" + oom.stackLength);
  17. throw e;
  18. }
  19. }
  20. }

运行结果:

70 4

代码分析:

jvm 设置参数 -Xss200k,目的是缩小栈的空间,这样栈溢出“来的快一点”

程序中用了递归,让栈帧疯狂的入栈,又不让栈帧出栈,这样就会栈溢出了。

3、运行时常量池溢出

这里储存的是一些常量、字面量。如果运行时常量池内存不足,就会发生内存溢出。

从 jdk1.7 开始,运行时常量池移动到了堆中,所以如果堆的内存不足,也会导致运行

时常量池内存溢出。

基于jdk8的代码:

  1. package com.lanhuigu.jvm.oom;
  2. import java.util.LinkedList;
  3. /**
  4. * VM Args:
  5. * jdk6以前:-XX:PermSize=10M -XX:MaxPermSize=10M
  6. * jdk7开始:-Xms10m -Xmx10m -XX:-UseGCOverheadLimit
  7. */
  8. public class RuntimePoolOOM {
  9. public static void main(String[] args){
  10. // 使用list保持常量的引用,避免Full GC回收常量池
  11. LinkedList<String> list=new LinkedList<>();
  12. // 疯狂添加常量到list
  13. int i=1;
  14. while (true) {
  15. list.add(String.valueOf(i++).intern());
  16. }
  17. }
  18. }

运行结果:

70 5

代码分析:

参数:-Xms10m -Xmx10m 固定堆的大小为10MB,-XX:-UseGCOverheadLimit

是关闭 GC 占用时间过长时会报的异常。

因为 jdk6 以前,运行时常量池是在方法区(永生代)中的,所以要限制永生代的容量,

让内存溢出来的更快。从 jdk7 开始,运行时常量池是在堆中的,那么固定堆的容量就好了。

这里用了链表去保存常量的引用,是因为防止被 fullgc 清理,因为 Full gc 会清理掉方法区

和老年代 intern() 方法是将常量添加到常量池中去,这样运行时常量池一直都在增长,

然后内存溢出。

4、方法区溢出

方法区用于存放 Class 的相关信息,如类名、访问修改时符、字段描述、方法描述等。

对这些区域的测试,基本的思想是运行时产生大量的类去填满方法区、直到内存溢出。

这里使用 Cglib 创建大量的代理类。

  1. package com.lanhuigu.spring.proxy;
  2. import net.sf.cglib.proxy.Enhancer;
  3. import net.sf.cglib.proxy.MethodInterceptor;
  4. import net.sf.cglib.proxy.MethodProxy;
  5. import java.lang.reflect.Method;
  6. /**
  7. * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
  8. */
  9. public class JavaMethodAreaOOM {
  10. public static void main(String[] args) {
  11. while (true) {
  12. Enhancer enhancer = new Enhancer();
  13. enhancer.setSuperclass(OOMObject.class);
  14. enhancer.setUseCache(false);
  15. enhancer.setCallback(new MethodInterceptor() {
  16. @Override
  17. public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
  18. return proxy.invokeSuper(obj, args);
  19. }
  20. });
  21. enhancer.create();
  22. }
  23. }
  24. static class OOMObject {
  25. }
  26. }

关于 java7 之前,java7 和 java8 方法区与堆的说明:

java7 之前:

方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以

设置一个固定值,不可变;

java7 中:

存储在永久代的部分数据就已经转移到 Java Heap 或者 Native memory。

但永久代仍存在于 JDK 1.7 中,并没有完全移除,譬如符号引用(Symbols)转移到了

native memory ;

字符串常量池(interned strings)转移到了Java heap;

类的静态变量(class statics)转移到了Java heap。

java8 中:

取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享

物理内存,逻辑上可认为在堆中 。

Native memory:

本地内存,也称为 C-Heap,是供 JVM 自身进程使用的。当 Java Heap 空间不足时会触发 GC,

但 Native memory 空间不够却不会触发 GC。

5、本机直接内存溢出

DirectMemory 容量可以通过 XX:MaxDirectMemorySize 指定,如果不指定,

则默认与 Java 堆最大( -Xmx 指定)值一样。

  1. package com.lanhuigu.jvm.oom;
  2. import sun.misc.Unsafe;
  3. import java.lang.reflect.Field;
  4. /**
  5. * 本机直接内存溢出,该类执行完ide都崩掉了,小心!!!
  6. * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
  7. */
  8. public class DirectMemoryOOM {
  9. private static final int _1MB = 1024 * 1024;
  10. public static void main(String[] args) throws IllegalAccessException {
  11. Field unsafeField = Unsafe.class.getDeclaredFields()[0];
  12. unsafeField.setAccessible(true);
  13. Unsafe unsafe = (Unsafe) unsafeField.get(null);
  14. while (true) {
  15. unsafe.allocateMemory(_1MB);
  16. }
  17. }
  18. }

参考文献

《深入理解Java虚拟机》 (第二版) 周志明 著;

发表评论

表情:
评论列表 (有 0 条评论,367人围观)

还没有评论,来说两句吧...

相关阅读

    相关 JVM内存溢出问题案例分析

    Java虚拟机(JVM)内存溢出,通常是指程序在运行过程中,试图申请的内存空间超过了可用的最大限制。 下面是一些关于JVM内存溢出问题案例的分析: 1. **数组过长**: