java大数据Java

深入理解Java虚拟机之图解Java内存区域与内存溢出异常

2022-01-13  本文已影响0人  Java弟中弟

Java内存区域与内存溢出异常

运行时数据区域

程序计数器

Java虚拟机栈

本地方法栈

Java堆

方法区

运行时常量池

直接内存

HotSpot虚拟机对象探秘

对象的创建

在类加载检查通过后,接下来虚拟机将为新生对象分配内存,而内存分配方式主要有两种:

对象的内存布局

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

对象的访问定位

Java程序会通过栈上的reference(一个指向对象的引用)数据来操作堆上的具体对象,具体的访问方式由虚拟机实现。

主流访问方式主要有两种:

实战OOM异常

采用不同的JDK及垃圾回收收集器均可能会产生不同的结果,以下实战均以JDK8,ParallelGC垃圾收集器为例运行代码

# 查看默认垃圾收集器VM参数
-XX:+PrintCommandLineFlags -version

Java堆溢出

只要不断创建对象实例,同时又避免垃圾收集器回收,这样达到最大堆容量限制后便能产生OOM异常

public class Hello {
    /**
     * -Xms:最小堆内存20M -Xmx:最大堆内存20M 两者设置一样避免自动扩展 
     * VM参数:-Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
     */
    public static void main(String[] args) {
        List<Hello> hellos = new ArrayList<>();
        while (true) {
            hellos.add(new Hello());
        }
    }
}

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

《Java虚拟机规范》明确允许Java虚拟机实现自行选择是否支持栈的动态扩展,而HotSpot虚拟机的选择是不支持扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常

public class Hello {
    /**
     * VM参数:-Xss128k
     */
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        // 递归调用方法,不断入栈
        stackLeak();
    }
    public static void main(String[] args) throws Throwable {
        Hello oom = new Hello();
        try {
            // 调用方法,入栈
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}
public class Hello {
    private static int stackLength = 0;

    public static void test() {
        // 局部变量多,栈帧增大
        long unused1, unused2, unused3, unused4, unused5,
                unused6, unused7, unused8, unused9, unused10,
                unused11, unused12, unused13, unused14, unused15,
                unused16, unused17, unused18, unused19, unused20,
                unused21, unused22, unused23, unused24, unused25,
                unused26, unused27, unused28, unused29, unused30,
                unused31, unused32, unused33, unused34, unused35,
                unused36, unused37, unused38, unused39, unused40,
                unused41, unused42, unused43, unused44, unused45,
                unused46, unused47, unused48, unused49, unused50,
                unused51, unused52, unused53, unused54, unused55,
                unused56, unused57, unused58, unused59, unused60,
                unused61, unused62, unused63, unused64, unused65,
                unused66, unused67, unused68, unused69, unused70,
                unused71, unused72, unused73, unused74, unused75,
                unused76, unused77, unused78, unused79, unused80,
                unused81, unused82, unused83, unused84, unused85,
                unused86, unused87, unused88, unused89, unused90,
                unused91, unused92, unused93, unused94, unused95,
                unused96, unused97, unused98, unused99, unused100;
        stackLength++;
        // 递归调用,不断入栈
        test();
        unused1 = unused2 = unused3 = unused4 = unused5 = unused6 = unused7 = unused8 = unused9 = unused10 
        = unused11 = unused12 = unused13 = unused14 = unused15 = unused16 = unused17 = unused18 = unused19 
        = unused20 = unused21 = unused22 = unused23 = unused24 = unused25 = unused26 = unused27 = unused28
        = unused29 = unused30 = unused31 = unused32 = unused33 = unused34 = unused35 = unused36 = unused37 
        = unused38 = unused39 = unused40 = unused41 = unused42 = unused43 = unused44 = unused45 = unused46 
        = unused47 = unused48 = unused49 = unused50 = unused51 = unused52 = unused53 = unused54 = unused55
        = unused56 = unused57 = unused58 = unused59 = unused60 = unused61 = unused62 = unused63 = unused64 
        = unused65 = unused66 = unused67 = unused68 = unused69 = unused70 = unused71 = unused72 = unused73 
        = unused74 = unused75 = unused76 = unused77 = unused78 = unused79 = unused80 = unused81 = unused82
        = unused83 = unused84 = unused85 = unused86 = unused87 = unused88 = unused89 = unused90 = unused91 
        = unused92 = unused93 = unused94 = unused95 = unused96 = unused97 = unused98 = unused99 = unused100 = 0;
    }

    public static void main(String[] args) {
        try {
            test();
        } catch (Error e) {
            System.out.println("stack length:" + stackLength);
            throw e;
        }
    }
}

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

public class Hello {
    /**
     * JDK8前VM参数: -XX:PermSize=6M -XX:MaxPermSize=6M
     * JDK8VM参数:-XX:MetaspaceSize=6M -XX:MaxMetaspaceSize=6M
     */
    public static void main(String[] args) {
        // 使用Set保持常量池引用,避免Full GC回收常量池行为
        Set<String> set = new HashSet<>();
        // 在short范围内足以让6M大小的PermSize(永久代,JDK8前有,JDK8及之后版本都已采用元空间替代)产生OOM了
        short i = 0;
        // JDK8前,抛出OOM异常
        // JDK8下,正常情况会进入死循环,并不会抛出任何异常
        while (true) {
            // String.intern()进入字符串常量池
            set.add(String.valueOf(i++).intern());
        }
    }
}

上述代码在JDK8环境下并不会抛出任何异常,这是因为字符串常量池已经被移至Java堆之中,控制方法区容量的大小对Java堆并没有什么影响

/**
 * JDK6:false false
 * JDK8:true  false
 */
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);
}
/*
 * 借助CGLib造成方法区溢出
 * VM参数:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
 */
public class Hello {
    public static void main(String[] args) {
        while (true) {
           // 创建CgLib增强对象
            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 {
    }
}

本机直接内存溢出

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

// 使用unsafe分配本机内存
public class Hello {
    // VM参数:-Xmx20M -XX:MaxDirectMemorySize=10M
    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);
        }
    }
}
上一篇 下一篇

猜你喜欢

热点阅读