- 浏览: 260641 次
- 性别:
- 来自: 新乡
文章分类
- 全部博客 (227)
- servciemix (10)
- db (18)
- javaTools (4)
- hibernate (31)
- web (3)
- spring (14)
- design pattern (4)
- java security (3)
- portal (1)
- ejb (6)
- session (2)
- java_lang (21)
- jbpm (29)
- struts (7)
- orgRights (2)
- project manager Jira (7)
- 跨库事务 (2)
- mysql (14)
- ubuntu (7)
- osgi (9)
- maven ant make (4)
- 分布式 高并发 高性能 (5)
- virgo-dm_server (0)
- osgi web (3)
- platform (1)
- smooks (1)
- business (1)
- 职场生涯 (14)
- Java编码格式 (2)
- web服务 (1)
- 计算机使用 (1)
- 健康工作生活的保障,工作中务必抛掉的不良心态 (4)
- 电信-网络监控 (1)
- 多线程-multithread (1)
- 海量数据-高性能 (2)
- Mybatis (1)
- web开发平台研发 (0)
- oracle (0)
- 应用服务器调优 (0)
- web前端 (0)
- servlet-jsp (0)
- tomcat (2)
- newtouch (1)
- portal_liferay (2)
- version control (1)
- apm-impact (2)
- tools (1)
- 研发管理 (1)
- 电商业务 (1)
- 生鲜电商市场调查 (0)
- PBX (0)
- 房东 (0)
最新评论
-
lifuchao:
...
权限问题 -
Branding:
谢谢,受教了,另外,CONN AS SYSDBA,必须是在操作 ...
Oracle密码忘记了怎么办? -
zhuchao_ko:
...
Portal实现原理 -
败类斯文:
不知道改哪里。。。木有见到红色。。表示悟性低了、、
jira error: Neither the JAVA_HOME nor the JRE_HOME environment variable is defin -
c__06:
正文:假如事务我是这样定义的: <tx:method n ...
Spring中Transactional配置
JVM内存管理深入Java内存区域与OOM
2011-2-22 javaeye 佚名 【字体:大 中 小】
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。
?
概述:
对于从事C、C++程序开发的开发人员来说,在内存管理领域,他们即是拥有最高权力的皇帝又是执行最基础工作的劳动人民——拥有每一个对象的“所有权”,又担负着每一个对象生命开始到终结的维护责任。
?
对于Java程序员来说,不需要在为每一个new操作去写配对的delete/free,不容易出现内容泄漏和内存溢出错误,看起来由JVM管理内存一切都很美好。不过,也正是因为Java程序员把内存控制的权力交给了JVM,一旦出现泄漏和溢出,如果不了解JVM是怎样使用内存的,那排查错误将会是一件非常困难的事情。
?
VM运行时数据区域
JVM执行Java程序的过程中,会使用到各种数据区域,这些区域有各自的用途、创建和销毁时间。根据《Java虚拟机规范(第二版)》(下文称VM Spec)的规定,JVM包括下列几个运行时数据区域:
?
1.程序计数器(Program Counter Register):
?
每一个Java线程都有一个程序计数器来用于保存程序执行到当前方法的哪一个指令,对于非Native方法,这个区域记录的是正在执行的VM原语的地址,如果正在执行的是Natvie方法,这个区域则为空(undefined)。此内存区域是唯一一个在VM Spec中没有规定任何OutOfMemoryError情况的区域。
?
2.Java虚拟机栈(Java Virtual Machine Stacks)
与程序计数器一样,VM栈的生命周期也是与线程相同。VM栈描述的是Java方法调用的内存模型:每个方法被执行的时候,都会同时创建一个帧(Frame)用于存储本地变量表、操作栈、动态链接、方法出入口等信息。每一个方法的调用至完成,就意味着一个帧在VM栈中的入栈至出栈的过程。在后文中,我们将着重讨论VM栈中本地变量表部分。
经常有人把Java内存简单的区分为堆内存(Heap)和栈内存(Stack),实际中的区域远比这种观点复杂,这样划分只是说明与变量定义密切相关的内存区域是这两块。其中所指的“堆”后面会专门描述,而所指的“栈”就是VM栈中各个帧的本地变量表部分。本地变量表存放了编译期可知的各种标量类型(boolean、byte、char、short、int、float、long、double)、对象引用(不是对象本身,仅仅是一个引用指针)、方法返回地址等。其中long和double会占用2个本地变量空间(32bit),其余占用1个。本地变量表在进入方法时进行分配,当进入一个方法时,这个方法需要在帧中分配多大的本地变量是一件完全确定的事情,在方法运行期间不改变本地变量表的大小。
在VM Spec中对这个区域规定了2中异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果VM栈可以动态扩展(VM Spec中允许固定长度的VM栈),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。
3.本地方法栈(Native Method Stacks)
本地方法栈与VM栈所发挥作用是类似的,只不过VM栈为虚拟机运行VM原语服务,而本地方法栈是为虚拟机使用到的Native方法服务。它的实现的语言、方式与结构并没有强制规定,甚至有的虚拟机(譬如Sun Hotspot虚拟机)直接就把本地方法栈和VM栈合二为一。和VM栈一样,这个区域也会抛出StackOverflowError和OutOfMemoryError异常。
4.Java堆(Java Heap)
对于绝大多数应用来说,Java堆是虚拟机管理最大的一块内存。Java堆是被所有线程共享的,在虚拟机启动时创建。Java堆的唯一目的就是存放对象实例,绝大部分的对象实例都在这里分配。这一点在VM Spec中的描述是:所有的实例以及数组都在堆上分配(原文:The heap is the runtime data area from which memory for all class instances and arrays is allocated),但是在逃逸分析和标量替换优化技术出现后,VM Spec的描述就显得并不那么准确了。
Java堆内还有更细致的划分:新生代、老年代,再细致一点的:eden、from survivor、to survivor,甚至更细粒度的本地线程分配缓冲(TLAB)等,无论对Java堆如何划分,目的都是为了更好的回收内存,或者更快的分配内存,在本章中我们仅仅针对内存区域的作用进行讨论,Java堆中的上述各个区域的细节,可参见本文第二章《JVM内存管理:深入垃圾收集器与内存分配策略》。
根据VM Spec的要求,Java堆可以处于物理上不连续的内存空间,它逻辑上是连续的即可,就像我们的磁盘空间一样。实现时可以选择实现成固定大小的,也可以是可扩展的,不过当前所有商业的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中无法分配内存,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
5.方法区(Method Area)
叫“方法区”可能认识它的人还不太多,如果叫永久代(Permanent Generation)它的粉丝也许就多了。它还有个别名叫做Non-Heap(非堆),但是VM Spec上则描述方法区为堆的一个逻辑部分(原文:the method area is logically part of the heap),这个名字的问题还真容易令人产生误解,我们在这里就不纠结了。
方法区中存放了每个Class的结构信息,包括常量池、字段描述、方法描述等等。VM Space描述中对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存,也可以选择固定大小或者可扩展外,甚至可以选择不实现垃圾收集。相对来说,垃圾收集行为在这个区域是相对比较少发生的,但并不是某些描述那样永久代不会发生GC(至少对当前主流的商业JVM实现来说是如此),这里的GC主要是对常量池的回收和对类的卸载,虽然回收的“成绩”一般也比较差强人意,尤其是类卸载,条件相当苛刻。
6.运行时常量池(Runtime Constant Pool)
Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量表(constant_pool table),用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。但是Java语言并不要求常量一定只有编译期预置入Class的常量表的内容才能进入方法区常量池,运行期间也可将新内容放入常量池(最典型的String.intern()方法)。
运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法在申请到内存时会抛出OutOfMemoryError异常。
?
7.本机直接内存(Direct Memory)
直接内存并不是虚拟机运行时数据区的一部分,它根本就是本机内存而不是VM直接管理的区域。但是这部分内存也会导致OutOfMemoryError异常出现,因此我们放到这里一起描述。
在JDK1.4中新加入了NIO类,引入一种基于渠道与缓冲区的I/O方式,它可以通过本机Native函数库直接分配本机内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java对和本机堆中来回复制数据。
显然本机直接内存的分配不会受到Java堆大小的限制,但是即然是内存那肯定还是要受到本机物理内存(包括SWAP区或者Windows虚拟内存)的限制的,一般服务器管理员配置JVM参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),而导致动态扩展时出现OutOfMemoryError异常。
?
实战OutOfMemoryError
上述区域中,除了程序计数器,其他在VM Spec中都描述了产生OutOfMemoryError(下称OOM)的情形,那我们就实战模拟一下,通过几段简单的代码,令对应的区域产生OOM异常以便加深认识,同时初步介绍一些与内存相关的虚拟机参数。下文的代码都是基于Sun Hotspot虚拟机1.6版的实现,对于不同公司的不同版本的虚拟机,参数与程序运行结果可能结果会有所差别。
?
Java堆
?
Java堆存放的是对象实例,因此只要不断建立对象,并且保证GC Roots到对象之间有可达路径即可产生OOM异常。测试中限制Java堆大小为20M,不可扩展,通过参数-XX:+HeapDumpOnOutOfMemoryError让虚拟机在出现OOM异常的时候Dump出内存映像以便分析。(关于Dump映像文件分析方面的内容,可参见本文第三章《JVM内存管理:深入JVM内存异常分析与调优》。)
清单1:Java堆OOM测试
/**
?* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
?* @author zzm
?*/
public class HeapOOM {
?
?????? static class OOMObject {
?????? }
?
?????? public static void main(String[] args) {
?????? ?????? List<OOMObject> list = new ArrayList<OOMObject>();
?
?????? ?????? while (true) {
?????? ?????? ?????? list.add(new OOMObject());
?????? ?????? }
?????? }
}
?
运行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3404.hprof ...
Heap dump file created [22045981 bytes in 0.663 secs]
?
?
VM栈和本地方法栈
?
Hotspot虚拟机并不区分VM栈和本地方法栈,因此-Xoss参数实际上是无效的,栈容量只由-Xss参数设定。关于VM栈和本地方法栈在VM Spec描述了两种异常:StackOverflowError与OutOfMemoryError,当栈空间无法继续分配分配时,到底是内存太小还是栈太大其实某种意义上是对同一件事情的两种描述而已,在笔者的实验中,对于单线程应用尝试下面3种方法均无法让虚拟机产生OOM,全部尝试结果都是获得SOF异常。
?
1.使用-Xss参数削减栈内存容量。结果:抛出SOF异常时的堆栈深度相应缩小。
2.定义大量的本地变量,增大此方法对应帧的长度。结果:抛出SOF异常时的堆栈深度相应缩小。
3.创建几个定义很多本地变量的复杂对象,打开逃逸分析和标量替换选项,使得JIT编译器允许对象拆分后在栈中分配。结果:实际效果同第二点。
?
清单2:VM栈和本地方法栈OOM测试(仅作为第1点测试程序)
/**
?* VM Args:-Xss128k
?* @author zzm
?*/
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;
?????? ?????? }
?????? }
}
?
运行结果:
stack length:2402
Exception in thread "main" java.lang.StackOverflowError
??????? at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:20)
??????? at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)
??????? at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)
?
如果在多线程环境下,不断建立线程倒是可以产生OOM异常,但是基本上这个异常和VM栈空间够不够关系没有直接关系,甚至是给每个线程的VM栈分配的内存越多反而越容易产生这个OOM异常。
?
原因其实很好理解,操作系统分配给每个进程的内存是有限制的,譬如32位Windows限制为2G,Java堆和方法区的大小JVM有参数可以限制最大值,那剩余的内存为2G(操作系统限制)-Xmx(最大堆)-MaxPermSize(最大方法区),程序计数器消耗内存很小,可以忽略掉,那虚拟机进程本身耗费的内存不计算的话,剩下的内存就供每一个线程的VM栈和本地方法栈瓜分了,那自然每个线程中VM栈分配内存越多,就越容易把剩下的内存耗尽。
?
清单3:创建线程导致OOM异常
/**
?* VM Args:-Xss2M (这时候不妨设大些)
?* @author zzm
?*/
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) throws Throwable {
?????? ?????? JavaVMStackOOM oom = new JavaVMStackOOM();
?????? ?????? oom.stackLeakByThread();
?????? }
}
?
特别提示一下,如果读者要运行上面这段代码,记得要存盘当前工作,上述代码执行时有很大令操作系统卡死的风险。
?
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
运行时常量池
?
要在常量池里添加内容,最简单的就是使用String.intern()这个Native方法。由于常量池分配在方法区内,我们只需要通过-XX:PermSize和-XX:MaxPermSize限制方法区大小即可限制常量池容量。实现代码如下:
?
清单4:运行时常量池导致的OOM异常
/**
?* VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
?* @author zzm
?*/
public class RuntimeConstantPoolOOM {
?
?????? public static void main(String[] args) {
?????? ?????? // 使用List保持着常量池引用,压制Full GC回收常量池行为
?????? ?????? List<String> list = new ArrayList<String>();
?????? ?????? // 10M的PermSize在integer范围内足够产生OOM了
?????? ?????? int i = 0;
?????? ?????? while (true) {
?????? ?????? ?????? list.add(String.valueOf(i++).intern());
?????? ?????? }
?????? }
}
?
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
?????? at java.lang.String.intern(Native Method)
?????? at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)
?
?
方法区
?
上文讲过,方法区用于存放Class相关信息,所以这个区域的测试我们借助CGLib直接操作字节码动态生成大量的Class,值得注意的是,这里我们这个例子中模拟的场景其实经常会在实际应用中出现:当前很多主流框架,如Spring、Hibernate对类进行增强时,都会使用到CGLib这类字节码技术,当增强的类越多,就需要越大的方法区用于保证动态生成的Class可以加载入内存。
?
清单5:借助CGLib使得方法区出现OOM异常
/**
?* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
?* @author zzm
?*/
public class JavaMethodAreaOOM {
?
?????? public static void main(String[] args) {
?????? ?????? while (true) {
?????? ?????? ?????? Enhancer enhancer = new Enhancer();
?????? ?????? ?????? enhancer.setSuperclass(OOMObject.class);
?????? ?????? ?????? enhancer.setUseCache(false);
?????? ?????? ?????? enhancer.setCallback(new MethodInterceptor() {
?????? ?????? ?????? ?????? public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
?????? ?????? ?????? ?????? ?????? return proxy.invokeSuper(obj, args);
?????? ?????? ?????? ?????? }
?????? ?????? ?????? });
?????? ?????? ?????? enhancer.create();
?????? ?????? }
?????? }
?
?????? static class OOMObject {
?
?????? }
}
?
运行结果:
Caused by: java.lang.OutOfMemoryError: PermGen space
?????? at java.lang.ClassLoader.defineClass1(Native Method)
?????? at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
?????? at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
?????? ... 8 more
?
本机直接内存
?
DirectMemory容量可通过-XX:MaxDirectMemorySize指定,不指定的话默认与Java堆(-Xmx指定)一样,下文代码越过了DirectByteBuffer,直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回实例,也就是基本上只有rt.jar里面的类的才能使用),因为DirectByteBuffer也会抛OOM异常,但抛出异常时实际上并没有真正向操作系统申请分配内存,而是通过计算得知无法分配既会抛出,真正申请分配的方法是unsafe.allocateMemory()。
?
/**
?* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
?* @author zzm
?*/
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
?????? at sun.misc.Unsafe.allocateMemory(Native Method)
?????? at org.fenixsoft.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:20)
?
?
总结
到此为止,我们弄清楚虚拟机里面的内存是如何划分的,哪部分区域,什么样的代码、操作可能导致OOM异常。虽然Java有垃圾收集机制,但OOM仍然离我们并不遥远,本章内容我们只是知道各个区域OOM异常出现的原因,下一章我们将看看Java垃圾收集机制为了避免OOM异常出现,做出了什么样的努力。
2011-2-22 javaeye 佚名 【字体:大 中 小】
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。
?
概述:
对于从事C、C++程序开发的开发人员来说,在内存管理领域,他们即是拥有最高权力的皇帝又是执行最基础工作的劳动人民——拥有每一个对象的“所有权”,又担负着每一个对象生命开始到终结的维护责任。
?
对于Java程序员来说,不需要在为每一个new操作去写配对的delete/free,不容易出现内容泄漏和内存溢出错误,看起来由JVM管理内存一切都很美好。不过,也正是因为Java程序员把内存控制的权力交给了JVM,一旦出现泄漏和溢出,如果不了解JVM是怎样使用内存的,那排查错误将会是一件非常困难的事情。
?
VM运行时数据区域
JVM执行Java程序的过程中,会使用到各种数据区域,这些区域有各自的用途、创建和销毁时间。根据《Java虚拟机规范(第二版)》(下文称VM Spec)的规定,JVM包括下列几个运行时数据区域:
?
1.程序计数器(Program Counter Register):
?
每一个Java线程都有一个程序计数器来用于保存程序执行到当前方法的哪一个指令,对于非Native方法,这个区域记录的是正在执行的VM原语的地址,如果正在执行的是Natvie方法,这个区域则为空(undefined)。此内存区域是唯一一个在VM Spec中没有规定任何OutOfMemoryError情况的区域。
?
2.Java虚拟机栈(Java Virtual Machine Stacks)
与程序计数器一样,VM栈的生命周期也是与线程相同。VM栈描述的是Java方法调用的内存模型:每个方法被执行的时候,都会同时创建一个帧(Frame)用于存储本地变量表、操作栈、动态链接、方法出入口等信息。每一个方法的调用至完成,就意味着一个帧在VM栈中的入栈至出栈的过程。在后文中,我们将着重讨论VM栈中本地变量表部分。
经常有人把Java内存简单的区分为堆内存(Heap)和栈内存(Stack),实际中的区域远比这种观点复杂,这样划分只是说明与变量定义密切相关的内存区域是这两块。其中所指的“堆”后面会专门描述,而所指的“栈”就是VM栈中各个帧的本地变量表部分。本地变量表存放了编译期可知的各种标量类型(boolean、byte、char、short、int、float、long、double)、对象引用(不是对象本身,仅仅是一个引用指针)、方法返回地址等。其中long和double会占用2个本地变量空间(32bit),其余占用1个。本地变量表在进入方法时进行分配,当进入一个方法时,这个方法需要在帧中分配多大的本地变量是一件完全确定的事情,在方法运行期间不改变本地变量表的大小。
在VM Spec中对这个区域规定了2中异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果VM栈可以动态扩展(VM Spec中允许固定长度的VM栈),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。
3.本地方法栈(Native Method Stacks)
本地方法栈与VM栈所发挥作用是类似的,只不过VM栈为虚拟机运行VM原语服务,而本地方法栈是为虚拟机使用到的Native方法服务。它的实现的语言、方式与结构并没有强制规定,甚至有的虚拟机(譬如Sun Hotspot虚拟机)直接就把本地方法栈和VM栈合二为一。和VM栈一样,这个区域也会抛出StackOverflowError和OutOfMemoryError异常。
4.Java堆(Java Heap)
对于绝大多数应用来说,Java堆是虚拟机管理最大的一块内存。Java堆是被所有线程共享的,在虚拟机启动时创建。Java堆的唯一目的就是存放对象实例,绝大部分的对象实例都在这里分配。这一点在VM Spec中的描述是:所有的实例以及数组都在堆上分配(原文:The heap is the runtime data area from which memory for all class instances and arrays is allocated),但是在逃逸分析和标量替换优化技术出现后,VM Spec的描述就显得并不那么准确了。
Java堆内还有更细致的划分:新生代、老年代,再细致一点的:eden、from survivor、to survivor,甚至更细粒度的本地线程分配缓冲(TLAB)等,无论对Java堆如何划分,目的都是为了更好的回收内存,或者更快的分配内存,在本章中我们仅仅针对内存区域的作用进行讨论,Java堆中的上述各个区域的细节,可参见本文第二章《JVM内存管理:深入垃圾收集器与内存分配策略》。
根据VM Spec的要求,Java堆可以处于物理上不连续的内存空间,它逻辑上是连续的即可,就像我们的磁盘空间一样。实现时可以选择实现成固定大小的,也可以是可扩展的,不过当前所有商业的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中无法分配内存,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
5.方法区(Method Area)
叫“方法区”可能认识它的人还不太多,如果叫永久代(Permanent Generation)它的粉丝也许就多了。它还有个别名叫做Non-Heap(非堆),但是VM Spec上则描述方法区为堆的一个逻辑部分(原文:the method area is logically part of the heap),这个名字的问题还真容易令人产生误解,我们在这里就不纠结了。
方法区中存放了每个Class的结构信息,包括常量池、字段描述、方法描述等等。VM Space描述中对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存,也可以选择固定大小或者可扩展外,甚至可以选择不实现垃圾收集。相对来说,垃圾收集行为在这个区域是相对比较少发生的,但并不是某些描述那样永久代不会发生GC(至少对当前主流的商业JVM实现来说是如此),这里的GC主要是对常量池的回收和对类的卸载,虽然回收的“成绩”一般也比较差强人意,尤其是类卸载,条件相当苛刻。
6.运行时常量池(Runtime Constant Pool)
Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量表(constant_pool table),用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。但是Java语言并不要求常量一定只有编译期预置入Class的常量表的内容才能进入方法区常量池,运行期间也可将新内容放入常量池(最典型的String.intern()方法)。
运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法在申请到内存时会抛出OutOfMemoryError异常。
?
7.本机直接内存(Direct Memory)
直接内存并不是虚拟机运行时数据区的一部分,它根本就是本机内存而不是VM直接管理的区域。但是这部分内存也会导致OutOfMemoryError异常出现,因此我们放到这里一起描述。
在JDK1.4中新加入了NIO类,引入一种基于渠道与缓冲区的I/O方式,它可以通过本机Native函数库直接分配本机内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java对和本机堆中来回复制数据。
显然本机直接内存的分配不会受到Java堆大小的限制,但是即然是内存那肯定还是要受到本机物理内存(包括SWAP区或者Windows虚拟内存)的限制的,一般服务器管理员配置JVM参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),而导致动态扩展时出现OutOfMemoryError异常。
?
实战OutOfMemoryError
上述区域中,除了程序计数器,其他在VM Spec中都描述了产生OutOfMemoryError(下称OOM)的情形,那我们就实战模拟一下,通过几段简单的代码,令对应的区域产生OOM异常以便加深认识,同时初步介绍一些与内存相关的虚拟机参数。下文的代码都是基于Sun Hotspot虚拟机1.6版的实现,对于不同公司的不同版本的虚拟机,参数与程序运行结果可能结果会有所差别。
?
Java堆
?
Java堆存放的是对象实例,因此只要不断建立对象,并且保证GC Roots到对象之间有可达路径即可产生OOM异常。测试中限制Java堆大小为20M,不可扩展,通过参数-XX:+HeapDumpOnOutOfMemoryError让虚拟机在出现OOM异常的时候Dump出内存映像以便分析。(关于Dump映像文件分析方面的内容,可参见本文第三章《JVM内存管理:深入JVM内存异常分析与调优》。)
清单1:Java堆OOM测试
/**
?* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
?* @author zzm
?*/
public class HeapOOM {
?
?????? static class OOMObject {
?????? }
?
?????? public static void main(String[] args) {
?????? ?????? List<OOMObject> list = new ArrayList<OOMObject>();
?
?????? ?????? while (true) {
?????? ?????? ?????? list.add(new OOMObject());
?????? ?????? }
?????? }
}
?
运行结果:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3404.hprof ...
Heap dump file created [22045981 bytes in 0.663 secs]
?
?
VM栈和本地方法栈
?
Hotspot虚拟机并不区分VM栈和本地方法栈,因此-Xoss参数实际上是无效的,栈容量只由-Xss参数设定。关于VM栈和本地方法栈在VM Spec描述了两种异常:StackOverflowError与OutOfMemoryError,当栈空间无法继续分配分配时,到底是内存太小还是栈太大其实某种意义上是对同一件事情的两种描述而已,在笔者的实验中,对于单线程应用尝试下面3种方法均无法让虚拟机产生OOM,全部尝试结果都是获得SOF异常。
?
1.使用-Xss参数削减栈内存容量。结果:抛出SOF异常时的堆栈深度相应缩小。
2.定义大量的本地变量,增大此方法对应帧的长度。结果:抛出SOF异常时的堆栈深度相应缩小。
3.创建几个定义很多本地变量的复杂对象,打开逃逸分析和标量替换选项,使得JIT编译器允许对象拆分后在栈中分配。结果:实际效果同第二点。
?
清单2:VM栈和本地方法栈OOM测试(仅作为第1点测试程序)
/**
?* VM Args:-Xss128k
?* @author zzm
?*/
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;
?????? ?????? }
?????? }
}
?
运行结果:
stack length:2402
Exception in thread "main" java.lang.StackOverflowError
??????? at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:20)
??????? at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)
??????? at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)
?
如果在多线程环境下,不断建立线程倒是可以产生OOM异常,但是基本上这个异常和VM栈空间够不够关系没有直接关系,甚至是给每个线程的VM栈分配的内存越多反而越容易产生这个OOM异常。
?
原因其实很好理解,操作系统分配给每个进程的内存是有限制的,譬如32位Windows限制为2G,Java堆和方法区的大小JVM有参数可以限制最大值,那剩余的内存为2G(操作系统限制)-Xmx(最大堆)-MaxPermSize(最大方法区),程序计数器消耗内存很小,可以忽略掉,那虚拟机进程本身耗费的内存不计算的话,剩下的内存就供每一个线程的VM栈和本地方法栈瓜分了,那自然每个线程中VM栈分配内存越多,就越容易把剩下的内存耗尽。
?
清单3:创建线程导致OOM异常
/**
?* VM Args:-Xss2M (这时候不妨设大些)
?* @author zzm
?*/
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) throws Throwable {
?????? ?????? JavaVMStackOOM oom = new JavaVMStackOOM();
?????? ?????? oom.stackLeakByThread();
?????? }
}
?
特别提示一下,如果读者要运行上面这段代码,记得要存盘当前工作,上述代码执行时有很大令操作系统卡死的风险。
?
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
运行时常量池
?
要在常量池里添加内容,最简单的就是使用String.intern()这个Native方法。由于常量池分配在方法区内,我们只需要通过-XX:PermSize和-XX:MaxPermSize限制方法区大小即可限制常量池容量。实现代码如下:
?
清单4:运行时常量池导致的OOM异常
/**
?* VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
?* @author zzm
?*/
public class RuntimeConstantPoolOOM {
?
?????? public static void main(String[] args) {
?????? ?????? // 使用List保持着常量池引用,压制Full GC回收常量池行为
?????? ?????? List<String> list = new ArrayList<String>();
?????? ?????? // 10M的PermSize在integer范围内足够产生OOM了
?????? ?????? int i = 0;
?????? ?????? while (true) {
?????? ?????? ?????? list.add(String.valueOf(i++).intern());
?????? ?????? }
?????? }
}
?
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
?????? at java.lang.String.intern(Native Method)
?????? at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)
?
?
方法区
?
上文讲过,方法区用于存放Class相关信息,所以这个区域的测试我们借助CGLib直接操作字节码动态生成大量的Class,值得注意的是,这里我们这个例子中模拟的场景其实经常会在实际应用中出现:当前很多主流框架,如Spring、Hibernate对类进行增强时,都会使用到CGLib这类字节码技术,当增强的类越多,就需要越大的方法区用于保证动态生成的Class可以加载入内存。
?
清单5:借助CGLib使得方法区出现OOM异常
/**
?* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
?* @author zzm
?*/
public class JavaMethodAreaOOM {
?
?????? public static void main(String[] args) {
?????? ?????? while (true) {
?????? ?????? ?????? Enhancer enhancer = new Enhancer();
?????? ?????? ?????? enhancer.setSuperclass(OOMObject.class);
?????? ?????? ?????? enhancer.setUseCache(false);
?????? ?????? ?????? enhancer.setCallback(new MethodInterceptor() {
?????? ?????? ?????? ?????? public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
?????? ?????? ?????? ?????? ?????? return proxy.invokeSuper(obj, args);
?????? ?????? ?????? ?????? }
?????? ?????? ?????? });
?????? ?????? ?????? enhancer.create();
?????? ?????? }
?????? }
?
?????? static class OOMObject {
?
?????? }
}
?
运行结果:
Caused by: java.lang.OutOfMemoryError: PermGen space
?????? at java.lang.ClassLoader.defineClass1(Native Method)
?????? at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
?????? at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
?????? ... 8 more
?
本机直接内存
?
DirectMemory容量可通过-XX:MaxDirectMemorySize指定,不指定的话默认与Java堆(-Xmx指定)一样,下文代码越过了DirectByteBuffer,直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回实例,也就是基本上只有rt.jar里面的类的才能使用),因为DirectByteBuffer也会抛OOM异常,但抛出异常时实际上并没有真正向操作系统申请分配内存,而是通过计算得知无法分配既会抛出,真正申请分配的方法是unsafe.allocateMemory()。
?
/**
?* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
?* @author zzm
?*/
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
?????? at sun.misc.Unsafe.allocateMemory(Native Method)
?????? at org.fenixsoft.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:20)
?
?
总结
到此为止,我们弄清楚虚拟机里面的内存是如何划分的,哪部分区域,什么样的代码、操作可能导致OOM异常。虽然Java有垃圾收集机制,但OOM仍然离我们并不遥远,本章内容我们只是知道各个区域OOM异常出现的原因,下一章我们将看看Java垃圾收集机制为了避免OOM异常出现,做出了什么样的努力。
发表评论
-
Java程序员常用工具集
2012-05-23 14:30 947我发现很多人没办 ... -
基于JDBC的数据库连接池技术研究与设计
2011-12-16 14:34 733基于JDBC的数据库连接池技术研究与设计 摘 要 本文 ... -
关于jvm的设置
2011-12-16 10:38 1456一、Java heap space (一 ... -
JVM内存管理深入垃圾收集器与内存分配策略
2011-12-15 16:45 1090JVM内存管理深入垃圾收 ... -
jdbc 连接池小结
2011-12-15 16:43 852java基础面试题 主题:[我的工具箱] jXLS ... -
JVM参数调优
2011-12-15 14:35 780JVM参数调优是个很头痛 ... -
Java对象和JSON互转换利器-Gson
2011-11-04 17:22 1812Java对象和JSON互转换利器-Gson . 2008-07 ... -
java.lang.OutOfMemoryError: PermGen space及其解决方法
2011-10-26 17:52 777java.lang.OutOfMemoryError: Per ... -
java.sql.Date,java.sql.Time和java.sql.Timestamp
2011-09-06 14:11 1071java.sql.Date,java.sql.Time和jav ... -
java 编码
2011-07-21 19:13 1220w.write(new String("中文网&qu ... -
对泛型进行反射
2011-05-05 19:06 1195对泛型进行反射 今天在用反射的时候突然想到,之前从来没有对泛 ... -
Java反射经典实例 Java Reflection Cookbook
2011-05-05 19:05 738Java反射经典实例 Java Reflection Cook ... -
java 反射机制详解
2011-05-05 19:04 668java 反射机制详解 Java 的反射机制是使其具有动态特性 ... -
一次Java垃圾收集调优实战
2011-05-05 19:03 724一次Java垃圾收集调优实战 1 资料 * JDK5 ... -
利用反射和泛型让JDBC编程方便点
2011-05-05 19:02 805利用反射和泛型让JDBC编程方便点 一直以来使用JDBC编 ... -
利用反射取得泛型信息
2011-05-05 18:22 608利用反射取得泛型信息 一、传统通过反射取得函数的参数和返回值 ... -
深入剖析JAVA反射机制强大功能
2011-04-08 20:47 839* 深入剖 ... -
关于Java反射机制的一个实例
2011-04-08 20:46 797* 关于Java反射机制的一个实例 ... -
Java虚拟机内部构成浅析
2011-04-08 20:44 770* Java虚拟 ... -
详解reflect Java的反射机制
2011-04-08 20:42 503* 详解refle ...
相关推荐
1. JVM调优 1.1 JVM调优总结(一)-一些概念 1.2 JVM调优总结(二)-一些概念 1.3 JVM调优总结(三)-基本...4.1 JVM内存管理:深入Java内存区域与OOM 4.2 JVM内存管理:深入垃圾收集器与内存分配策略 4.3 深入理解JVM
JVM 内存管理之道 JVM垃圾回收机制 JVM GC组合 JVM 内存监控工具
java获得jvm内存大小
Sun JVM原理与内存管理
详细介绍了JVM 内存管理相关知识 内存空间( VM运行时数据区域) ◦ 内存结构 ◦ 内存空间 内存分配 内存回收(GC) 内存分析工具
性能测试,线程的 dump 看到线程的 死锁,等待 运行状态
对于深入掌握的java的人士,对于内存管理是不得不看的东西
主要是JVM内存分配及简单的JVM性能调优
JVM内存结构Java 代码是要运行在虚拟机上的,而虚拟机在执行 Java 程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。如果
JVM内存区域划分Java系列2021.pdf
sun公司出版的jvm运行机制管理丛书,需要深入jvm的同学可以下载来看看
JVM内存调优,java内存管理总结。包含新生代、老年代等详解。还有垃圾回收收集器详解。
JVM 深入学习教程深入分析JVM教程!jvm 内存原型,优化等等
(二)MATJVM 内存分析工具.MAT JVM 内存分析工具.MAT JVM 内存分析工具.(二)MATJVM 内存分析工具.MAT JVM 内存分析工具.MAT JVM 内存分析工具.
jvm内存反洗工具:
用java内存监控工具生成的JVM内存日志,用jmap生成的
JVM内存dump分析工具MAT独立安装包,分析内存溢出利器,可以准确定位内存异常原因,解决问题,MemoryAnalyzer-1.10.0.20200225.zip
JVM内存管理和垃圾回收 JVM内存管理和垃圾回收 JVM内存管理和垃圾回收
关于java的内存分配问题,jvm的运行原理相关资料总结
idea插件JVM内存工具JProfiler11,下载完,即可导入idea,可idea快捷打开使用。