JVM 内存区域模型

作者: Aiibai | 来源:发表于2018-12-17 10:28 被阅读17次
    1.运行时内存区域的划分

    java 虚拟机在执行 java 程序的过程中,会将它管理的内存区域划分为若干个不同的数据区域,这些区域有各自不同的用途,以及创建和销毁时间。

    image.png

    HotSpot 虚拟机并不区分虚拟机栈和本地方法栈,所以下面统称这两种内存区域叫 Java 栈

    2.每个内存区域的作用以及服务的对象
    • 程序计数器
      程序计数器是一块较小的内存空间,它可以看做是当前线程所执行字节码的行号指示器。如果线程正在执行的是一个 java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是 Native 方法,这个计数器的的值为空(Undefined)。

    • Java 虚拟机栈
      虚拟机栈描述的是 Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈。
      局部变量表存放了编译期可知的各种基本数据类型以及对象的引用。其中 64 位长度的 longdouble 类型的数据会占用 2 个局部变量空间,其余数据类型只占用 1 个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

    • 本地方法栈
      本地方法栈和虚拟机栈所发挥的作用是非常类似的,只不过虚拟机栈是为虚拟机执行 java 方法服务的,而本地方法栈是为虚拟机使用到的 Native 方法服务的。


    • 堆存放的是对象的实例,也是垃圾收集器管理的主要区域。根据 Java 虚拟机规范规定, Java 堆可以处于物理上不连续的内存空间,只要在逻辑上连续即可。

    • 方法区
      它用于存储被虚拟机加载的类信息,常量,静态变量以及即时编译器编译后的代码等。类似的,Java 虚拟机规范规定,方法区可以处于物理上不连续的内存空间,只要逻辑上连续就可以。另外,方法区可以选择不实现垃圾收集器,因为垃圾收集行为在这个区域比较少见,这个区域的内存回收目标主要是针对常量池和类型卸载。

    • 直接内存
      直接内存并不是虚拟机运行时数据区域,也不是 Java 虚拟机规范定义的内存区域,也叫做堆外内存。

    3.可能抛出的异常
    • 程序计数器
      唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

    • Java 栈(虚拟机栈和本地方法栈)
      有两种可能抛出的异常:
      OutOfMemoryError:如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时。(如:创建的线程太多)
      StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度时。(如:没有终止条件的递归调用)


    • OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时。

    • 方法区
      OutOfMemoryError:当方法区无法满足内存分配需求时。

    4.控制的参数
    • Java 堆
      -Xms 最小堆大小
      -Xmx 最大堆大小
      如果两个值相等,堆不能自动扩展,是固定大小。

    • Java 栈(虚拟机栈和本地方法栈)
      -Xss 栈大小

    -方法区
    -XX:PermSize 最小方法区大小
    -XX:MaxPermSize 最大方法区大小

    5.模拟异常

    • OutOfMemoryError
    /**
     * -Xms10m
     * -Xmx10m
     * */
    public class HeapOOM {
        public static void main(String[] args) {
            List<OOMObject> list = new ArrayList<>();
            while (true) {
                list.add(new OOMObject());
            }
        }
    
        static class OOMObject {
    
        }
    }
    
    

    运行结果

    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at java.util.Arrays.copyOf(Arrays.java:3210)
        at java.util.Arrays.copyOf(Arrays.java:3181)
        at java.util.ArrayList.grow(ArrayList.java:261)
        at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
        at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
        at java.util.ArrayList.add(ArrayList.java:458)
        at jvm.exception.HeapOOM.main(HeapOOM.java:13)
    

    解决这种溢出的思路是:首先确定是内存泄露还是内存溢出导致的异常。可以通过堆转储快照进行分析,如果是内存泄露导致的,就需要检查泄露的对象为什么不能被垃圾回收,也即寻找泄露对象与 GC Roots 的可达路径。如果是内存溢出查看


    • OutOfMemoryError
    /**
     * -Xss128k
     */
    public class StackOOM {
        public static void main(String[] args) {
            while (true) {
                new Thread(() -> {
                    while (true){
                        try {
                            Thread.sleep(5000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //...
                    }
                }).start();
            }
        }
    }
    

    执行结果:

    Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
        at java.lang.Thread.start0(Native Method)
        at java.lang.Thread.start(Thread.java:717)
        at jvm.exception.StackOverflow.main(StackOverflow.java:23)
    
    

    StackOverflowError

    /**
     * -Xss128k
     */
    public class StackOverflow {
        public static void main(String[] args) {
            test();
        }
        public static void test(){
            test();
        }
    }
    

    执行结果:

    Exception in thread "main" java.lang.StackOverflowError
        at jvm.exception.StackOverflow.test(StackOverflow.java:11)
        at jvm.exception.StackOverflow.test(StackOverflow.java:11)
        at jvm.exception.StackOverflow.test(StackOverflow.java:11)
        at jvm.exception.StackOverflow.test(StackOverflow.java:11)
    ....
    
    • 方法区
      OutOfMemoryError:可以用两种方式导致这种异常,增加更多的常量,创建更多的代理类。
    /**
     * -XX:PermSize=10M  -XX:MaxPermSize=10M
     */
    public class RuntimeConstantPoolOOM {
        public static void main(String[] args) {
            List<String> values = new ArrayList<>();
    
            int i=0;
            while (true){
                values.add(String.valueOf(i++).intern());
            }
        }
    }
    

    上面代码中的 intern 方法的作用是:判断是否存在需要的常量,如果存在就返回,如果不存在,则将当前值加入到常量池中。

    执行结果:
    发现设置了 -XX:PermSize和-XX:MaxPermSize 并不会导致方法区溢出。原来在 java8 中移除了这两个参数,原来的永久代(PermGen)概念也没有了,取而代之的是元空间(Metaspace)。不过它们也有一些区别:原先永生代中类的元信息会被放入本地内存(元数据区),将类的静态变量和内部字符串放入到java堆中。

    新的元空间设置参数:

    -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对改值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
    -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
    -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集。
    -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。

    总结起来,java8中对方法区的实现做了如下的变化:

    • 移除了永久代(PermGen),替换为元空间(Metaspace);
    • 永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
    • 永久代中的 interned Stringsclass static variables 转移到了 Java heap
    • 永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize

    那么,很明显上面的代码中的字符串常量是被放到了堆中了,增加参数-Xms10M和-Xmx10M,验证效果如下:

    Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    Disconnected from the target VM, address: '127.0.0.1:53648', transport: 'socket'
        at java.nio.CharBuffer.wrap(CharBuffer.java:373)
        at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:265)
        at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)
        at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)
        at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)
        at java.io.PrintStream.newLine(PrintStream.java:545)
        at java.io.PrintStream.println(PrintStream.java:807)
        at jvm.exception.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:17)
    Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize1=10M; support was removed in 8.0
    Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=10M; support was removed in 8.0
    

    在上面的结果中我们不仅看到了 OutOfMemoryError ,还有一句 GC overhead limit exceeded ,这句是什么意思的?
    GC overhead limt exceed 检查是 Hotspot VM 1.6 定义的一个策略,通过统计 GC 时间来预测是否要 OOM 了,提前抛出异常,防止 OOM 发生。Sun 官方对此的定义是:并行/并发回收器在GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存。用来避免内存过小造成应用不能正常工作。“

    /**
     * //-XX:PermSize=128k  -XX:MaxPermSize=128k
     * -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
     */
    public class JavaMethodAreaOOM {
        public static void main(String[] args) {
            while (true) {
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOMObject.class);
                enhancer.setUseCache(false);
                enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
                enhancer.create();
            }
        }
    
        static class OOMObject {
    
        }
    }
    
    

    执行结果:

    Error occurred during initialization of VM
    OutOfMemoryError: Metaspace
    
    6.对象访问
    Object obj = new Object();
    

    上面这行代码涉及到的内存区域有:方法区、堆、栈。
    Object obj 这部分的语义将反映到 Java 栈的本地变量表 中。
    new Object() 这部分的语义将反映到 Java 堆 中。
    另外,还需要类型数据(对象类型、父类、实现的接口、方法等),这部分数据储存在 方法区

    obj 中的引用代表的含义在不同的虚拟机中有不同的实现,主流的有两种:句柄和指针。

    • 句柄
      Java 堆 中将会划分出一块内存来作为句柄池,obj 中存储的是句柄地址,而句柄中包含对象的实例数据和类型数据的具体地址。
      image.png
    • 指针
      直接指针访问的方式中,类型数据的信息时存储在对象中, obj 中直接存储的是对象的地址。
      image.png

    这两种方式都有各自的优缺点
    在对象被移动式,只要修改句柄中的对象对象地址即可,而 obj 中的句柄地址不用改变。但是直接指针方法的速度更快,它节省的一次指针定位的时间开销。

    参考:
    https://blog.csdn.net/laomo_bible/article/details/83067810
    https://www.cnblogs.com/hucn/p/3572384.html

    相关文章

      网友评论

        本文标题:JVM 内存区域模型

        本文链接:https://www.haomeiwen.com/subject/vcrqkqtx.html