出现outofmemoryerror的原因,java出现outofmemory

  出现outofmemoryerror的原因,java出现outofmemory

  00-1010 1.简介2。代码练习1。Java堆溢出2。虚拟机堆栈和本地方法堆栈溢出3。运行时常量池溢出4。方法区溢出5。本机直接内存溢出3。JVM 4的常用启动参数。面试问题5。总结在Java虚拟机规范的描述中,除了程序计数器之外,OutOfMemoryError发生在虚拟机内存的其他几个运行时区域(以下简称OOM)本博客结合《深入理解Java虚拟机》一书编写。如果你有兴趣,跟边肖学吧!

  这篇文章和上一篇文章写的Java内存区域的划分有很大关系。如果不太了解Java内存区的划分,建议了解一下,不然这篇文章读起来会很痛苦。

  00-1010本节的目的有两个。

  首先,用代码验证Java虚拟机规范中描述的每个运行时区域存储的内容;其次,希望读者在工作中遇到实际的内存溢出异常时,能够根据异常信息快速判断出是哪个区域的内存溢出,知道什么样的代码可能导致这些区域的内存溢出,以及如何处理这些异常。在下面这段代码的开头,对执行过程中需要设置的虚拟机启动参数(注释中的参数后跟“VM Args”)都做了注释。这些参数对实验结果有直接影响。请不要在调试代码时忽略它们。(本文中的所有案例都经过了1.8版本的测试)

  如果读者使用控制台命令执行程序,可以直接写在Java命令之后。如果读者使用Eclipse IDE,他们可以在Debug/Run选项卡中设置它。

  

目录

 

  

一、简言

Java堆用于存储对象实例。只要我们不断地创建对象,保证GC根和对象之间存在可达路径,防止垃圾收集机制清除这些对象,当对象数量达到最大堆的容量限制时,就会发生内存溢出异常。

 

  将Java堆大小设置为20MB,不能扩展(将堆的minimum -Xms参数设置为与maximum -Xmx参数相同,以避免堆自动扩展)。通过参数-xx3360 heapDumppoontoffmemoryerror,虚拟机可以转储出当前的内存堆转储快照,供以后分析使用(内存堆转储快照是指溢出后内存中对象的占用情况)。

  我正在使用ider:

  设置参数:Xms:最小堆内存Xmx:最大可扩展内存xx3360 heapDumppoontof memory error:发生内存溢出异常时,虚拟机可以转储当前堆转储快照,以供以后分析。

  -Xms20m-Xmx20m-XX: HeapDumpOnOutOfMemoryError

  导入Java . util . ArrayList;导入Java . util . list;public class HeapOOM { static class OOMObject { } public static void main(String[]args){ ListOOMObject list=new ArrayList();while(true){ list . add(new oom object());}}运行结果:

  生成此报告是因为设置了-xx3360 heapdumpponoutofmemory error参数。您可以查看对象占用的内存。

  Java堆内存OOM异常是实际应用中最常见的内存溢出异常。当Java堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”后面会跟随一个进一步的提示“Java heapspace”。

  解决这方面的异常,一般的方法是先用内存镜像分析工具(如Dier的EclipseMemory Analyzer和jprofiler)分析转储快照,重点是确认内存中的对象是否必要,即区分是否存在内存泄漏或内存溢出。

  如果是内存泄漏,可以通过工具进一步检查泄漏对象到GC根的引用链。这样我们就能找到漏洞。

  对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置。

  如果不存在泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

  后面我会专门写一篇关于内存分析工具的博客,XX:+HeapDumpOnOutOfMemoryError这个只是有内存占用情况,工具可以帮我们看到对象的引用链情况。

  

 

  

2、虚拟机栈和本地方法栈溢出

由于在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常。注意:HotSpot虚拟机的栈容量是不可以动态扩展的。

 

  如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常允许栈空间动态扩展时,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常

public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + oom.stackLength); throw e; } }}

当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在JAVA虚拟机栈中当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在JAVA虚拟机的栈中,该变量所指向的对象是放在堆类存中的。实验结果表明:在单个线程下,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。换成远古时代的Classic虚拟机,这款虚拟机可以支持动态扩展 栈内存的容量,这时候就会报StackOverflowError异常了。

 

  也就是当我设置-Xss128k和不设置都是报同样的错误,并没有出现内存溢出异常,原因就是 HotSpot虚拟机的栈容量是不可以动态扩展的,但是值得注意的是我的电脑是16G运行内存的,当我设置-Xss128k的时候输出的长度是将近1000,当我不限制-Xss128k大小的时候输出的长度是20000左右,也就意味着每个线程的栈帧大小默认最大是2MB

  如果测试时不限于单线程,通过不断地建立线程的方式倒是可以产生内存溢出异常,在这种情况下,给每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。

  原因其实不难理解,操作系统分配给每个进程的内存是有限制的,譬如32位Windows的单个进程 最大内存限制为2GB。HotSpot虚拟机提供了参数可以控制Java堆和方法区这两部分的内存的最大值。那么虚拟机栈和本地方法栈内存如下:

  

虚拟机栈和本地方法栈内存=2GB-最大堆容量-最大方法区容量-程序计数器容量

 

  

因此为每个线程分配到的栈内存越大,可以建立的线程数量自 然就越少,建立线程时就越容易把剩下的内存耗尽。

 

  通过上面了解到,出现StackOverflowError异常时有错误堆栈可以阅读,相对来说,比较容易找到问题的所在(一般出现死循环可能会导致)。

  如果是建立过多线程导致的内存溢出,而不是栈溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。如果没有这方面的经验,这种通过减少内存的手段来解决内存溢出的方式会比较难以想到。

  

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(); }}

注意 重点提示一下,如果读者要尝试运行上面这段代码,记得要先保存当前的工作,由于在 Windows平台的虚拟机中,Java的线程是映射到操作系统的内核线程上,无限制地创建线程会对操 作系统带来很大压力,上述代码执行时有很高的风险,可能会由于创建线程数量过多而导致操作系统 假死(电脑可能直接死机)。

 

  在32位操作系统下的运行结果:原因:32位有进程大小内存限制。

  

Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread

注意:如果要测试上面内存溢出代码,记住先保存当前的工作,避免电脑卡死带来的麻烦。

 

  

 

  

3、运行时常量池溢出

由于运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放到一起进行。前面曾经 提到HotSpot从JDK 7开始逐步去永久代的计划,并在JDK 8中完全使用元空间来代替永久代,在此我们就以测试代码来观察一下,使用永久代还是元空间来实现方法区,对程序有什么 实际的影响。

 

  String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的 字符串,则返回代表池中这个字符串的String对象的引用否则,会将此String对象包含的字符串添加 到常量池中,并且返回此String对象的引用

  

import java.util.ArrayList;import java.util.List;public class RuntimeConstantPoolOOM { public static void main(String[] args) { // 使用List保持着常量池引用,避免Full GC回收常量池行为 List<String> list = new ArrayList<>(); // 10MB的PerSize在integer范围内足够产生00M int i = 0; while (true){ list.add(String.valueOf(i++).intern()); } }}

JDK7及以前(了解):-XX:PermSize设置永久代初始大小。-XX:MaxPermSize设置永久代最大可分配空间。(JDK7目前已经很少用了,这两个参数在JDK8及以后已经没有了,所以不必掌握,了解一下)JDK8及以后:可以使用-XX:MetaspaceSize和-XX:MaxMetaspaceSize设置元空间初始大小以及最大可分配大小。使用JDK 7或更高版本的JDK来运行这段程序并不会得到相同的结果,无论是在JDK 7中继续使 用-XX:MaxPermSize参数或者在JDK 8及以上版本使用-XX:MaxMeta-spaceSize参数把方法区容量同 样限制在6MB,都不会出现溢出异常,循环将一直进行下去,永不停歇。出现这种变 化,是因为自JDK 7起,原本存放在永久代的字符串常量池被移至Java堆之中,所以在JDK 7及以上版 本,限制方法区的容量对该测试用例来说是毫无意义的

 

  在JDK1.7中(包括1.7以上)常量池存储的不再是对象,而是对象引用,真正的对象是存储在堆中的。把RuntimeConstantPoolOOM.java运行时的VM参数改为如下(设置堆大小)所示:

  

-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

运行结果:

 

  

 

  查看生成的堆内存快照:

  

 

  

 

  

4、方法区溢出

方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这个区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。虽然直接使用Java SE API也可以动态产生类(如反射时的GeneratedConstructorAccessor 和动态代理等),但在本次实验中操作起来比较麻烦。借助CGLib直接操作字节码运行时,生成了大量的动态类。

 

  值得特别注意的是,我们在这个例子中模拟的场景并非纯粹是一个实验,这样的应用经常会出现在实际应用中:当前的很多主流框架,如Spring和Hibernate对类进行增强时,都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载人内存

  测试示例:

  

import org.springframework.cglib.proxy.Enhancer;import org.springframework.cglib.proxy.MethodInterceptor;import org.springframework.cglib.proxy.MethodProxy;import java.lang.reflect.Method;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() { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invokeSuper(obj, args); } }); enhancer.create(); } } static class OOMObject { }}

设置元空间最大空间,和初始化空间参数:类信息是都存在方法区的,方法区在jdk1.8将永久区改为了元空间。自此以后,常量池在元空间都是存储的引用。实际对象是在堆中。

 

  

-XX:MaxMetaspaceSize=10m -XX:MetaspaceSize=10m

运行结果:

 

  

 

  方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,要达成的条件是比 较苛刻的。在经常运行时生成大量动态类的应用场景里,就应该特别关注这些类的回收状况。这类场 景除了之前提到的程序使用了CGLib字节码增强和动态语言外,常见的还有:大量JSP或动态产生JSP 文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同 的加载器加载也会视为不同的类)等。

  

 

  

5、本机直接内存溢出

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

 

  直接内存:可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用`进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据(但是有一点注意,虽然不占用堆内存,但是他占用了服务器内存)。

  直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不 去指定,则默认与Java堆最大值(由-Xmx指定)一致。

  代码示例:

  越过了DirectByteBuffer类直接通 过反射获取Unsafe实例进行内存分配Unsafe类的getUnsafe()方法指定只有引导类加载器才会返回实 例,体现了设计者希望只有虚拟机标准类库里面的类才能使用Unsafe的功能,在JDK 10时才将Unsafe 的部分功能通过VarHandle开放给外部使用),因为虽然使用DirectByteBuffer分配内存也会抛出内存溢 出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配就会 在代码里手动抛出溢出异常,真正申请分配内存的方法是Unsafe::allocateMemory()

  

import sun.misc.Unsafe;import java.lang.reflect.Field;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); } }}

运行参数:

 

  

-Xmx20M -XX:MaxDirectMemorySize=10M -XX:+HeapDumpOnOutOfMemoryError

运行结果:

 

  

 

  我设置了-XX:+HeapDumpOnOutOfMemoryError发现运行完成之后并没有发现有内存快照。

  由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常 情况,如果读者发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了 DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了。

  

 

  

三、JVM常用的启动参数

堆:

 

  -Xms3550m:设置JVM初始内存为3550M。表示初始化JAVA堆的大小及该进程刚创建出来的时候,他的专属JAVA堆的大小,一旦对象容量超过了JAVA堆的初始容量,JAVA堆将会自动扩容到-Xmx大小。-Xmx3550m:设置JVM最大可用内存为3550M。表示java堆可以扩展到的最大值,在很多情况下,通常将-Xms和-Xmx设置成一样的,因为当堆不够用而发生扩容时,会发生内存抖动影响程序运行时的稳定性。栈:

  -Xss128k:规定了每个线程虚拟机栈及堆栈的大小,一般情况下,256k是足够的,此配置将会影响此进程中并发线程数的大小(和堆是不一样的,不支持动态扩展)。方法区:

  JDK7及以前(了解):-XX:PermSize设置永久代初始大小。-XX:MaxPermSize设置永久代最大可分配空间。(JDK7目前已经很少用了,这两个参数在JDK8及以后已经没有了,所以不必掌握,了解一下)-XX:MaxMetaspaceSize=10m:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存 大小。-XX:MetaspaceSize=10m:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集 进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放 了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。-XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可 减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:Max-MetaspaceFreeRatio,用于控制最 大的元空间剩余容量的百分比。内存:

  -XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后进行分析(内存堆转储快照 指的是溢出后,内存当中的对象占用情况)GC:

  -XX:-PrintGCDetails:每次GC时打印详细信息。

 

  

四、面试题

public static void main(String[] args) { String str1 = new StringBuilder("计算机").append("软件").toString(); System.out.println(str1.intern() == str1); String str2 = new StringBuilder("ja").append("va").toString(); System.out.println(str2.intern() == str2);}

这段代码在JDK 6中运行,会得到两个false,而在JDK 7中运行,会得到一个true和一个false。在jdk1.8运行也是,true、false。

 

  

产 生差异的原因是,在JDK 6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池 中存储,返回的也是永久代里面这个字符串实例的引用,而由StringBuilder创建的字符串对象实例在 Java堆上,所以必然不可能是同一个引用,结果将返回false。

 

  

而JDK 7(以及部分其他虚拟机,例如JRockit)的intern()方法实现就不需要再拷贝字符串的实例 到永久代了,既然字符串常量池已经移到Java堆中,那只需要在常量池里记录一下首次出现的实例引 用即可,因此intern()返回的引用和由StringBuilder创建的那个字符串实例就是同一个。而对str2比较返 回false,这是因为java这个字符串在执行String-Builder.toString()之前就已经出现过了,字符串常量 池中已经有它的引用,不符合intern()方法要求首次遇到的原则,计算机软件这个字符串则是首次 出现的,因此结果返回true。(这块说实话不好理解,说白了就是java是个特殊的字符串,他在常量池里面就一直存在)

 

  

总结:在1.8之后通过intern()添加到常量池,只有字符串在常量池不存在的时候才会返回字符串的引用。

 

  

 

  

五、总结

到此为止,我们明白了虚拟机里面的内存是如何划分的,哪部分区域、什么样的代码和操作可能 导致内存溢出异常。虽然Java有垃圾收集机制,但内存溢出异常离我们并不遥远,本章只是讲解了各 个区域出现内存溢出异常的原因,下一章将详细讲解Java垃圾收集机制为了避免出现内存溢出异常都 做了哪些努力。

 

  到此这篇关于Java实战之OutOfMemoryError异常的文章就介绍到这了,更多相关javaOutOfMemoryError异常内容请搜索盛行IT以前的文章或继续浏览下面的相关文章希望大家以后多多支持盛行IT!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: