第2章 Java内存区域与内存溢出异常
2.2运行时数据区域
Java虚拟机所管理的内存包括以下几个运行时数据区域:方法区、堆区、虚拟机栈、本地方法栈、程序计数器。
程序计数器(ProgramCounter Register):用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。程序计数器是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域。
虚拟机栈(Java Virtual Machine Stacks):线程私有。每当创建一个线程时,JVM就会为这个线程创建一个对应的JVM栈。在这个栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储方法中的局部变量表、操作栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈(Native Method Stacks):线程私有。其作用与虚拟机栈相似,区别是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈为虚拟机使用到的Native方法服务。本地方法栈也会抛出*Error和OutOfMemoryError异常。
Java堆(Java Heap):是Java虚拟机管理的最大的一块内存区域,被所有线程共享,在虚拟机启动时创建。存储对象实例以及数组。这块是GC的主要区域(因而也被称为”GC堆”)。可以扩展内存空间(通过-Xmx和-Xms控制)。当堆中无内存来完成实例分配且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
方法区(Method Area):线程共享。存储已被虚拟机加载的类信息、常量、静态变量、构造函数等数据。GC行为在这个区域是比较少出现的。同样,当方法区无法满足内存分配需求时,也会抛出OutOfMemoryError异常。方法区还包含一个运行时常量池。
运行时常量池(Runtime Constant Pool):顾名思义,运行时用来存放常量池的,是方法区的一部分,线程共享。当常量池无法再申请到足够的内存时,也会抛出OutOfMemoryError异常。
直接内存(Direct Memory):并不是Java虚拟机运行时数据区的一部分。但这部分内存被频繁使用,也可能导致OutOfMemoryError异常出现。属于堆外内存,其分配不受Java堆大小的限制。在JDK1.4中引入了NIO类,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用来进行操作。这避免了在Java堆和Native堆中来回赋值数据,能在一些场景中显著提高性能。
运行时数据区域的简单示意图如下:
2.3对象访问
对于形如:
Object obj = new Object();
的代码,Object obj作为一个reference类型的数据在Java栈的本地变量表中,而new Object() 则会反映到Java堆中。同时,Java堆中还必须含有能查到对象类型数据的地址信息,这些类型数据存储在方法区中。
由于reference类型在Java虚拟机规范里面只规定了一个指向对象的引用,定位方式不确定。目前主流的访问方式有两种:使用句柄和直接指针。
使用句柄访问:Java堆中划分出一块内存作为句柄池,reference存放对象的句柄地址,而句柄中包含了对象实例数据和类型数据的具体地址信息。
直接指针访问:Java堆对象的布局中再放置类型数据相关信息的引用,reference直接存储对象地址。
两种访问方式各具优势:
使用句柄访问时,reference中存储的事稳定的句柄地址,在对象被移动时值改变句柄中的指针,而不需要修改reference本身;
始终直接指针访问时,速度更快,它节省了一次指针定位的时间开销。因为对象访问在Java中非常频繁,这类开销积少成多也是一项可观的执行成本。