深入理解JVM虚拟机——2. Java内存区域与内存溢出异常

2.1 概述

对于c/c++程序员来说,每一个new操作后面要配对delete/free操作来释放内存,很繁琐。
而Java将内存控制权交给了Java虚拟机,使得程序员不需要去关心内存的内在机制,而知专注于代码本身。而这一章将介绍虚拟机是如何使用内存的。


2.2 运行时数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区 域。

  • 方法区
  • 虚拟机栈
  • 本地方法栈
  • 程序计数器
2.2.1 程序计数器

简单来说程序计数器就是用于程序选取的,每一个线程都有自己独立的程序计数器(线程私有)。通过程序计数器可以选取下一条需要执行的指令,由它指示到字节码行号,也就是记录指令的地址。

2.2.2 Java虚拟机栈

虚拟机栈也是平常我们所说的栈区域,它也是线程私有的,也就是生命周期与线程一致,简单来说它就是用来方法执行的,每个方法在执行的时候,会创建一个栈帧,用于存储于 存储局部变量表操作数栈动态链接方法出口 等信息,每一个方法从调用到执行完成,都有一个栈帧从入栈到出栈。

2.2.3 本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间 的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚 拟机使用到的Native方法(非Java代码)服务

2.2.4 Java堆

Java堆是内存中最大的一块区域,它是线程共享的,虚拟机启动时创建,而它的唯一目的就是存放对象实例,Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。

2.2.5 方法区

方法区也是线程共享的,它用于存储已被虚 拟机加载的 类信息常量静态变量、即时编译器_编译后的代码_等数据。

2.2.6 运行时常量池

它是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。

2.2.7 直接内存

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓 冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储 在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著 提高性能,因为避免了在Java堆和Native堆中来回复制数据。


2.3 HotSpot虚拟机对象

笔者以常用的虚拟机HotSpot和常用的内存区域Java堆为例,深入探讨HotSpot虚拟机在Java堆中对象分 配、布局和访问的全过程。

2.3.1 对象的创建
  • new指令: 开始创建对象
  • 类加载检查: 首先检查指令参数能否在常量池中定位到一个类的符号,检查是否加载过,如果没有需要类加载过程
  • 分配内存: 内存大小确定后,分区指针向空闲区移动内存大小的距离(指针碰撞),如果空闲区和已使用区域交错,则虚拟机维护一个列表,记录空闲的内存块,并为对象从列表中分配一块足够大的空间(空闲列表)。
  • 对象信息设置: 例如对象是哪个类的实例,对象的哈希码,对象的GC年代划分等等,这些信息放在对象的对象头(Object Header) 当中,虚拟机视角来看,当前对象已经创建完成。
  • 方法: 从Java程序角度来看,执行方法后,这个对象才算完全的生产出来
2.3.2 对象的内存布局

在Hospot虚拟机中,对象在内存中布局可分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

  1. 对象头:
    包括两部分信息,一部分存储运行时信息,例如哈希码GC分代年龄锁状态标志线程持有的锁偏向线程ID偏向时间戳
    另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过指针来确定对象是哪个类的实例,但是并不一定,查找对象元数据信息不一定要通过对象本身。
  2. 实例数据: 这部分存储真正有效的数据信息,也是在程序代码中所定义的各种类型的字段内容。
  3. 对齐填充: 这部分不是必然存在的,仅仅起着占位符的作用,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节整数倍,也就是说对象大小必须是8的倍数,因此当实例数据部分没有对齐时,通过这部分来进行填充对齐。
2.3.3 对象的定位访问

Java程序需要通过栈上的reference数据来操作堆上的具体对象。而这个数据只规定了一个指向对象的引用,没有定义用何种方式去定位访问,目前主流的访问方式有使用句柄和直接指针两种方式

  • 使用句柄访问

    划分一块内存作为句柄池,reference中存储的就是对象的句柄地址,优势是稳定的句柄地址,对象被移动时只会改变句柄中的示例数据指针。
    image

  • 直接指针访问
    reference中存储的直接是对象的地址,优势是访问速度快,节省一次定位指针的开销。
    image


2.4 实战:OutOfMemoryError异常

Java虚拟机中除了程序计数器以外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(下文称OOM)异常的可能。首先设置debug的参数显示详细信息,通过代码进行各种溢出的尝试。

2.4.1 Java堆溢出

Java堆用于存储对象实例,不断地创建对象,并且保证GC ROOTS到对象之间有可达路径来避免垃圾回收清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出

public class Main {
    static class OOMObject{

    }

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        while(true){
            list.add(new OOMObject());
        }
    }
}
2.4.2虚拟机栈和本地方法栈溢出

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
Java虚拟机把异常分为这两种情况,但却有重叠的的地方,当栈空间无法继续分配时,是内存太小还是已使用栈空间太大,本质是相同的。
实验结果表明:在单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。
每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。

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

由于运行时常量池是方法区的一部分,因此这两个区域的溢出测试就放在一起进行。

  • 常量池: String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等 于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包 含的字符串添加常量池中,并且返回此String对象的引用。
/*
* VM Options
* -XX:PermSize=10M
* -XX:MaxPermSize=10M
* 限制常量区大小
* */
public class Main {

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        int i = 0;
        while(true){
            list.add(String.valueOf(i++).intern());
        }
    }
}
  • 方法区: 测试思路是运行时产生大量的类去填满方法区,直到溢出
/*
* VM Options
* -XX:PermSize=10M
* -XX:MaxPermSize=10M
* 限制常量区大小
* */
public class Main {

    public static void main(String[] args) {
        while(true){
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OMMObject.class);
            enhancer.setUseCatch(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 OMMObject{}
}

方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是 比较苛刻的。

2.4.4 本机直接溢出

DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java 堆最大值(-Xmx指定)一样

由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显 的异常,如果读者发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就 可以考虑检查一下是不是这方面的原因

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值