第一章
运行时数据区
程序计数器
程序计数器是一块较小的空间,可看作线程的当前执行字节码的行号指示器.用来控制分支、循环、跳转、异常处理、线程恢复等基础功能.每个线程都有一个独立的程序计数器,各个线程之间互不影响,是线程私有的.
如果线程正在执行的是java方法代码,则计数器记录的是字节码指令的地址,如果正在执行的Native方法,则计数器的值为Undefined.程序计数器是Java虚拟机规范中唯一没有规定任何OutOufMemoryError情况的的区域
Java虚拟机栈
Java虚拟机栈也是线程私有的,生命周期与线程相同虚拟机栈描述的是Java方法执行的内存模型:每个方法在线执行的时候都会创建栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息.方法执行到完成,对应一个栈帧入栈到出栈的过程.
此区域操作不当会抛出OverFlowError和OutOfMemoryError
本地方法栈
本地方法栈与虚拟机栈类似,区别在于虚拟机栈为虚拟机执行java方法,即字节码,而本地方法栈执行的是Native方法.此区域操作不当会抛出OverFlowError和OutOfMemoryError
Java堆
堆是java虚拟机管理的内存中最大的一块.是线程共享的,在虚拟机启动是创建.J此内存区域的唯一 目的就是存放对象实例,几乎所有的对象实例都在这里分配内存ava堆是垃圾收集器管理的主要区域.由于收集器都采用分代算法,因此Java堆还可以细分为新生代和老年代.再细致还可以再分:Eden空间、From Survivor空间、To survivor空间.此区域会抛出OutOfMemoryError异常.
方法区
方法区和堆一样,是线程共享的,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译后的代码.在jdk7以前,习惯上把方法区,成为永久代。jdk8开始,使用元空间取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不再虚拟机设置的内存中,而是使用本地内存。Java虚拟机规范规定,当方法区无法满足内存分配需求时,会抛出OutOfMemoryError异常
运行时常量池
运行时常量池(Runtime Constant Pool),它是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。运行时常量池和Class文件常量池的有个重要的区别就是运行时常量池具备动态性,即不仅仅是预置入Class文件中的常量池才能进入运行时常量池,程序运行期间,可以将新的常量放入池中,例如String.intern()方法.
运行时常量池既然时方法区的一部分,自然也会抛出OutOfMemoryError异常
直接内存
直接内存有一种叫法,堆外内存。
直接内存(堆外内存)指的是Java应用程序通过直接方式从操作系统中申请的内存。这个的差别与之前的堆,栈,方法区不同。那些内存都是经过了虚拟化的内存。使用Java的Unsafe类,可以操作直接内存
Hotspot虚拟机对象探秘
对象的创建
- 指针碰撞: 假设Java堆中内存是绝对规整的,所有用 过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器, 那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种 分配方式称为“指针碰撞”(Bump the Pointer)
- 空闲列表: 堆中的内存并不是规整的,已使 用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间 划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List), 选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否 带有压缩整理功能决定。因此,在使用Serial、ParNew等带Compact过程的收集器时,系 统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常 采用空闲列表。
对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头 (Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头: HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数 据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、 偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为 32bit和64bit,官方称它为"Mark Word"。
- 实例数据:是对象真正存储的有效信息,也是在程序代码中所定义的各种 类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来
- 对齐填充:并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用
HotSpot 虚拟机对象头 Mark Word
存储内容 | 标识位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC标记 |
偏向线程ID,偏向时间戳,对象分代年龄 | 01 | 可偏向 |
对象的访问定位
对象访问的方式由虚拟机自己实现,主流的有两种:句柄和直接指针两种。
-
句柄访问:那么Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息,如图:
-
指针直接访问:堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址,如图:
这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是 稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句 柄中的实例数据指针,而reference本身不需要修改。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开 销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本.