一、Java 内存区域

JVM 运行时的数据区域分为:

数据区

  1. 方法区 Method Area: 线程共享的区域,存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

  2. Java 堆 Java Heap: 线程共享的区域,存储对象实例,也是垃圾回收的主要区域。

  3. 虚拟机栈 Java Virtual Machine Stacks: 线程隔离的区域,线程私有,生命周期和线程相同。每个方法在执行的同时创建一个栈帧 Stack Frame,每个栈帧存储和方法运行有关的局部变量表、操作数栈、动态链接、方法返回地址等信息。

  4. 程序计数器 Program Countter Register: 线程隔离的区域,每个线程都有自己的程序计数器,存储程序当前执行的字节码的行号。

  5. 本地方法栈 Native Method Stack: 线程隔离,和虚拟机栈类似,是虚拟机调用 Native 方法时使用的。

  6. 运行常量池 Runtime Constant Pool: 方法区的一部分,存放字面量和符号引用。

  7. 直接内存 Direct Memory: 他不是虚拟机运行时数据区的一部分,也不是JVM中定义的数据区,但是通过 NIO 引入通道与缓冲区的I/O方式,可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在堆中DirectByteBuffer对象对这块内存进行引用操作。

二、对象的创建与访问

对象的创建

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否在常量词中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。有两种分配方式:

  1. 假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞” Bump the Pointer

  2. 如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”Free List

选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

对象的访问

1
Object obj = new Object();

这句代码会影响 Java栈、Java堆、方法区。假设这句代码出现在方法体中,那 Object obj 将会放映到 Java栈 的本地变量表中作为一个 Reference 类型出现。 而 new Object() 这部分会反映到 Java堆 中,形成一块存储了Object类型所有实例数据值(Instance Data, 对象中各个实例字段的数据)的结构化内存。另外在Java堆中还必须包含能找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些数据则存储在方法区中。

由于 reference 类型在 JVM 中只规定了一个指向对象的引用,其主流的定位方式依靠:使用句柄和直接指针。

  1. 句柄访问主要是 Java 堆中划分一块句柄池,虚拟机栈中存放句柄池中的地址,句柄池中包括对象的实例数据和对象类型的数据的地址,基本分布如下图:

句柄

  1. 直接指针访问,就是虚拟机栈直接指向 Java 堆中的对象类型指针和对象的实例数据,然后对象类型指针在指向方法区中对象类型的实例数据,分布如下图:

直接指针

【实战】OutOfMemoryError 异常