深入理解Java虚拟机这本书,JVM的内存管理,GC,JVM加载等内容有了一个较清晰的理解;但是对于Java字节码执行,内部优化的一些内容还是感觉比较吃力,这些内容还需要再看,再消化..
Java内存区域
运行时数据区域:
JVM在执行Java程序过程中将其管理的内存划分为若干个不同的数据区域。每个区域都有其各自的用途,以及创建和销毁的时间。
程序计数器 (Program Counter Register):
是一块较小的内存区域,是当前线程所执行的字节码的行号指示器。在JVM概念模型中(实际中可能采用更加高效方式),字节码解释工作就是通过改变这个计数器的值来选择下一条需要执行的字节码指令。
为了在线程切换后恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,每个线程之间的计数器相互独立。
执行的是Java方法,记录的是正在执行的虚拟机字节码指令的地址;是Native方法,计数器则为空。
该区域为唯一一个没有规定任何OutOfMemoryEror的区域。Java虚拟机栈(Java Virual Machine Stacks):
是Java方法执行的内存模型,线程私有,且生命周期与线程相同。
每个方法在执行的同时将创建一个栈帧(Stack Frame),该栈帧存储着局部变量表,操作数栈,动态链接,方法出口等信息。每个方法调用至执行完成的过程,对应着一个栈帧在虚拟机栈中入栈到出栈。- 局部变量表:存储了编译器可知的基本数据类型,对象引用(reference类型)和returnAddress类型(指向一条字节码的地址)。
局部变量表需要的内存空间在编译期间完成分配,每个方法需要在帧中分配的局部变量空间是完全确定的,且运行期间不会改变。
异常:
- StackOverflowError异常:线程请求的栈深度大于虚拟机允许的深度。(方法过多?)
- OutOfMemoryError异常:当栈可以动态扩展,直至扩展时无法申请到足够的内存。
- 局部变量表:存储了编译器可知的基本数据类型,对象引用(reference类型)和returnAddress类型(指向一条字节码的地址)。
本地方法栈:(Native Method Stack):
作用于虚拟机栈作用相似,区别在于本地方法栈为Native方法服务,虚拟机栈为Java方法(字节码)服务。Java堆(Java Heap):
JVM管理的内存最大的一块,被所有线程所共享,作用是存放对象实例。
Java堆是垃圾收集管理的主要区域,因此也称为“GC堆”。在后期会详细介绍内部的细分,回收方式。
异常:OutOfMemory:当堆内内存不足完成实例分配,并且堆无法再扩展。方法区(Method Area):
被所有线程共享,存储已被JVM加载的类信息,常量,静态变量,即时编译器编译后的代码等。
垃圾收集行为在该区域较少出现,因该区域内的回收目标(常量池,类型的卸载)的回收条件苛刻,回收效果差。
异常:OutOfMemory:当无法满足内存分配需求。
其他区域:
运行时常量池(Runtime Constant Pool):
属于方法区的一部分。用于存储Class文件中的常量池(Constant Pool Table),存放着编译期生成的各种字面量和符号引用(一个包含着一些足以唯一的识别一个类,方法,变量的字符串;会在装载class文件时转化为直接引用,直接引用也存储在这)。
动态性:运行期间也可能将新的变量放入池中(如String类的intern方法,将某字符串置入常量池)。
异常:OutOfMemory:当无法申请到内存时。直接内存(Direct Memory):
不属于运行时数据区和JVM规定的内存区域。在NIO中,被一个存储在Java堆中的DirectByteBuffer对象作为引用而进行操作。
不受到Java堆大小的限制,受到本机内存以及寻址空间限制。
对象解析
指普通的Java对象,不包括Class对象与数组等。
对象的创建过程:
类加载检查:
虚拟机遇到一条new指令时,首先去检查该指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。(没有,则执行类加载过程,在后文具体解释)为新生对象分配内存:
对象所需内存的大小在类加载完成后便可以完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。根据Java堆中内存是否规整(取决于GC算法?),划分的方法主要有两种:- 指针碰撞(Bump the Pointer):
当内存为绝对规整,将存在一个用过内存和空闲内存的分界点指针,通过将指针向空闲内存一端移动所需要的大小。 - 空闲列表(Free List):
当内存不规整,则需要在已使用与空闲内存交错内存块上需要可用的内存,则需要通过维护一个记录着哪些内存块可用的列表;通过在列表上找到一块足够大的看空间块,并更新列表。
因对象创建非常频繁,则需要考虑在并发条件下的分配内存的方案:
- 对分配内存的动作进行同步处理:通过采用CAS和失败重试的方式保证更新操作原子性。
- 对分配内存的动作按照线程划分在不同空间进行:本地线程分配缓冲,每个线程在Java堆中预先分配一块内存。
- 指针碰撞(Bump the Pointer):
后续工作:
- 初始化:将分配到的内存空间都初始化为零值(不包括对象头)。
- 必要的设置:主要是对对象头内信息的设置。对象头内包含着对象的很多信息:该对象是哪个类的实例,如何找到类的元数据,对象的哈希值,对象的GC分代年龄等。
以上完成后,对于JVM一个对象已经产生了;但从Java程序角度,所有字段都还为默认值,需要方法,将对象按照需要初始化。
对象的内存布局:
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域: 对象头(Header),实例数据(Instance Data),对齐补充(Padding)。
对象头:
对象头中包含两部分信息:Mark Word和类型指针。- MarkWord:用于存储对象自身的运行时数据。如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。
MarkWord被设计为一个非固定的数据结构在极小的空间内存储尽可能多的信息。32位,64位虚拟机中大小分别为32bit,64bit。
32位虚拟机对象头例子:
- 类型指针:对象指向它的类元数据的指针,JVM通过这个指针确定这个对象是哪个类的实例。
- MarkWord:用于存储对象自身的运行时数据。如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。
实例数据:
对象真正存储的有效信息,找程序代码中所定义的各种类型的字段内容。
存储顺序受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。对齐填充:
不一定存在,只是起到占位符的作用。因为虚拟机规定对象起始位置必须是8字节的整数倍,即对象大小必须是8字节的整数倍,则当没有对齐时,通过对齐填充补齐。
对象的访问定位:
通过(虚拟机栈)栈上的reference数据操作(GC堆)堆上的具体对象。reference类型在Java虚拟机规范中只规定为指向一个对象的引用,没有规定实现方式等等…则取决于虚拟机实现。
实现方式:
- 句柄访问:
在Java堆中划分出一块内存作为句柄池,reference存储的是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
好处:reference存储的是稳定的句柄地址。在对象被移动(GC时可能会移动对象)只会改变句柄的实例数据指针,对reference没有影响。
- 直接指针:
堆对象中需要考虑怎么放置访问类型数据相关信息,reference中存储的直接就是对象地址。
好处:速度快,节省了一次指针定位的时间开销(对象实例数据指针的定位过程)。