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

异步磷火
• 阅读 632

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

运行时数据区域

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

程序计数器

  • 用于记录从内存执行的下一条指令的地址,线程私有的一小块内存,也是唯一不会报出OOM异常的区域

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

Java虚拟机栈

  • Java虚拟机栈(Java Virtual Machine Stack)是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

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

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

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

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

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

本地方法栈

  • 与Java虚拟机栈类似,只不过服务对象不一样,本地方法栈为虚拟机使用到的本地方法服务,Java虚拟机栈为虚拟机执行Java方法(字节码)服务

Java堆

  • 对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存
  • 当堆内存没有足够空间给对象实例分配内存并且堆内存无法扩展时都会抛出OOM异常

方法区

  • 方法区与Java堆类似,也是各个线程共享的区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据
  • 通常用别名“非堆”来与Java堆做区分
  • 当方法区没有足够空间满足内存分配要求时,也会抛出OOM异常

运行时常量池

  • 运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量与符号引用
  • 受方法区内存限制,当常量池无法再申请到内存时会抛出OOM异常

直接内存

  • 直接内存并不是运行时数据区的一部分,但它受总内存限制,也可能会出现OOM异常

HotSpot虚拟机对象探秘

对象的创建

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

  • 指针碰撞

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

  • 空闲列表

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

对象的内存布局

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

  • 对象头

    • 存储对象自身运行时数据(Mark Word),如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
    • 类型指针(对象指向其类型元数据的指针)
  • 实例数据

    • 对象真正存储的有效信息,即代码中的各类型字段内容
  • 对齐填充

    • 由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,即任何对象大小都是8字节的整数倍,故实例数据部分没有对齐的话需要对齐填充来充当占位符补全

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

对象的访问定位

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

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

  • 句柄

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

  • 直接指针

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

实战OOM异常

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

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

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

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虚拟机栈和本地方法栈溢出

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

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

  • 定义了大量的本地变量,增大此方法帧中本地变量表的长度(即调整栈帧大小)
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;
        }
    }
}

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

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

  • 方法区容量控制
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堆并没有什么影响

  • String.intern()方法介绍:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回常量池中这个字符串的String对象;否则,将此String对象包含的字符复制添加到常量池中,并返回此String对象的引用
/**
 * 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);
}
  • JDK6因为new StringBuilder()分配到的是Java堆内存,而String.intern()会把首次遇到的字符串复制到的是字符串常量池(方法区),所以都是false

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

  • JDK8因为字符串常量池都移动到了Java堆中,new StringBuilder()分配到Java堆内存后,字符串常量池也记录到了首次遇到的实例引用,那么String.intern()new StringBuilder()都是同一个了(true);而因为java字符串在sun.misc.Version类加载时已进入常量池,那么intern()方法就返回当前常量池的String对象,new StringBuilder()在堆中重新创建了一个,自然也就不一样了(false)

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

  • 方法区的主要职责是用于存放类型的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等,因此运行时产生大量的类填满方法区也可以造成方法区溢出
/*
 * 借助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 {
    }
}

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

本机直接内存溢出

直接内存(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);
        }
    }
}

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

参考资料

《深入理解Java虚拟机》(第三版) 第2章:Java内存区域与内存溢出异常
点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
java虚拟机(四)
 学习了java运行时数据区,知道每个内存区域保存什么数据,可以参考:https://www.cnblogs.com/huigelaile/p/diamondshine.html,然后了解内存溢出和内存泄露是很有必要的,一方面是为了面试,更重要是的在工作中能够快速定位错误原因并且解决内存溢出分类:1、java.lang.OutOf
红烧土豆泥 红烧土豆泥
4年前
(转载)Java内存区域(运行时数据区域)和内存模型(JMM) - czwbig
转载自:Java内存区域和内存模型是不一样的东西,内存区域是指Jvm运行时将数据分区域存储,强调对内存空间的划分。而内存模型(JavaMemoryModel,简称JMM)是定义了线程和主内存之间的抽象关系,即JMM定义了JVM在计算机内存(RAM)中的工作方式,如果我们要想深入了解Java并发编程,就要先理解好Java内存模型。Java
九路 九路
4年前
1 Java内存区域与内存溢出异常
1java虚拟机对内存的管理java虚拟机在执行java程序的时候把内存分为若干个不同的区,这些区各自有不同的用处,以及创建和销毁时间.有的区随着虚拟机的启动而启动,有的区则依赖用户线程的启动和结束而启动和结束.根据java虚拟机规范,java虚拟机将内存分为下面几个部分:如下图image(https://imghelloworld.o
Wesley13 Wesley13
3年前
Java 几种常见的OOM
Java虚拟机内存有好几个运行时数据区会有OOM的异常,如果能够区分根据报错区分出是哪些区域报出来的异常,会更便于定位问题,解决问题。1.Java堆溢出原因:由于不断创建对象实例,当对象数量达到了最大堆的容量限制后产生内存溢出异常。现象:java.lang.OutOfMemoryError:Javaheapspace解决方法:1)首
Easter79 Easter79
3年前
Tomcat中JVM内存溢出及合理配置
Tomcat本身不能直接在计算机上运行,需要依赖于硬件基础之上的操作系统和一个Java虚拟机。Tomcat的内存溢出本质就是JVM内存溢出,所以在本文开始时,应该先对JavaJVM有关内存方面的知识进行详细介绍。一、JavaJVM内存介绍JVM管理两种类型的内存,堆和非堆。按照官方的说法:“Java虚拟机具有一个堆,堆是运行时数据区域,
Stella981 Stella981
3年前
JVM内存区域划分
JVM内存区域划分一、JVM运行时数据区划分根据《Java虚拟机规范》JVM会把它管理的内存划分为若干个不同的数据区域,如下图所示:方法区、堆、栈(虚拟机栈、本地方法栈)、程序计数器。线程私有的意思是指,JVM每遇到一个新的线程就会为他们分配栈和程序计数器。!(https
Wesley13 Wesley13
3年前
Java内存区域与内存溢出异常
Java的内存管理是一个老生常谈的问题,虽然Java号称可以自动管理自己的内存,使程序员从内存管理的围墙解放出来,但是一连串的内存泄漏和溢出方面的问题,使得我们不得不去深入了解Java的内存管理机制。本篇文章将从Java的内存区域开始剖析Jvm的内存机制,阐述内存溢出异常产生的原因。运行时数据区域众说周知,Java程序是运行在Java虚拟机
Wesley13 Wesley13
3年前
Java运行时数据区域
两个名词的对比java内存模型定义了线程和主内存之间的抽象关系,即Jvm在计算机内存中的工作方式,控制线程之间的通信。java内存区域内存区域是指Jvm运行时将数据分区域存储,强调对内存空间的划分。今天主要总结一下Java运行时的数据区域Java运行时数据区域
Stella981 Stella981
3年前
JVM笔记二:Java内存区域
Java程序在虚拟机自动内存管理的机制的帮助下,不容易出现内存泄露和内存溢出问题,这也就要求程序员需要了解虚拟机处理内存的机制,以解决OOM问题。运行时数据区域!Java虚拟机运行时数据区(https://oscimg.oschina.net/oscnet/3755e1d9e9bf4068b2b3b77b4c0b6bf99b8.jpg)
Stella981 Stella981
3年前
JVM探秘3:内存溢出
在Java虚拟机内存区域中,除了程序计数器外,其他几个内存区域都可能会发生OutOfMemoryError,这次通过一些代码来验证虚拟机各个内存区域存储的内容。在实际工作中遇到内存溢出异常时,需要做到能根据异常信息快速判断是哪个内存区域的溢出,知道什么样的代码会导致这些区域内存溢出,并且知道出现内存溢出后如何处理。Java堆溢出Jav
Wesley13 Wesley13
3年前
Java 虚拟机垃圾收集机制详解
本文摘自深入理解Java虚拟机第三版垃圾收集发生的区域之前我们介绍过Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈三个区域随线程共存亡。栈中的每一个栈帧分配多少内存基本上在类结构确定下来时就已知,因此这几个区域的内存分配和回收都具有确定性,不需要考虑如何回收的问题,当方法结束或线程结
异步磷火
异步磷火
Lv1
是谁,在寒冷的冬天,掀开被单,冰冻爱情的温暖。
文章
3
粉丝
0
获赞
0