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

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)

  • 程序计数器占用非常小的内存,指向下一条指令的地址。
  • 每个线程拥有一个程序计数器。
  • 程序计数器在线程创建时创建。
  • 如果是Java方法,程序计数器指向字节码指令的地址。
  • 如果是Native方法,程序计数器值则为空(Undefined)。
  • 程序计数器不会出现OutOfMemoryError。

1.2 Java栈

  • Java栈是线程私有的,生命周期和线程相同。
  • 栈是由一系列栈帧组成的。
  • 每个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
  • 每一个方法被调用到执行完成的过程,就是一个栈帧在JVM从入栈到出栈的过程。
jvm-stack-frame.png

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

  • StackOverflowError:线程请求的栈深大于虚拟机所允许的深度(例如无限递归)。
  • OutOfMemoryError:虚拟机栈可以动态扩展,如果扩展无法申请足够的内存时,就会报出。

1.3 Native方法栈

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

1.4 堆(Heap)

  • Java堆是JVM所管理的内存中最大的一块,生命周期和JVM进程相同。
  • 用于存放对象实例,几乎所有的对象都在Heap上。
  • Java堆是所有线程共享的空间。

从垃圾回收的角度来说,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中。

  • Eden区是Java实例对象优先分配的区域,如果Eden没有足够的空间,将会执行一次Minor GC。
  • 经过Minor GC后,Eden+S0(或者S1)中还存活的对象将会转移到S1中,然后S0会被清空。
  • Survivor中放不下的、存活次数超过一定数目的对象,会被转移到老年代(Old)空间,大对象也可能会直接分配到老年代(Old)空间。
  • 当老年代(Old)空间不够时,将会发生Major GC。
  • 如果垃圾回收后,仍然没有足够的空间,那么将会抛出OutOfMemoryError。

1.5 方法区

  • 方法区是线程共享的空间,生命周期和JVM进程相同。
  • 方法区用于存储类的信息、常量池、字段和方法数据、字节码内容等。

在我们常用的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 对象的内存布局

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

  • 对象头 包括两个部分,第一部分是“Mark Word”,用于存储对象自身的运行时数据,包括HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向ID、偏向时间戳等;第二部分是类型指针,指向存放指向方法区的类数据,即JVM通过这个指针来确定对象是哪个类的实例。

  • 实例数据 存放类的属性,包括父类的属性信息。相同宽度的字段(例如long和double都是8字节)分配在一起。

  • 对齐填充 这是虚拟机要求对象起始地址必须是8字节的整数倍,如果实例数据部分不是8字节的整数倍,那么就需要对齐填充来补齐,除此之外,并无它意。

2.3 对象的访问定位

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

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

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

对象句柄访问.png

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

对象指针访问.png

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

(完)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容