《深入理解Java虚拟机》笔记一
1. 自动内存管理机制
1.1. 运行时数据区
图1 java运行时数据区域
- 程序计数器(PC,线程私有)
为了线程恢复后能切换到正确的位置,每条线程都有一个PC,互不影响。
若线程正在执行java方法,pc记录正在执行的虚拟机字节码指令的地址;若正在执行native方法,则pc为空(undefined)。
- java虚拟机栈(线程私有)
用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
局部变量表存放:基本数据类型(int, long, short, byte, float, double, char, boolean. 64位长度的long和double占用两个局部空间(slot),其他的都只占用一个)、对象引用(reference类型,不等同于对象本身)、returnAddress类型(指向一条字节码指令的地址)。
局部变量表所需内存在编译时完成。运行期间不会改变局部变量表的大小。
这个区域有两种异常状况:StackOverflowError(请求的栈深度大于虚拟机所允许的深度), OutOfMemeryError(当虚拟机可以动态扩展,扩展时申请不到足够的内存)。
- 本地方法栈
同java虚拟机栈。java虚拟机栈为虚拟机执行java方法服务,本地方法栈为虚拟机执行native方法服务。有的虚拟机将二者合二为一。
- Java堆(所有线程共享,虚拟机启动时创建)
目的:存放对象实例。
java堆是垃圾收集器管理的主要区域,也被叫做GC堆。堆还可分为:新生代,老年代;Eden空间,From Survior空间,To Survivor空间。
java堆中可能划分出多个线程私有的分配缓冲区(TLAB),目的是为了更好地回收内存或者更快的分配内存。
Java堆只需逻辑上是连续的即可,可通过-Xmx和-Xms进行扩展。
如果在堆中没有内存完成实例分配,且堆无法再扩展时,将抛出OutofMemoryError异常。
- 方法区(线程共享)
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译完成后的代码等数据。(别名:Non-Heap)
可用多种方法实现方法区,比如堆中的永久代。但是永久代有-XX:MaxPermSize的限制,容易遇到内存溢出问题。也需要实现垃圾回收机制。
当方法区无法满足内存分配需求时,将抛出OutOfMemory异常。
- 运行时常量池(方法区的一部分)
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性, Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
- 直接内存
并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
JDK1.4加入了NIO,引入一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。因为避免了在Java堆和Native堆中来回复制数据,提高了性能。
当各个内存区域总和大于物理内存限制,抛出OutOfMemoryError异常。
1.2. 虚拟机对象
- Java对象的创建过程
图2 java对象的创建过程
为对象分配内存,有两种方式:
指针碰撞:用过的内存放在一边,空闲内存放另一边,中间一个作为分界点的指针。分配内存时就把指针左右移动。(需要堆中的内存是规整的)
空闲列表:若堆内存不是规整的,需要维护一个表来记录哪些内存是可用的。分配时从列表中找到足够的内存空间划分给对象。
堆内存是否规整,取决于垃圾收集器是否带有压缩整理功能。
上述两种内存分配方式还需考虑线程安全性。实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。另一种是:引入TLAB(本地线程分配缓冲),将内存分配的动作按照线程划分到不同的空间进行。虚拟机是否使用TLAB,通过-XX:+/-UseTLAB参数来设定。
- 对象的内存布局
在HotSpot中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
对象头包括两部分,一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据部分是对象真正存储的有效信息。存储顺序受虚拟机分配策略参数和字段在Java源码中定义顺序的影响。
对齐填充不是必然存在的。起占位符的作用。HotSpot VM要求对象起始地址必须是8字节的整数倍。
- 对象的访问定位
方式:使用句柄和直接指针。
使用句柄方式:Java堆中划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,句柄中包含对象实例数据与类型数据各自的具体地址信息。
优点:在对象被移动时只会改变句柄中实例数据指针,而reference本身不需要修改。
使用指针方式:Java堆对象的布局中必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址。
优点:访问速度快。HotSpot使用指针方式。