原文地址:Java 内存溢出分析

博客地址:http://www.moonxy.com

一、前言

Java 的 JVM 的内存一般可分为 3 个区:堆(heap)、栈(stack)和方法区(method)。

1.1 堆区

1)存储的全部是对象,每个对象都包含一个与之对应的 Class 的信息,Class 的目的是得到操作指令;

2)JVM 只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身。

1.2 栈区

1)每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中;

2)每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问;

3)栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

1.3 方法区

1)又叫静态区或永久代,跟堆一样,被所有的线程共享。方法区包含所有的 Class 和 static 变量;

2)方法区中包含的都是在整个程序中永远唯一的元素,如 Class,static 变量;

3)运行时常量池都分配在 Java 虚拟机的方法区之中,但是从 JDK 1.7 的 HotSpot 虚拟机开始中,已经把原本放在永久代的字符串常量池移出了。

二、内存溢出分析

以下代码验证,建议使用 JDK 1.6,因为从 JDK 1.7 开始,HotSpot 虚拟机改进了很多,特别是方法区(永久代)。

2.1 Java 堆溢出

Java堆是用于存储对象实例的,因此,只要我们不断创建对象,并且保证对象不被垃圾回收机制清除,那么当堆中对象的大小超过了最大堆的容量限制,就会出现堆内存溢出。

堆溢出常见的异常是:java.lang.OutOfMemoryError: Java heap space

下面这段代码,将Java堆的大小设置为 20MB,并且不可扩展(即将堆的最小值 -Xms 参数和最大值 -Xmx 参数设置为相等的 20MB,来避免堆自动扩展);通过参数 -XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机在出现内存溢出异常时生成当前内存堆的快照,以便事后进行分析;-verbose:gc 的作用是在虚拟机发生内存回收时在输出设备显示信息。

package com.java.error;

import java.util.ArrayList;
import java.util.List; /**
* VM Args:-verbose:gc -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* Error:java.lang.OutOfMemoryError: Java heap space
* @author moonxy
*
*/
public class HeapOOM { public static void main(String[] args) { List<HeapOOM> list = new ArrayList<HeapOOM>(); while(true) {
list.add(new HeapOOM());
}
}
}

运行后显示如下:

[GC (Allocation Failure)  5380K->3745K(19968K), 0.0042158 secs]
[GC (Allocation Failure) 9287K->9710K(19968K), 0.0058399 secs]
[Full GC (Ergonomics) 9710K->7589K(19968K), 0.1200134 secs]
[Full GC (Ergonomics) 16387K->12869K(19968K), 0.1112792 secs]
[Full GC (Ergonomics) 16428K->16382K(19968K), 0.1711686 secs]
[Full GC (Allocation Failure) 16382K->16370K(19968K), 0.1371103 secs]
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid1204.hprof ...
Heap dump file created [28024534 bytes in 0.077 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at com.java.error.OutOfMemoryJavaHeapSpaceTest.main(OutOfMemoryJavaHeapSpaceTest.java:19)

控制台打印了OutOfMemoryError,并且提示是Java heap发生的内存溢出,同时生成了dump文件。对于Java堆溢出,一般通过内存映像分析工具(如Eclipse Memory Analyzer)对dump文件进行堆快照分析,确认内存中的对象是不是必要的:

如果对象不是必要的,那就属于内存泄漏,需要分析为什么对象没有被回收;

如果对象确实有必要继续存在,那就属于内存溢出,需要通过将堆的大小调高(-Xms-Xmx)来避免内存溢出。

此处比较一下内存泄漏和内存溢出这两个概念,两者通常一起说,但是两个却是不同的概念,彼此之间又有联系,如下:

内存泄漏 memory leak:是指程序在申请内存后,无法释放已经申请的内存空间,一次内存泄露似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出;

内存溢出 out of memory:是指程序申请内存后,没有足够的内存供申请者使用或将 long 类型的数据存储到 int 类型的存储空间时,就会出现 OOM 错误。

2.2 虚拟机栈和本地方法栈溢出

由于HotSpot虚拟机不区分虚拟机栈和本地方法栈,因此,对于 HotSpot 来说,-Xoss参数(设置本地方法栈大小)是无效的,栈容量只由 -Xss 设置。

在Java虚拟机规范中,这个区域有两种异常情况:

如果线程运行时的栈帧的总大小超过虚拟机限制的大小,会抛出 StackOverflowError,这一点通常发生在递归运行时,栈溢出容易出现:java.lang.StackOverflowError

如果虚拟机栈设置为可以动态扩展,并且在扩展时无法申请到足够内存,则会抛出 OutOfMemoryError,此时容易出现:java.lang.OutOfMemoryError: unable to create native thread

StackOverflowError

/**
* VM Args:-Xss128k
* Error:java.lang.StackOverflowError
* @author moonxy
*/
public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() {
stackLength++;
stackLeak();
} public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}

运行后显示如下:

994
Exception in thread "main" java.lang.StackOverflowError
at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:15)
at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:16)
at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:16)
at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:16)
at com.java.error.StackOverflowTest.StackOver(StackOverflowTest.java:16)
     ……

OutOfMemoryError

不建议执行如下验证,容易发生系统假死,因为在 Windows 平台的虚拟机中,Java 线程是映射到操作系统的内核线程中的。

/**
* VM Args:-verbose:gc -Xss2m
* Error:java.lang.OutOfMemoryError: unable to create native thread
* @author moonxy
*
*/
public class JavaVMStackOOM { private void dontStop() {
while(true) { }
} public void stackLeakByThread() {
while(true) {
Thread thread = new Thread(new Runnable(){
@Override
public void run() {
dontStop();
}
});
thread.start();
}
} public static void main(String[] args) { JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}

运行后显示如下:

java.lang.OutOfMemoryError: unable to create new native thread

线程需要消耗栈容量,而操作系统分配给每个进程的内存是有限制的。考虑如下场景:系统总内存 6G,JVM分配了 2G 内存,其中堆内存分配了 1G,方法区(永久代)分配 512M,忽略其他较小的内存分配,则剩余分配给虚拟机栈和本地方法栈的内存就只有512M,很有可能没有足够的可用内存创建很多的线程。就有可能出现 java.lang.OutOfMemoryError: unable to create new native thread。如果是这种情况,考虑减小堆内存大小或适当减少每个线程的栈分配大小。

线程会占用内存,如果每个线程都占用更多内存,整体上将消耗更多的内存。每个线程默认占用内存大小取决于 JVM 实现。可以利用 -Xss 参数限制线程内存大小,降低总内存消耗。例如,JVM 默认每个线程占用 1M 内存,应用有 500个 线程,那么将消耗 500M 内存空间。如果实际上 256K 内存足够线程正常运行,配置 -Xss256k,那么 500 个线程将只需要消耗 125M 内存。(注意,如果 -Xss 设置的过低,又将会产生 java.lang.StackOverflowError 错误)

2.3 方法区溢出

方法区存储的是虚拟机加载的类信息、常量、静态变量、JIT 编译器编译后的代码等数据,在 JDK1.7之前,HotSpot 都是使用 "永久代" 来管理这些数据,也就是说,方法区的数据,其实也是属于堆内存的,会占用堆内存。

因此:方法区的数据大小 < -XX:MaxPermSize < -Xmx

我们只要限制一下永久代的大小(-XX:MaxPermSize),很容易就会发生方法区溢出,此时方法区容易出现:java.lang.OutOfMemoryError: PermGen space。可以通过设置 -XX:PermSize-XX:MaxPermSize 来限制方法区大小。

/**
* VM Args:-verbose:gc -XX:PermSize=10M -XX:MaxPermSize=10M
* Error:java.lang.OutOfMemoryError: PermGen space
* @author moonxy
*
*/
public class OutOfMemoryPermGenSpaceTest { public static void main(String[] args) { List<String> oomPgsList = new ArrayList<String>();
int i = 0;
while(true) {
oomPgsList.add(String.valueOf(i++).intern());
}
}
}

运行后结果如下:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

如果我们在 JDK 1.7 的环境下执行上面同样的代码,会发现 while 循环会一直执行下去。原因是在 JDK 1.7 中,HotSpot 已经不再将常量放入永久代中进行管理,而是放到内存上限是本机内存的 Native Memory 中,这样做的好处就是减少了内存溢出的几率(没有了-XX:MaxPermSize 的限制),同时常量不再占用堆的内存。这种 "去永久代" 的做法,从 JDK 1.7 已经开始进行。终于在 JDK 1.8 中,被彻底的执行了,永久代被彻底去掉了,同时 HotSpot 新增了一块叫做 Mataspace 的区域,并提供了 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 参数,来设置运行 Java 虚拟机使用的 Metaspace 的初始容量和最大容量。

2.4 本机直接内存溢出

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。但是这部分内存也被频繁的使用,而且也可能导致 OutOfMemoryError 出现,即:java.lang.OutOfMemory

本机直接内存的容量可以通过 -XX:MaxDirectMemorySize 指定,如果不指定,默认与 Java 堆最大值(-Xmx指定)一样。

本机直接内存溢出的一个明显特征是,dump 文件很小,因为主要对象都在 direct memory了,并且异常信息也不会说明是在哪个区域发生内存溢出,就像这样:java.lang.OutOfMemoryError

/**
*
* VM Args:-verbose:gc -XX:MaxDirectMemorySize
* Error:java.lang.OutOfMemory
* @author moonxy
*
*/
public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}

运行结果如下:

Exception in thread "main" java.lang.OutOfMemoryError

综上,当 Java 应用比较大时,可以设置如下的 JVM 参数:

-Xms256m -Xmx1024m -XX:PermSize=256m -XX:MaxPermSize=512m

注意:

方法区存储的对象类型在 JDK 1.6、1.7、1.8 等不同的 JDK 版本中都有变化,在具体的开发中,需要特别注意 JDK 版本的不同,而导致的一些差异。

JDK 1.6 之后提供了 jvisualvm 工具,用于监控 JVM 内存的使用情况,通常在 jdk 的 bin 目录下,直接运行 jvisualvm.exe,可以监控内存泄露、跟踪垃圾回收、运行时内存、cpu 分析、线程分析等。

Java 内存溢出分析的更多相关文章

  1. java内存溢出分析(二)

    我们继续java内存溢出分析(一)的分析,点击Details>按钮,显示如下图,我们发现有一个对象数量达到280370216个,再点击其中的List objects 点击后,显示下图 至此,我们 ...

  2. java内存溢出分析工具

    http://www.cnblogs.com/preftest/archive/2011/12/08/2281322.html java内存溢出分析工具:jmap使用实战 在一次解决系统tomcat老 ...

  3. Java内存溢出分析方法(Eclipse Memory Analyzer 使用简单入门)

    转载至:http://outofmemory.cn/java/jvm/OutOfMemoryError-analysis 工具 安装Memory Analyse Tools(MAT) 工具, 可以直接 ...

  4. java内存溢出分析工具:jmap使用实战

    在一次解决系统tomcat老是内存撑到头,然后崩溃的问题时,使用到了jmap. 1 使用命令 在环境是linux+jdk1.5以上,这个工具是自带的,路径在JDK_HOME/bin/下 jmap -h ...

  5. java内存溢出分析(一)

    在项目中发现内存占用过高,且一直不会释放,top命令如下图显示 可以看到pid为17453的java进程占用27.1%内存,且长时间没有释放. 1.使用命令生成heap日志以供分析 sudo jmap ...

  6. jvm内存溢出分析

    概述 jvm中除了程序计数器,其他的区域都有可能会发生内存溢出 内存溢出是什么? 当程序需要申请内存的时候,由于没有足够的内存,此时就会抛出OutOfMemoryError,这就是内存溢出 内存溢出和 ...

  7. Java内存溢出的详细解决方案

    本文介绍了Java内存溢出的详细解决方案.本文总结内存溢出主要有两种情况,而JVM经常调用垃圾回收器解决内存堆不足的问题,但是有时仍会有内存不足的错误.作者分析了JVM内存区域组成及JVM设置虚拟内存 ...

  8. 老李案例分享:定位JAVA内存溢出

    老李案例分享:定位JAVA内存溢出   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.在poptest的loadrunner的培 ...

  9. Java内存溢出异常(上)

    上一篇文章我们讲了JVM运行时数据区域与内存溢出异常,其中对于内存溢出异常这部分将的不够详细,这篇文章将着重讲解Java内存溢出异常的相关知识.如果有没看过上一篇文章的小伙伴们,请点击Java内存区域 ...

随机推荐

  1. [Spring cloud 一步步实现广告系统] 20. 系统运行测试

    系统运行 经过长时间的编码实现,我们的主体模块已经大致完成,因为之前我们都是零散的对各个微服务自行测试,接下来,我们需要将所有的服务模块进行联调测试,Let's do it. 清除测试数据&测 ...

  2. Amazon S3 分布式存储的 python 接口实现

    Amazon s3 是一种分布式的对象存储.用键值对的方式,来存储数据.其中,存入的所有数据都是一个对象(object),每一个对象都有一个键(key)存在. 具有非常方便的 web 访问接口,以及权 ...

  3. CGI getenv 参数

    变量名 描述 CONTENT_TYPE 这个环境变量的值指示所传递来的信息的MIME类型.目前,环境变量CONTENT_TYPE一般都是:application/x-www-form-urlencod ...

  4. Linux设备驱动程序学习----3.模块的编译和装载

    模块的编译和装载 更多内容请参考Linux设备驱动程序学习----目录 1. 设置测试系统 第1步,要先从kernel.org的镜像网站上获取一个主线内核,并安装到自己的系统中,因为学习驱动程序的编写 ...

  5. 用Canvas实现Photoshop的钢笔工具(贝塞尔曲线)

    前两天在用Canvas实现一个绘制路径的小功能.做完之后发现加以完善可以“复刻”一下PS里面的钢笔工具. PS里的钢笔工具对我来说是PS中最好用的工具! 所以本文主要介绍如何用Canvas来实现Pho ...

  6. Python笔记_基础

    1.注释 # 单行注释 """ 多行注释,一般用于类说明 """ 或 ''' 多行注释 ''' 2.工作日志 # TODO 说明性文字 记录 ...

  7. Java 并发:学习Thread 类

    Java 中 Thread类 的各种操作与线程的生命周期密不可分,了解线程的生命周期有助于对Thread类中的各方法的理解.一般来说,线程从最初的创建到最终的消亡,要经历创建.就绪.运行.阻塞 和 消 ...

  8. python requests接口测试系列:连接mysql,获取mysql查询的值作为接口的入参

    主要思路: 连接mysql数据库,这里数据库需要使用Proxifier来设置代理,然后才能正常连接 获取mysql数据库中某一数据,作为接口的参数信息 将接口返回结果保存至csv数据表中 # -*- ...

  9. Spring系列(五):Spring AOP源码解析

    一.@EnableAspectJAutoProxy注解 在主配置类中添加@EnableAspectJAutoProxy注解,开启aop支持,那么@EnableAspectJAutoProxy到底做了什 ...

  10. React项目升级遇到的问题复盘(2019-09-02)

    老铁们,发没发现我换了个贼帅的头像,高端大气上档次,非洲大地我最凶!可把我自己牛逼坏了. 不扯啦不扯啦,抓紧进入今天的正题,从今天开始我会每天写一下每天工作的出现的问题,主要对这些问题出现的原因,以及 ...