堆内存的详细结构以及java中内存溢出和排查方式
三种JVM
我们可以通过Java -version
来查看我们使用的虚拟机版本。
我们大部分使用的就是HotSpot虚拟机,还有两种分别是JRockit JVM
和IBM J9VM
Oracle JRockit JVM
是业界性能最高的 Java 虚拟机,现内置于 Oracle 融合中间件中。
与 HotSpot 类似,支持 服务器端、桌面应用 和 嵌入式系统,是 IBM 产品(如 WebSphere、AIX、z/OS)的默认 JVM2017 年 IBM 将 J9 开源并交由 Eclipse 基金会管理,更名为 Eclipse OpenJ9
我们学习的虚拟机大部分都是HotSpot虚拟机。
堆
堆(Heap)我们的虚拟机里面只有一块堆内存,同时我们可以对其进行调整大小。
类加载器读取了类文件之后,会把类,方法,常量,变量,保存我们创建对象的真实对象。
在我们的堆内存当中可以细分为三个区域:
-
新生区:存放新创建的对象(绝大多数对象在此区域被快速回收),高频 Minor GC 处理短期对象,复制算法高效无碎片。
-
养老区:存放长期存活的对象(如缓存、全局变量等),低频 Major GC 处理长期对象,标记-整理算法减少碎片。
-
永久存储区:存储 JVM 的元数据(类信息、方法、常量池等),Java 8 后被元空间取代,存储元数据,不再受堆大小限制。
特性 | 永久代(PermGen) | 元空间(Metaspace) |
---|---|---|
存储位置 | 堆内存中 | 本地内存(非堆) |
溢出错误 | java.lang.OutOfMemoryError: PermGen | java.lang.OutOfMemoryError: Metaspace |
大小限制 | 固定上限(需手动调整) | 默认无上限(受物理内存限制) |
调优参数 | -XX:PermSize / -XX:MaxPermSize | -XX:MetaspaceSize / -XX:MaxMetaspaceSize |
我们通过代码来进行内存的模拟,我们通过对字符串的无限增加,可以使堆内存被加爆,就会出现内存溢出OOM
package com.JvmTest.TestjVMDemo1;
import java.util.Random;
public class HeapTest {public static void main(String[] args) {String string = "HeapTest";while(true){string+=string+ new Random().nextInt(999999999)+new Random().nextInt(999999999);}}
}
新生区
-
类诞生和成长的地方,甚至死亡。
-
伊甸园区,所有的对象都是在伊甸园区产生的
-
幸存者区(0,1)
因为我们程序当中绝大部分都是临时对象,所以我们基本上很少见到OOM
永久区
永久区这个区域是常驻内存的,用来存放JDK自身携带的Class对象,interface元数据,存储的是java运行时的一些环境或类信息,这个区域不存在垃圾回收机制,当虚拟机关闭的时候才会进行内存的释放。
当启动类加载过多的第三方JAR包,或在Tomcat中部署过多应用时,加之持续生成动态反射类不断被加载,最终会导致内存耗尽,从而引发OOM问题。
上图当中元空间这片区域也叫做非堆,事实上这片区域里面的方法区是线程公用的,所以它是非堆,非堆并不是说不是堆,它还是堆,只是用来区分开,jdk8以上就把元空间永久替换了永久代,所以这片区域叫过,永久代,元空间,方法区。
-
方法区:是《Java 虚拟机规范》定义的逻辑区域,用于存类信息、常量、静态变量等,本身属于非堆范畴(区分于存储对象的堆),JDK 1.7 及之前,HotSpot 用 “永久代” 实现它;JDK 8+ 改用 “元空间” 实现,元空间是方法区的落地实现(物理存储)。
-
“非堆” 是区分于新生代、老年代组成的 “堆(Heap)” 的内存分类,包含方法区(元空间 / 永久代)、直接内存等。它属于 JVM 管理的内存,但不参与堆的垃圾回收分代流程,本质是 “JVM 内存的非堆存储区”,并非 “不是堆”,而是逻辑概念上与堆(存对象实例)区分。
元空间这一片区域我们逻辑上存在,内存不存在解释过程如下。
我们通过代码来查看虚拟机试图使用的最大内存和jvm初始化总内存
package com.JvmTest.TestjVMDemo1;
public class JvmDemo1 {public static void main(String[] args) {//返回虚拟机试图使用的最大内存long maxMemory = Runtime.getRuntime().maxMemory();//返回JVM的初始化总内存long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("maxMemory="+maxMemory+"字节\t"+(maxMemory/(double)1024/1024)+"MB");System.out.println("totalMemory"+totalMemory+"字节\t"+(totalMemory/(double)1024/1024)+"MB");
}
}
默认情况下默认分配的总内存是电脑内存的1/4,而初始化的内存为1/64
我们可以通过参数的调优来查看一下内存的分布,调优的参数为-Xms1024m -Xmx1024m -XX:+PrintGCDetails
-
-XX:+PrintGCDetails
启用 详细垃圾回收日志,打印每次 GC 的详细信息
注意!! jdk9+的需要通过配置文件的方式才能够出现,9以下的参数无需更改
我们将新生代和老年代的内存相加然后进行内存的换算,最后结果也就是我们的虚拟机初始化总内存。
我们出现OOM的情况的时候就需要我们首先尝试扩大内存看结果是否改变,如果依然报错就说明代码内部存在着问题,就需要我们分析内存查看问题
我们对于之前的一个OOM的例子来进行解析
如上图我们就可以看出,当所有的内存都满了之后,就会出现错误OOM。
很经典的面试问题,当出现OOM怎么进行排错。
-
内存快照分析工具,MAT,Jprofiler这两个都是java的插件。
作用:
-
分析Dump内存文件,快速定位内存泄漏。
-
获得堆中的对象
-
获得大的对象
-
我们需要下载Jprofiler下载地址:ej-technologies - JProfiler
下载之后需要在图中的地址配置文件位置。
下载之后需要配置一下idea的参数
-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
参数的意思是Xms8m
设置JVM初始堆内存大小-Xmx8m
最大堆内存文件,HeapDumpOnOutOfMemoryError
当出现OOM错误的时候dump这个错误。这个时候当我们有错误的时候,就能够在idea里面看到dump文件
package TestjVMDemo1;
import java.util.ArrayList;
public class Demo2 {byte[] bytes = new byte[1024*1024];//-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryErrorpublic static void main(String[] args) {ArrayList<Demo2> list = new ArrayList<>();int count = 0;try {while (true){list.add(new Demo2());count=count+1;}} catch (Error e) {System.out.println(e);}}
}
我们把文件打开就可以在大文件的错误里面看到我们运行的出现的问题。
我们通过下面的设置就可以看到我们的程序当中具体到哪一行出现了OOM的错误。