前言
Java在内存管理方面是要比C/C++更方便的,不需要为每一个对象编写释放内存的代码,JVM虚拟机将为我们选择合适的时间释放内存空间,使得程序不容易出现内存泄漏和溢出的问题
不过,也正是因为Java把内存控制的权利交给了Java虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎么使用内存的,那排查错误将会成为一项异常艰难的工作
下面先看看JVM如何管理内存的
内存管理
根据Java虚拟机规范(第3版) 的规定,Java虚拟机所管理的内存将会包括以下几个运行内存数据区域:
- 线程隔离数据区:
- 程序计数器: 当前线程所执行字节码的行号指示器
- 虚拟机栈: 里面的元素叫栈帧,存储局部变量表、操作栈、动态链接、方法出口等,方法被调用到执行完成的过程对应一个栈帧在虚拟机栈中入栈到出栈的过程。
- 本地方法栈: 和虚拟机栈的区别在于虚拟机栈为虚拟机执行Java方法,本地方法栈为虚拟机使用到的本地Native方法服务。
- 线程共享数据区:
- 方法区: 可以描述为堆的一个逻辑部分,或者说使用永久代来实现方法区。存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 堆: 唯一目的就是存放对象的实例,是垃圾回收管理器的主要区域,分为Eden、From/To Survivor空间。
Java各版本内存管理改进
下图中永久代理解为堆的逻辑区域,移除永久代的工作从JDK7就已经开始了,部分永久代中的数据(常量池)在JDK7中就已经转移到了堆中,JDK8中直接去除了永久代,方法区中的数据大部分被移到堆里面,还剩下一些元数据被保存在元空间里
内存溢出
- 内存泄露Memory Leak: 申请的内存空间没有及时释放,导致后续程序里这块内容永远被占用。
- 内存溢出Out Of Memory: 要求的内存超过了系统所能提供的
运行时数据区域的常见异常
在JVM中,除了程序计数器外,虚拟机内存的其他几个运行时数据区域都有发生OOM异常的可能。
堆内存溢出
不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象。
- public class HeapOOM {
- static class ObjectInHeap{
- }
- public static void main(String[] args) {
- List<ObjectInHeap> list = new ArrayList();
- while (true) {
- list.add(new ObjectInHeap());
- }
- }
- }
栈溢出
单个线程下不断扩大栈的深度引起栈溢出。
- public class StackSOF {
- private int stackLength = 1;
- public void stackLeak() {
- stackLength++;
- stackLeak();
- }
- public static void main(String[] args) {
- StackSOF sof = new StackSOF();
- try {
- sof.stackLeak();
- } catch (Throwable e) {
- System.out.println("Stack Length: " + sof.stackLength);
- throw e;
- }
- }
- }
循环的创建线程,达到最大栈容量。
- public class StackOOM {
- private void dontStop() {
- while (true) {
- }
- }
- public void stackLeadByThread() {
- while (true) {
- Thread thread = new Thread(new Runnable() {
- @Override
- public void run() {
- dontStop();
- }
- });
- thread.start();
- }
- }
- public static void main(String[] args) {
- StackOOM stackOOM = new StackOOM();
- stackOOM.stackLeadByThread();
- }
- }
运行时常量池溢出
不断的在常量池中新建String,并且保持引用不释放。
- public class RuntimeConstantPoolOOM {
- public static void main(String[] args) {
- // 使用List保持着常量池的引用,避免Full GC回收常量池
- List<String> list = new ArrayList<String>();
- int i = 0;
- while (true) {
- // intern()方法使String放入常量池
- list.add(String.valueOf(i++).intern());
- }
- }
- }
方法区溢出
借助CGLib直接操作字节码运行时产生大量的动态类,最终撑爆内存导致方法区溢出。
- public class MethodAreaOOM {
- static class ObjectInMethod {
- }
- public static void main(final String[] args) {
- // 借助CGLib实现
- while (true) {
- Enhancer enhancer = new Enhancer();
- enhancer.setSuperclass(ObjectInMethod.class);
- enhancer.setUseCache(false);
- enhancer.setCallback(new MethodInterceptor() {
- @Override
- public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
- return methodProxy.invokeSuper(o, objects);
- }
- });
- enhancer.create();
- }
- }
- }
元空间溢出
助CG Lib运行时产生大量动态类,唯一的区别在于运行环境修改为Java 1.8,设置-XX:MaxMetaspaceSize参数,便可以收获java.lang.OutOfMemoryError: Metaspace这一报错
本机直接内存溢出
直接申请分配内存(实际上并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是抛出异常)
- public class DirectMemoryOOM {
- private static final int _1MB = 1024 * 1024;
- public static void main(String[] args) throws IllegalAccessException {
- Field unsafeField = Unsafe.class.getDeclaredFields()[0];
- unsafeField.setAccessible(true);
- Unsafe unsafe = (Unsafe) unsafeField.get(null);
- while (true) {
- unsafe.allocateMemory(_1MB);
- }
- }
- }
常见案例
在工作中一般会遇到有以下几种情况导致内存问题
传输数据量过大
因为传输数量过大、或一些极端情况导致代码中间结果对象数据量过大,过大的数据量撑爆内存
查询出大量对象
这个多为SQL语句设置问题,SQL未设置分页,用户一次查询数据量过大、频繁查询SQL导致内存堆积、或是未作判空处理导致WHERE条件为空查询出超大数据量等
接口性能问题导致
这类为外部接口性能较慢,占用内存较大,并且短时间内高QPS导致的,导致服务内存不足,线程堆积或挂起进而出现FullGC
元空间问题
使用了大量的反射代码,Java字节码存取器生成的类不断生成
问题排查
使用jmap分析内存泄漏
1.生成dump文件
- jmap -dump:format=b,file=/xx/xx/xx.hprof pid
2.dump文件下载到本地
3.dump文件分析
可以使用MAT,MAT可作为Eclipse插件或一个独立软件使用,MAT是一个高性能、具备丰富功能的Java堆内存分析工具,主要用来排查内存泄漏和内存浪费的问题。
使用MAT打开上一部后缀名.hprof的dump文件
- Histogram:直方图,各个类的实例,包括个数和大小,可以查看类引用和被引用的路径。
- Dominator Tree:支配图,列出所有线程和线程下面的那些对象占用的空间。
- Top Consumers:通过图形列出消耗内存多的实例。
- Leak Suspects:MAT自动分析的内存泄漏报表
可以用这个工具分析出什么对象什么线程占用内存空间较大,对象是被什么引用的,线程内有哪些资源占用很高
以运行时常量池溢出为例
打开Histogram类实例表
Objects是类的对象的数量;Shallow是对象本身占用内存大小、不包含其他引用;
Retained是对象自己的Shallow加上直接或间接访问到对象的Shallow之和,也可以说是GC之后可以回收的内存总和
从图中可以看出运行时常量池溢出的情况,产生了大量的String和char[]实例
在char[]上右键可以得到上图所有char[]对象的被引用路径,可以看出这些char数组都是以String的形式存在ArrayList中,并且是由main这个线程运行的
可以看出是main线程中新建了一个数组,其中存了32w+个长度为6的char数组组成的String造成的内存溢出
关于MAT的详细使用可以从MAT官方教程学习更多