zl程序教程

您现在的位置是:首页 >  IT要闻

当前栏目

《深入理解Java虚拟机》读书笔记(三)

2023-02-19 12:20:04 时间

实战:OutOfMemoryError异常

Java堆内存溢出时的快照.png

Java堆溢出

Java堆用于存储对象实例,只要不断的创建对象并且保证GC Roots到对象之间有可达路径来避免垃圾回收,就可以触发Java堆的内存溢出异常

控制Java堆的扩展容量可以通过参数-Xms和-Xmx来设置,为更方便的获取到内存溢出时的内存快照数据可以使用参数-XX:+HeapDumpOnOutOfMemoryError

  • 代码示例
import java.util.ArrayList;
import java.util.List;

/**
 * Java堆内存溢出异常测试
 * {@link 《深入理解Java虚拟机》第三版 代码清单2-3}
 * VM Args:-Xms2m -Xmx2m -XX:+HeapDumpOnOutOfMemoryError
 * 代码在JDK1.8下运行
 */
public class HeapOOM {

    static class OOMObject {
    }

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}
  • 异常堆栈
Java堆内存溢出异常测试期望结果.png
Java堆内存溢出异常测试实际结果.png
  • 关于GC overhead limit exceeded

运行结果和书中描述的异常堆栈不一致,异常堆栈描述中出现了GC overhead limit exceeded信息

Oracle官方给出了这个错误产生的原因和解决方法:

Exception in thread thread_name: java.lang.OutOfMemoryError: GC Overhead limit exceeded Cause: The detail message "GC overhead limit exceeded" indicates that the garbage collector is running all the time and Java program is making very slow progress. After a garbage collection, if the Java process is spending more than approximately 98% of its time doing garbage collection and if it is recovering less than 2% of the heap and has been doing so far the last 5 (compile time constant) consecutive garbage collections, then a java.lang.OutOfMemoryError is thrown. This exception is typically thrown because the amount of live data barely fits into the Java heap having little free space for new allocations. Action: Increase the heap size. The java.lang.OutOfMemoryError exception for GC Overhead limit exceeded can be turned off with the command line flag -XX:-UseGCOverheadLimit.

GC overhead limit exceeded,是JDK6新增的一个错误类型,根据官方的描述,这种错误类型描述了这样一种情形:Java虚拟机使用了98%的时间做GC,却只得到了2%的可用内存,以至于最终无内存可用,抛出了OutOfMemoryError

Oracle官方提供了-XX:-UseGCOverheadLimit参数禁用此类检查,使得异常堆栈中不再出现GC overhead limit exceeded信息;因此,为复现书中结果,可以选择加上此参数(注:这并不是一种解决方案,而只是关闭了一类错误类型的开关,根治还是要从代码检查和内存占用去实际分析)

  • 对内存溢出时的快照
Java堆内存溢出时的快照.png

从快照数据中,可以看出造成此次内存溢出的原因:频繁创建且存活的对象

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

在Java虚拟机规范中,对虚拟机栈和本地方法栈描述了两种异常,同时允许Java虚拟机实现自行选择是否支持栈的动态扩展

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

HotSpot虚拟机并不区分虚拟机栈和本地方法栈,同时,HotSpot虚拟机并不支持栈的动态扩展,所以除非在创建线程申请内存时就因为无法获得足够内存而出现OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常

控制栈容量通过参数-Xss来设置

  • 代码示例一:无法容纳新的栈帧而栈溢出
/**
 * 虚拟机栈和本地方法栈测试
 * {@link 《深入理解Java虚拟机》第三版 代码清单2-4}
 * VM Args:-Xss128k
 * 代码在JDK1.8下运行
 */
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;
        }
    }
}
  • 异常堆栈
栈容量的最小值.png

栈容量的配置,在不同版本的Java虚拟机和不同的操作系统,会有不同的栈容量最小值限制,此处堆栈信息表示最小配置640k,遂更改JVM参数为-Xss640k

无法容纳新的栈帧而栈溢出.png
  • 代码示例二:无法容纳新的栈帧而栈溢出,同样的代码,增加了本地变量,异常出现时输出的堆栈深度相应缩小
/**
 * 虚拟机栈和本地方法栈测试
 * {@link 《深入理解Java虚拟机》第三版 代码清单2-5}
 * VM Args:-Xss128k
 * 代码在JDK1.8下运行
 */
public class JavaVMStackSOF {

    private int stackLength = 1;

    public void stackLeak() {
        String s1 = "";
        stackLength ++;
        stackLeak();
        s1 = stackLength + "Exception in thread thread_name: java.lang.OutOfMemoryError: GC Overhead limit exceeded Cause: The detail message \"GC overhead limit exceeded\" indicates that the garbage collector is running all the time and Java program is making very slow progress. After a garbage collection, if the Java process is spending more than approximately 98% of its time doing garbage collection and if it is recovering less than 2% of the heap and has been doing so far the last 5 (compile time constant) consecutive garbage collections, then a java.lang.OutOfMemoryError is thrown. This exception is typically thrown because the amount of live data barely fits into the Java heap having little free space for new allocations. \n" +
                "Action: Increase the heap size. The java.lang.OutOfMemoryError exception for GC Overhead limit exceeded can be turned off with the command line flag -XX:-UseGCOverheadLimit."
             + stackLength + "Exception in thread thread_name: java.lang.OutOfMemoryError: GC Overhead limit exceeded Cause: The detail message \"GC overhead limit exceeded\" indicates that the garbage collector is running all the time and Java program is making very slow progress. After a garbage collection, if the Java process is spending more than approximately 98% of its time doing garbage collection and if it is recovering less than 2% of the heap and has been doing so far the last 5 (compile time constant) consecutive garbage collections, then a java.lang.OutOfMemoryError is thrown. This exception is typically thrown because the amount of live data barely fits into the Java heap having little free space for new allocations. \n" +
                "Action: Increase the heap size. The java.lang.OutOfMemoryError exception for GC Overhead limit exceeded can be turned off with the command line flag -XX:-UseGCOverheadLimit.";
    }

    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;
        }
    }
}
  • 异常堆栈
增加本地变量栈溢出时的深度缩小.png
  • 代码示例三:创建线程申请内存时不足导致OutOfMemoryError
/**
 * 虚拟机栈和本地方法栈测试
 * {@link 《深入理解Java虚拟机》第三版 代码清单2-6}
 * VM Args:-Xss640k
 * 代码在JDK1.8下运行
 */
public class JavaVMStackOOM {

    private void dontStop() {
        while (true) {
        }
    }

    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(() -> {
                dontStop();
            });
            thread.start();
        }
    }

    public static void main(String[] args) {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}
  • 异常堆栈

操作系统分配给每个进程的内存是有限制的,譬如32位Windows的单个进程最大内存限制为2GB。HotSpot虚拟机提供了参数可以控制Java堆和方法区这两部分的内存的最大值,那剩余的内存即为2GB减去最大堆容量,再减去最大方法区容量,由于程序计数器消耗内存很小,可以忽略掉,如果把直接内存和虚拟机进程本身耗费的内存也去掉的话,剩下的内存就由虚拟机和本地方法栈来分配了。因此为每个线程分配到的栈内存越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽

创建线程申请内存时不足导致栈空间的OutOfMemoryError.png

如果是建立过多线程导致的内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程

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

方法区的主要职责是用于存放类型的相关信息,如类名、访问修饰符、运行时常量池、字段描述、方法描述等;对于这部分的测试,一个是利用String包中的intern()方法往运行时常量池中不断添加常量直到溢出,另一个是运行时产生大量的类来填满方法区直到溢出

JDK8之前,通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小

  • 代码示例一:运行时常量池导致内存溢出
import java.util.HashSet;
import java.util.Set;

/**
 * 虚拟机栈和本地方法栈测试
 * {@link 《深入理解Java虚拟机》第三版 代码清单2-7}
 * VM Args:-XX:PermSize=2M -XX:MaxPermSize=2M
 * 代码在JDK1.6下运行
 */
public class RuntimeConstantPoolOOM {

    public static void main(String[] args) {
        Set<String> set = new HashSet<>();
        short i = 0;
        while (true) {
            set.add(String.valueOf(i ++).intern());
        }
    }
}
运行时常量池导致内存溢出.png
  • 代码示例二:操作字节码运行时生成大量动态类导致内存溢出
import java.util.HashSet;
import java.util.Set;

/**
 * 虚拟机栈和本地方法栈测试
 * {@link 《深入理解Java虚拟机》第三版 代码清单2-9}
 * VM Args:-XX:PermSize=2M -XX:MaxPermSize=2M
 * 代码在JDK1.7下运行
 */
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 OOMObejct {
    }
}

元空间

JDK8以后,永久代已经由元空间替代,已经很难迫使虚拟机产生方法区的溢出异常了,不过,HotSpot还是提供了以下参数作为元空间的防御措施:

  • -XX:MaxMetaspaceSize:设置元空间的最大值,默认是-1,不限制
  • -XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少空间,那么在不超过-XX:MaxMetaspaceSize的情况下,适当提高该值
  • -XX:MinMetaspaceFreeRatio:在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集频率。类似的还有-XX:MaxMetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比

本机直接内存溢出

直接内存的容量大小可通过-XX:MaxDirectMemorySize参数来指定,默认与Java堆最大值(-Xmx)一致

  • 代码示例
import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * 虚拟机栈和本地方法栈测试
 * {@link 《深入理解Java虚拟机》第三版 代码清单2-9}
 * VM Args:-Xmx2m -XX:MaxDirectMemorySize=1m
 * 代码在JDK1.8下运行
 */
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);
        }
    }
}
  • 异常堆栈
直接内存溢出.png