java 什么时候会oom内存中哪些地方会报oom

Java中关于OOM的场景及解决方法
1、OOM for Heap=&例如:java.lang.OutOfMemoryError: Java
heap space
&此OOM是由于JVM中heap的最大值不满足需要,将设置heap的最大值调高即可,参数样例为:-Xmx2G
【解决方法】
调高heap的最大值,即-Xmx的值调大。
2、OOM for Perm=&例如:java.lang.OutOfMemoryError: Java
perm space
&此OOM是由于JVM中perm的最大值不满足需要,将设置perm的最大值调高即可,参数样例为:-XX:MaxPermSize=512M
【解决方法】
调高heap的最大值,即-XX:MaxPermSize的值调大。
另外,注意一点,Perm一般是在JVM启动时加载类进来,如果是JVM运行较长一段时间而不是刚启动后溢出的话,
很有可能是由于运行时有类被动态加载进来,此时建议用CMS策略中的类卸载配置。
如:-XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled
3、OOM for GC=&例如:java.lang.OutOfMemoryError: GC
overhead limit exceeded
此OOM是由于JVM在GC时,对象过多,导致内存溢出,建议调整GC的策略,在一定比例下开始GC而不要使用默认的策略,或者将新代和老代设置合适的大小,
需要进行微调存活率。
【解决方法】
改变GC策略,在老代80%时就是开始GC,并且将-XX:SurvivorRatio(-XX:SurvivorRatio=8)和-XX:NewRatio(-XX:NewRatio=4)设置的更合理。
4、OOM for native thread
created=&例如:java.lang.OutOfMemoryError: unable to
create new native thread
参考如下:
(MaxProcessMemory - JVMMemory - ReservedOsMemory) /
(ThreadStackSize) = Number of threads&
MaxProcessMemory&&
指的是一个进程的最大内存
JVMMemory&&&&&&&&
ReservedOsMemory&&
保留的操作系统内存
ThreadStackSize&&&&&
线程栈的大小
如果JVM内存调的过大或者可利用率小于20%,可以建议将heap及perm的最大值下调,并将线程栈调小,即-Xss调小,如:-Xss128k
【解决方法】
在JVM内存不能调小的前提下,将-Xss设置较小,如:-Xss:128k
5、OOM for allocate huge array=&例如:Exception in
thread "main": java.lang.OutOfMemoryError: Requested array size
exceeds VM limit&
此类信息表明应用程序(或者被应用程序调用的APIs)试图分配一个大于堆大小的数组。例如,如果应用程序new一个数组对象,大小为512M,但是最大堆大小为256M,因此OutOfMemoryError会抛出,因为数组的大小超过虚拟机的限制。
【解决方法】
(1)、首先检查heap的-Xmx是不是设置的过小
(2)、如果heap的-Xmx已经足够大,那么请检查应用程序是不是存在bug,例如:应用程序可能在计算数组的大小时,存在算法错误,导致数组的size很大,从而导致巨大的数组被分配。
&6、 OOM for small
swap=&例如:Exception in thread "main":
java.lang.OutOfMemoryError: request
&size& bytes for
&reason&. Out of swap
&抛出这类错误,是由于从native堆中分配内存失败,并且堆内存可能接近耗尽。这类错误可能跟应用程序没有关系,例如下面两种原因也会导致错误的发生:
(1)操作系统配置了较小的交换区
(2)系统的另外一个进程正在消耗所有的内存
&【解决方法】
(1)、检查os的swap是不是没有设置或者设置的过小
(2)、检查是否有其他进程在消耗大量的内存,从而导致当前的JVM内存不够分配。
注意:虽然有时&reason&部分显示导致OOM的原因,但大多数情况下,&reason&显示的是提示分配失败的源模块的名称,所以有必要查看日志文件,如crash时的hs文件。
已投稿到:
以上网友发言只代表其个人观点,不代表新浪网的观点或立场。1323人阅读
java开发(9)
在做服务器端开发的时候,经常会遇到服务由于内存溢出挂掉的情况,这种情况的发生一般来说是很难预期的,也比较难以重现,对于这种问题,一般可以通过记录内存溢出时候的堆信息来排查。
1、首先可以查看服务器运行日志以及项目记录的日志,捕捉到内存溢出异常。
2、如果程序挂掉了,但是没有找到任何这个操作的日志记录。这时查看一下/var/log/messages文件。messages 日志是核心系统日志文件。它包含了系统启动时的引导消息,以及系统运行时的其他状态消息。在messages里会出现以下信息:
造成这种情况的原因是因为,服务器以及项目日志都只能记录JVM内发生的内存溢出,也就是说heap(堆)的大小超出了JVM设置的大小。然而如果JVM设置的堆大小超出了操作系统允许的内存大小,那么操作系统会直接杀死进程,这种情况JVM就无法记录本次操作。
从图1中可以看出操作系统由于内存使用率过高,直接杀死了评分最高的进程,这里是java进程。Linux对于每个进程有一个OOM评分,这个评分在/proc/pid/oom_score文件中。例如/proc/8398/oom_score,如果不希望杀死这个进程,就将oom_adj内容改为-17。
3、在这种情况下首先需要调整JVM的heap大小,在运行参数中设置
&-Xms20m -Xmx20m&
将heap变小,使得JVM的OOM优先于操作系统的OOM出现。接着设置运行参数,在发生OOM的时候输出heapdump文件。
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/home/admin/logs/java.hprof
4、捕获到OOM异常的时候就会生成一个java.hprof文件。
产生OOM的程序示例:
public class OutofMemeorySample {&
&& &public static voidmain(String[] args) {&
&& &&&&headOutOfMemory();&
&& &static void headOutOfMemory(){&
&& &&& &long count= 0;&
&& &&& &try {&
&& &&&&&& &List&Object& objects = newArrayList&Object&();&
&& &&&&&& &while (true) {&
&& &&&&&& &&& &count++;&
&& &&&&&& &&& &objects.add(new Object());&
&& &&&&&& &}&
&& &&& &} catch(Throwable ex) {&
&& &&&&&& &System.out.println(count);&
&& &&&&&& &ex.printStackTrace();&
&& &&& &}&
5、在eclipse安装MAT工具。详情:
将输出的java.hprof文件导入MAT,如图2,可以看到发生OOM时候的heap状态。
点击Histogram可以看出所有heap中的对象列表,以及每类对象的数量,如图3。
右键对象,选择List Object-&with incoming reference,可以查看这个对象都被那些对象引用,从而查出为何对象没有被回收,如图4。
通过MAT工具查看可疑的类。再通过部分代码执行前和执行后的dump对比,就能找出没有释放的对象。
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:23710次
排名:千里之外
原创:16篇
(2)(1)(2)(2)(1)(1)(1)(1)(1)(1)(1)(4)(3)
(window.slotbydup = window.slotbydup || []).push({
id: '4740881',
container: s,
size: '200,200',
display: 'inlay-fix'4283人阅读
JAVA基础探讨(17)
Android技术初探(48)
本文内容来源于《深入理解Java虚拟机》一书,非常推荐大家去看一下这本书。最近开始看这本书,打算再开一个相关系列,来总结一下这本书中的重要知识点。呃呃呃,说好的那个图片请求框架呢~ &不要急哈,因为这个请求框架设计的内容还是比较广的,目前业余时间正在编写当中,弄好了之后就会放上来。在完成之前,咱还是先来学习一下其他知识。1、内存模型java虚拟机在执行java程序的过程中会把它说管理的内存划分为若干个不同的数据区域,如下图所示:图片来源于网络(1)程序计数器(Program Counter Register)& &&线程私有。程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复登记处功能都需要依赖这个计数器的值来完成。为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。这类内存区域称为“线程私有”的内存。& &&程序计数器,是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。(2)Java虚拟机栈& & 也是线程私有的,生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时,都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。平常我们把java分为堆内存和栈内存,其中的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中局部变量表部分。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。& & 对于java虚拟机栈,有两种异常情况:& & 1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;& & 2)如果虚拟机栈在动态扩展时,无法申请到足够的内存,就会抛出OutOfMemoryError;& &&& & &Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。因此我们也称Java虚拟机是基于栈的,这点不同于Android虚拟机,Android虚拟机是基于寄存器的。(3)本地方法栈(Native Method Stack)& &&线程私有。本地方法栈和虚拟机栈所发挥的作用非常相似,它们之间的区别主要是,虚拟机栈是为虚拟机执行Java方法(也就是字节码)服务的,而本地方法栈则为虚拟机使用到的Native方法服务。& & 与虚拟机栈类似,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。(4)Java堆(Java Heap)& &&所有线程共享。Java堆在虚拟机启动时创建,是Java虚拟机所管理的内存中最大的一块。Java堆的唯一目的就是存放对象实例和数组。& & Java堆是垃圾收集器管理的主要区域,因此也成为“GC堆”。从内存回收的角度来看,由于现在收集器大都采用分代收集算法,所以Java堆可以细分为:新生代和老年代;再细分一点:Eden空间、From Survivor空间、&To Survivor空间等。从内存分配角度来看,线程共享的Java堆可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。但是不管怎么划分,哪个区域,存储的都是对象实例。& & Java堆物理上不需要连续的内存,只要逻辑上连续即可。如果堆中没有内存完成实例分配,并且也无法再扩展时,将会抛出OutOfMemoryError异常。(5)方法区(Method Area)& &&所有线程共享。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也有一个别名叫做Non-Heap(非堆),用于与Java堆区分。对于HotSpot虚拟机来说,方法区又习惯称为“永久代”(Permancent Generation),但这只是对于HotSpot虚拟机来说的,其他虚拟机的实现上并没有这个概念。相对而言,垃圾收集行为在这个区域比较少出现,但也并非不会来收集,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载上。& &&& &&运行时常量池:& &&运行时常量池属于方法区。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面常量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。也就是说,这部分内容,在编译时只是放入到了常量池信息中,到了加载时,才会放到运行时常量池中去。运行时常量池县归于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区的运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用的比较多的是String类的intern()方法。& & 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常,常量池属于方法区,同样可能抛出OutOfMemoryError异常。(6)直接内存(Direct Memory)& &&直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁的使用,也可能会导致OutOfMemoryError异常。直接内存的分配不会受到Java堆大小的限制,但需要收到本机总内存的大小以及处理器寻址空间的限制。典型的一个使用直接内存的例子,就是JDK1.4中加入的NIO。关于NIO具体内容可看《》中的第六部分。内存区域模型小结:& & (1)线程私有的区域:程序计数器、虚拟机栈、本地方法栈;& & (2)所有线程共享的区域:Java堆、方法区;(注:直接内存不属于虚拟机内存模型的部分)& & (3)没有异常的区域:程序计数器;& & (4)StackOverflowError异常:Java虚拟机栈、本地方法栈;& & (5)OutOfMemoryError异常:除程序计数器外的其他四个区域,Java虚拟机栈、本地方法栈、Java堆、方法区;直接内存也会出现OutOfMemoryError。2、对象的创建、对象内存布局、对象的访问定位2.1 对象的创建过程 & &Java在语言层面,通过一个关键字new来创建对象。在虚拟机中,当遇到一条new指令后,将开始如下创建过程:& &&(1)判断类是否加载、解析、初始化& & 虚拟机遇到一条new指令时,先去检查这个指定的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那先执行相应的类加载过程。& &&(2)为新对象分配内存& & 前面说到,对象的内存分配是在Java堆中的。为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来,此时Java堆中的情况有两种可能,一种是Java堆中内存是绝对规整的,一种是Java堆中的内存并不是规整的。因此有两种分配方式:& & 1)Java堆内存是规整的,即所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,此时,分配内存仅需要把这个指针向空闲空间那边挪动一段与对象大小相等的距离,这种方式也称为“指针碰撞”(Bump the Pointer);& & 2)Java堆内存不是规整的,即已使用的内存和空闲的内存相互交错,就没有办法简单地进行指针的移动,此时的分配方案是,虚拟机必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的控件划分给对象实例,并更新列表上的记录,这种方式也称为“空闲列表”(Free List);& & 选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,对于Serial、ParNew等带Compact过程的垃圾收集器,系统采用的是指针碰撞算法;对于CMS这种基于Mark-Sweep算法的收集器,通常采用空闲列表算法。& &&(3)解决并发安全问题& & 确定了如何划分内存空间之后,还有一个问题就是,对象的创建在虚拟机中是非常频繁的行为,比如,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况,解决这种并发问题,一般有两种方案:& & 1)对分配内存空间的动作进行同步处理,比如,虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;& & 2)另一种方式是,把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定,虚拟机是否使用TLAB,可以通过-XX:+/-UserTLAB参数来设定。& &&(4)初始化分配到的内存空间& & 内存分配完成后,虚拟机将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作也可以提前至TLAB分配时进行。也正是这一步操作,才保证了我们对象的实例字段在Java代码中可以不赋初值就直接使用。注意,此时对象的实例字段全部为零值,并没有按照程序中的初值进行初始化。& &&(5)设置对象实例的对象头& & 上面工作完成后,虚拟机对对象进行必要的设置,主要是设置对象的对象头信息,比如,这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等...& &&& &&(6)初始化对象&init&方法& & 其实,上面工作完成后,从虚拟机角度来看,一个新的对象已经产生了,但从Java程序的角度来看,对象创建才刚刚开始,对象实例中的字段仅仅都为零值,还需要通过&init&方法进行初始化,把对象按照程序员的意愿进行初始化。此时,一个真正可用的对象才算完全产生出来。2.2 对象的内存布局 &经过前面的创建工作,一个对象已经成功产生,也已经在Java堆中分配好了内存。那这个对象在Java堆内存中到底是什么形态呢?又包括哪些部分呢?这就涉及到了对象的内存布局了。& & 不同的虚拟机实现中,对象的内存布局有差别,以最常用的HotSpot虚拟机为例,对象在内存中存储的布局分为3块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。& &&1)对象头:包含两部分信息,一部分是用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志等;另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。如果对象是一个Java数组,对象头中还有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组大小。& &&2)实例数据:真正存储对象有效信息的部分。也就是在程序中定义的各种类型的字段内容,包括从父类继承下来的,以及子类中定义的,都会在实例数据中记录。& &&3)对齐填充:不是必然存在的,仅起着占位符的作用,对于HotSpot来说,虚拟机的自动内存管理系统要求对象其实地址必须是8字节的整数倍,因此,如果对象实例数据部分没有对齐时,就需要通过对齐填充的方式来补全。2.3 对象的访问定位 &建立了对象是为了使用对象,我们对数据的使用是通过栈上的reference数据来操作堆上的具体对象,对于不同的虚拟机实现,reference数据类型有不同的定义,主要是如下两种访问方式:& &&1)使用句柄访问。此时,Java堆中将会划出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,如下图: & && &&2)使用直接指针访问。此时reference中存储的就是对象的地址。如下图:上面所说的,所谓对象类型,其实就是指,对象所属的哪个类。& &&上面两种对象访问方式各有优势,使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时,只会改变句柄中的实例数据指针,而reference本身不需要修改;使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销(根据上图,节省的是对象实例数据的指针定位),由于对象的访问在Java中非常频繁,因此,这类开销积少成多后也是一项非常可观的执行成本。对于HotSpot而言,选择的是第二种方式。3、常见的OOM和SOFOOM分为两种情况:内存溢出(Memory Overflow)和内存泄漏(Memory Leak)。OOM:OutOfMemoryError异常,& & 即内存溢出,是指程序在申请内存时,没有足够的空间供其使用,出现了Out Of Memory,也就是要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。& & 内存溢出分为上溢和下溢,比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。& &&& & 有时候内存泄露会导致内存溢出,所谓内存泄露(memory leak),是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,举个例子,就是说系统的篮子(内存)是有限的,而你申请了一个篮子,拿到之后没有归还(忘记还了或是丢了),于是造成一次内存泄漏。在你需要用篮子的时候,又去申请,如此反复,最终系统的篮子无法满足你的需求,最终会由内存泄漏造成内存溢出。& & 遇到的OOM:& & (1)Java Heap 溢出& & Java堆用于存储对象实例,我们只要不断的创建对象,而又没有及时回收这些对象(即内存泄漏),就会在对象数量达到最大堆容量限制后产生内存溢出异常。& & (2)方法区溢出& &方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。异常信息:java.lang.OutOfMemoryError:PermGen space方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。SOF:StackOverflow(堆栈溢出)& &&当应用程序递归太深而发生堆栈溢出时,抛出该错误。因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。& & 栈溢出的原因:& & (1)递归调用& & (2)大量循环或死循环& & (3)全局变量是否过多& & (4)数组、List、Map数据过大(注:文章中图片来源于网络)
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:880443次
积分:6906
积分:6906
排名:第3460名
原创:59篇
转载:33篇
评论:266条
文章:12篇
阅读:96277
文章:11篇
阅读:33858
文章:14篇
阅读:80859
(1)(10)(12)(11)(1)(4)(2)(1)(1)(1)(2)(3)(3)(8)(7)(8)(2)(4)(6)(1)(4)
(window.slotbydup = window.slotbydup || []).push({
id: '4740887',
container: s,
size: '250,250',
display: 'inlay-fix'JVM内存管理:深入Java内存区域与OOM - 高级语言虚拟机 - ITeye知识库频道
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堆存放的是对象实例,因此只要不断建立对象,并且保证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 [ 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();
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
运行结果:
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() {
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 oom 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信