《深入理解Java虚拟机》学习笔记第2章

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

2.1 概述

Java程序员把内存控制权交给Java虚拟机。

2.2 运行时数据区域

Java虚拟机运行时数据区
Java虚拟机在执行Java程序的过程中会把所管理的区域划分成若干块,各自有不同用途,其中堆和方法区是线程共享的,虚拟机栈,本地方法区和程序计数器都是线程私有的。

2.2.1 程序计数器

程序计数器是一块较小的内存区域,作为当前线程所执行的字节码的行号指示器。对于Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是Natvie方法,计数器为空。
此区域是唯一一个在Java虚拟机规范里没有规定任何OOM(OutOfMemeoryError)情况的区域。
线程私有。

2.2.2 Java虚拟机栈

生命周期与线程相同;
每个方法被执行时都会常见一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口信息等。每一个方法被调用直至执行完成的过程,就对应着一个栈在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期可知的各种数据类型(8种基础数据类型)、引用对象和returnAddress类型(指向了一条字节码指令的地址)。它所需的内存是在编译期完成分配,方法运行期间不会改变。
虚拟机栈会产生两种异常:当请求的栈深大于虚拟机允许的栈深,会抛出StackOverflowError;当虚拟机栈拓展到无法再申请内存时,会抛出OOM异常。
线程私有。

2.2.3 本地方法栈

与虚拟机栈类似,虚拟机栈为Java方法服务,本地方法栈为Native服务,有些版本的虚拟机会把两者合并在一起。
线程私有。

2.2.4 Java堆

Java堆在虚拟机启动时创建,用于存放对象实例,是垃圾收集器管理的主要区域。它可以是物理上不连续的内存空间,但是逻辑上是连续的。
线程共享。

2.2.5 方法区

方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码数据,物理上是堆的一部分。
当方法区无法满足内存分配的需求时,将抛出OOM异常。
线程共享。
运行时常量池
运行时常量池时方法区的一部分。有一类信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容在类加载后存放在运行时常量池,运行期间也可以将新的常量放入池子中。

2.2.6 直接内存

直接内存并不是虚拟机的一部分,到那时被频繁使用。和NIO类相关。本身收到机器的内存大小限制,所以也会出现OOM

2.3 对象访问

我们以下面这句代码来观察对象访问是如何进行的

Object obj = new Object();
  • Object object 反应到Java栈的本地变量表中,作为一个引用类型的数据出现
  • new Object() 这部分会反映到Java堆中,形成一块大小不固定、存储Object类型所有实例数值的结构化内存
  • 能查到此对象类型数据(对象类型、父类、实现的接口、方法等)的地址信息被存储在方法区中

主流的访问方式分为两种:使用句柄和直接指针

  • 使用句柄时,Java堆会划分出一块内存作为句柄池,句柄中包含了对象实例数据和类型数据各自的具体地址
    在这里插入图片描述

  • 直接指针的方式

在这里插入图片描述
使用句柄的好处就是reference存储的是稳定的句柄地址,对象被移动时只会改变句柄中的实例数据的指针,而reference不会被改变。
使用直接指针访问的方式的好处就是快,减少一次指针定位的时间开销。

2.4 OutOfMemory异常

这一部分介绍了几种会出现OOM的情况。

2.4.1 Java堆溢出

Java堆溢出时实际中最容易发生的OOM情况,Java堆用于存储对象,只要不停的创造对象,并且保证GC Roots到对象之有可达路径避免垃圾回收,就会在达到最大堆容量时出现溢出异常。

    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        while (true){
            list.add(new Object());
        }
    }

会出现“java.lang.OutOfMemoryError"和"Java heap space"

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

前面说过Java虚拟机栈会抛出两种异常:StackOverflowError和OutOfMemoryError
在单个线程下,无论时栈帧太大还是虚拟机栈容量太小都抛出的时StackOverflowError
多线程会出现OOM现象,虚拟机内存-堆醉倒内存-方法区最大内存=栈+本地方法栈内存(程序计数区忽略),操作系统给每个进程分配的内存有限,每个进程分配的栈内存越大,可以建立的线程就越少,建立线程时就越容易发生溢出。解决过多线程导致的溢出问题,如果不能减少线程数,可以考虑减小堆和方法区的内存,或者减小栈容量。

2.4.3 运行时常量池溢出
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        int i = 0;
        while (true){
            list.add(String.valueOf(i++).intern());
        }
    }

String.intern这个Native方法的作用是:如果池子中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;如果不含有,则将次String对象包含的字符串添加到常量池中,并返回此String对象的引用。
运行时常量池溢出会出现OOM提示和“PermGen space”

2.4.4 方法区溢出

方法区会存储Class的相关信息,像要让方法区溢出只需要运行时产生大量的类去填满方法区。

2.4.5 本机直接内存溢出
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true){
            unsafe.allocateMemory(_1MB);
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值