Java内存溢出的几个区域,注意避坑

发布于:2022-11-09 ⋅ 阅读:(7) ⋅ 点赞:(0) ⋅ 评论:(0)

在开发过程中,时常会遇到内存溢出的问题,有可能是在生产环境,有的就在开发中,今天就聊一聊内存溢出。

存在内存的区域:

  • Java堆溢出

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

  • 方法区和运行时常量池溢出

  • 本机内存溢出

1、Java堆溢出

Java堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。

1、案例创建

需要手动调节JVM参数,不然需要等很长时间:-Xms20m -Xmx20m

 public class JavaHeapDemo {
 
     static class OOMObject {
 
     }
     public static void main(String[] args) {
         List<OOMObject> list = new ArrayList<OOMObject>();
         //利用while循环不断创建对象
         while (true) {
             list.add(new OOMObject());
         }
     }
 }

2、处理方法

常规的处理方法是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析

  1. 分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)

  2. 内存泄漏:通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置

  3. 内存溢出:检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查 是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗

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

关于虚拟机栈和本地方法栈,在《Java虚拟机规范》中描述了两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。

  • 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常

《Java虚拟机规范》明确允许Java虚拟机实现自行选择是否支持栈的动态扩展,而HotSpot虚拟机的选择是不支持扩展,所以除非在创建

线程申请内存时就因无法获得足够内存而出现OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为

栈容量无法容纳新的栈帧而导致StackOverflowError异常

1、使用-Xss参数减少栈内存容量

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

这里可以通过指定参数-Xss128k,用来测试栈溢出的情况

3、方法区和运行时常量池溢出

HotSpot从JDK 7开始逐步“去永久代”的计划,并在JDK 8中完全使用元空间来代替永久代的背景故事,使用“永久代”还是“元空间”来

实现方法区,对程序有什么实际的影响。

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

String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。

这里测试需要JDK6:-XX:PermSize=6M -XX:MaxPermSize=6M

 public class RuntimeConstantPoolOOM {
 
     public static void main(String[] args) {
         // 使用Set保持着常量池引用,避免Full GC回收常量池行为
         Set<String> set = new HashSet<String>();
         // 在short范围内足以让6MB的PermSize产生OOM了
         short i = 0;
         while (true) {
             set.add(String.valueOf(i++).intern());
         }
     }
 }

JDK8模拟测试

 package jdk8;
 
 import java.io.File;
 import java.lang.management.ClassLoadingMXBean;
 import java.lang.management.ManagementFactory;
 import java.net.URL;
 import java.net.URLClassLoader;
 import java.util.ArrayList;
 import java.util.List;
 
 /**
  *
  * @ClassName:OOMTest
  * @Description:模拟类加载溢出(元空间oom)
  * 为了快速溢出,设置参数:-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=80m
  * @author diandian.zhang
  */
 public class OOMTest {
     public static void main(String[] args) {
         try {
             //准备url
             URL url = new File("D:/58workplace/11study/src/main/java/jdk8").toURI().toURL();
             URL[] urls = {url};
             //获取有关类型加载的JMX接口
             ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();
             //用于缓存类加载器
             List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
             while (true) {
                 //加载类型并缓存类加载器实例
                 ClassLoader classLoader = new URLClassLoader(urls);
                 classLoaders.add(classLoader);
                 classLoader.loadClass("ClassA");
                 //显示数量信息(共加载过的类型数目,当前还有效的类型数目,已经被卸载的类型数目)
                 System.out.println("total: " + loadingBean.getTotalLoadedClassCount());
                 System.out.println("active: " + loadingBean.getLoadedClassCount());
                 System.out.println("unloaded: " + loadingBean.getUnloadedClassCount());
             }
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
 }

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

在JDK 8以后,永久代便完全退出了历史舞台,元空间作为其替代者登场。在默认设置下,前面列举的那些正常的动态创建新类型的测试用例已经很难再迫使虚拟机产生方法区的溢出异常了。不过为了让使用者有预防实际应用里出现类似于代码清单2-9那样的破坏性的操作,HotSpot还是提供了一些参数作为元空间的防御措施,主要包括:

  • -XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。

  • -XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。

  • -XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:Max-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。

4、本机直接内存溢出

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

JVM参数:-Xmx20M -XX:MaxDirectMemorySize=10M

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

越过了DirectByteBuffer类直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法指定只有引导类加载器才会返回实

例,体现了设计者希望只有虚拟机标准类库里面的类才能使用Unsafe的功能,在JDK 10时才将Unsafe的部分功能通过VarHandle开放给

外部使用),因为虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而

是通过计算得知内存无法分配就会在代码里手动抛出溢出异常,真正申请分配内存的方法是Unsafe::allocateMemory()