JVM笔记02-JVM内存区域结构

2017-06-27  本文已影响137人  郭寻抚

0. 前言

JVM笔记系列,以JDK1.7为基准,主要以《深入理解Java虚拟机》(第二版)和《Java虚拟机规范(Java SE 7版)》 为参考,主要包括下图所示的五部分内容:1.类加载,2.内存区域,3.垃圾回收,4.JVM参数,5.JVM监控工具。

本人是Java程序员,重点关注这些有助于优化开发、性能调优、问题解决等这些和具体生产密切相关的部分;关于Class的文件结构、编译、指令等部分,可以阅读上述书籍或其它材料。

jvm.png

本文主要记录JVM内存区域结构的相关知识,本文的主要知识点如下:

jvm内存区域结构.png

1. JVM内存区域结构

JVM定义了若干程序运行期使用到的数据区,其中一些随着JVM进程启动而创建,随着JVM退出而销毁;另一些则是与线程一一对应,随着线程的启动和结束而建立和销毁。JVM的运行时数据区分为5个部分,如下图所示,分别是程序计数器、Java栈、Native方法栈、堆、方法区。

jvm-runtime-area.png

1.1 程序计数器(Program Counter)

1.2 Java栈

jvm-stack-frame.png

JVM规范中描述,Java栈可能会出现两种异常。

1.3 Native方法栈

本地方法栈和Java栈是非常相似的,Java栈是为了执行Java方法服务,本地方法栈是为了执行Native方法使用。在HotSpot虚拟机中,Java栈和本地方法栈合二为一。

1.4 堆(Heap)

从垃圾回收的角度来说,Java堆分为新生代和老生代,其中新生代还分为Eden、From Survivor(S0)、To Survivor(S1)三部分,如下图所示。


jvm-heap.png

默认参数下,新生代:老生代 = 1:2,Eden:Survivor = 8:1。Java堆中最大可用内存 = 老生代+ Eden + Survivor*1,即S0和S1永远有一个处于闲置的状态,GC的时JVM候会把其中一个Survivor中存活的对象复制到另一个Survivor中。

1.5 方法区

在我们常用的HotSpot虚拟机中,JDK1.7之前,使用PermGen(永久代)来实现方法区;在JDK1.8中完全移除了PermGen,改用Metaspace(元空间)来实现方法区。

其实,移除PermGen的工作从JDK1.7就开始了,符号引用(Symbols)、字面量(interned strings)、类的静态变量(class statics)在1.7中都转移到了Heap中,这大大减少了PermGen抛出OutOfMemoryError的机会。

Metaspace使用的是本地内存,而非JVM内存;因此Metaspace的大小限制,受限于物理内存的的限制;当然它是可以通过参数-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 来指定的。

方法区的空间不够用了,将会抛出OutOfMemoryError。

关于方法区,运行时常量池特别值得一提,运行时常量池中的常量,基本来源于各个class文件中的常量池;程序运行时,除非手动向常量池中添加常量(比如调用String.intern方法),否则jvm不会自动添加常量到常量池。

1.6 直接内存(Direct Memory)

直接内存并不是JVM运行时数据区的一部分,属于堆外(off-heap)内存,也不是JVM规范中定义的内存区域。JDK1.4新增了NIO包,引入了一种基于Channel和Buffer的IO方式,可以使用Native方法直接分配堆外内存,然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

//见 java.nio.ByteBuffer
public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

// native方法,见sun.misc.Unsafe类
public native long allocateMemory(long var1);

使用堆外内存,可以扩展使用更大的内存空间,理论上能减少GC的暂停时间,还可以在进程间共享(MappedByteBuffer和FileChannel)。

Direct Memory默认的大小是等同于JVM最大堆,我们可以通过-XX:MaxDirectMemorySize参数来控制其大小。

如果直接内存空间不够用了,将会抛出OutOfMemoryError。

2. 对象的创建和访问过程

2.1 对象的创建过程

  1. 类加载检测。当new对象的时候,将会检查能否在常量池中定位到一个类的符号引用,并检查这个类是否被加载、解析和初始化,如果没有,则执行相应的类加载过程。

  2. 类加载检查通过后,JVM将会为新生的对象分配内存。如果Java堆内存是规整的,内存分配采用“指针碰撞”方式;如果内存不是规整的,则采用“空闲列表”的方式。Java堆内存是否规整,取决于使用的垃圾回收器是否带有压缩整理的功能。因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。给对象分配内线的过程,是指针移动的过程,它不是线程安全的,需要同步;为了解决这个问题,JVM给每个线程在Java堆中预先分配一块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),这样以来,只有缓冲区用完了,重新分配时才需要同步操作。

  3. 对象内存分配完毕之后,JVM把分配的内存空间都初始化为零值。

  4. JVM对对象做必要的设置。例如对象是哪个类的实例、如何找到类的元数据、对象的哈希码、对象的GC分代年龄等,这些信息存放在对象头(Object Header)中。

  5. 至此,在JVM看来对象创建完成;接下来执行<init>方法,把对象按照程序员的意愿初始化,形成一个真正可用的对象。

2.2 对象的内存布局

对象在堆中的布局分为三个区域:对象头,实例数据,对齐填充。

2.3 对象的访问定位

引用存放在Java栈上,数据类型为reference;对象存放在Java堆中,引用是如何指向对象实例呢?

目前主流的访问方式有两种,1.使用句柄;2.使用直接指针。

如果使用句柄访问,那么Java堆中将会分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据和类型数据的具体地址。句柄的好处在于,当对象被移动时(垃圾回收时发生),只会改变句柄中的实例数据指针,reference本身不需要修改。

对象句柄访问.png

如果使用直接指针访问,reference引用直接指向堆中的对象实例,对象实例的对象头存放对象类型指针,这种方式的好处在于,减少了一次指针定位的开销,访问速度更快。

对象指针访问.png

HotSpot虚拟机中使用的是直接指针访问的方式。

(完)

上一篇 下一篇

猜你喜欢

热点阅读