原文地址: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. 使用pandoc简单教程

    使用pandoc作为过滤器 {#step-4-using-pandoc-as-a-filter} 类型 pandoc 并按Enter键.你应该看到光标就在那里,等着你输入一些东西.输入: Hello ...

  2. CentOS 安装 JDK 三种形式详细总结

    一.下载 JDK   点击下载:jdk-8u211-linux-x64.tar.gz   根据需要选择对应版本和位数,并将文件放入CentOS中的相关目录中,以 /java/jdk 目录为例,执行 m ...

  3. python变量前的单下划线(私有变量)和双下划线()

    1.单下划线 变量前的单下划线表示表面上私有 ,但是其实这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意 ...

  4. K8S学习笔记之filebeat采集K8S微服务java堆栈多行日志

    0x00 背景 K8S内运行Spring Cloud微服务,根据定制容器架构要求log文件不落地,log全部输出到std管道,由基于docker的filebeat去管道采集,然后发往Kafka或者ES ...

  5. coo ceo cfo cto cio 区别

    常见的CEO(Chief executive officer)首席执行官类似总经理.总裁,是企业的法人代表. COO(Chief operating officer)首席运营官 类似常务总经理CFO( ...

  6. ajax调用免费的天气API

    最近在做项目中要用到调用天气接口,在网上找了很多资料之后发现https://www.tianqiapi.com/的天气API挺好的,好用而且免费,调用也很简单.在此做个笔记,大家一起学习交流,如有问题 ...

  7. HttpClient 三种 Http Basic Authentication 认证方式,你了解了吗?

    Http Basic 简介 HTTP 提供一个用于权限控制和认证的通用框架.最常用的 HTTP 认证方案是 HTTP Basic authentication.Http Basic 认证是一种用来允许 ...

  8. visual studio code 应用到.net core 实战

    鉴于visual studio 2019 近期动不动卡顿与切分支后F12等功能失效的问题,开始考虑用visual studio code 代替他,对,你没有看错,就是代替visual studio 这 ...

  9. SPOJ - GSS1-Can you answer these queries I 线段树维护区间连续和最大值

    SPOJ - GSS1:https://vjudge.net/problem/SPOJ-GSS1 参考:http://www.cnblogs.com/shanyr/p/5710152.html?utm ...

  10. Educational Codeforces Round 43 E&976E. Well played! 贪心

    传送门:http://codeforces.com/contest/976/problem/E 参考:https://www.cnblogs.com/void-f/p/8978658.html 题意: ...