方法区 和 堆 是随虚拟机启动而启动的;
虚拟机栈、本地方法栈和程序计数器是线程私有的;
程序计数器 (Program Counter Register)
是 当前线程 执行字节码的行号指示器,每一条线程都有一个独立的程序计数器。
执行Java方法,计数器记录的时虚拟机字节码指令的地址
执行Native方法,则为 Undefined
字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令。
Java虚拟机栈 (Java Virtual Machine Stack)
虚拟机栈描述的是Java方法执行的线程内存模型,是线程私有的:
每个方法被执行时会同步创建一个帧栈 (Stack Frame),用于存储局部变量表、操作数栈、动态连接、方法接口等信息。方法的调用和退出对应着帧栈在虚拟机栈中的入栈和出栈。
局部变量表:存放了编译期可知的基本数据类型、对象引用和ReturnAddress类型。
存储空间以局部变量槽 (slot) 为基本单位。
局部变量表所需要的 slot 的数量是在编译期间完成分配并确定的。
虚拟机栈的两种异常状况:
StackOverFlowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈容量可以动态扩展,但是栈扩展无法获取足够的内存。
本地方法栈 (Native Method Stacks)
和Java虚拟机栈类似,不过是为本地方法服务,也会存在StackOverFlowError和OutOfMemoryError;
(部分虚拟机会将 Java虚拟机栈 和 本地方法栈 合并)
Java堆 (Java heap)
是虚拟机所管理内存中最大的一块,在虚拟机启动时创建,为所有线程共享。
几乎所有的对象实例和数组都应当在堆上分配。
由于即使编译技术的进步和逃逸分析技术的强大,栈上分配和标量替换优化并不在堆上申请内存。
Java堆是垃圾收集器管理的内存区域,因此堆也被称为 Garbage Collected Heap。
线程共享的Java堆可以划分出多个线程私有的分配缓冲区 (Thread Local Allocation Buffer, TLAB)
Java堆逻辑上是连续的,物理上可以存储在不连续的空间中。
Java堆可以选择扩展或不扩展,如果堆空间被用尽会抛出 OutOfMemoryError。
方法区 (Method area)
方法区也为多个线程共共享,是堆的一个逻辑区域,被称为非堆 (Non-Heap)。
方法区中存储了已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
JDK7之后,字符串常量池、静态变量等被移到了Java堆中。
JDK8中,抛弃了永久代的观念,永久代中的内容被本地内存中实现的元空间替代。
方法区可以选择大小是否可扩展,还可以选择是否实现垃圾回收机制。(因为方法很少改变,某只意义上是永久的)
方法区无法申请新内存时,会抛出 OutOfMemoryError。
运行时常量池 (Runtime Constant Pool)
是方法区的一部分,JDK8之后被移到元空间中,而字符串常量池则在堆中。
Class文件包含类的版本、字段、方法、接口等信息外,还包含常量池表 (Constant Pool Table)
其用于存放编译器生成的各种字面量和符号引用,在类加载后被方法方法区的运行时常量区中。
运行时常量池是动态的,Class文件常量池是静态的,可以在运行期间将新的常量放入运行时常量池中。
{String}.intern()
寻找当前字符串所属的常量字符串,如果没有则创建并返回常量字符串。
常量池无法申请内存时会抛出 OutOfMemoryError。
直接内存 (Direct Memory)
不是运行时数据区的一部分,但是这部分区域被频繁使用,且会抛出OutOfMemoryError。
对象的创建
Java虚拟机遇到一条字节码 new 指令时采取的动作:
检查new指令的参数是否能够在常量池中定义到一个类的符号引用。
检测这个类是否被已经被加载、解析和初始化过。
否则执行相应类的加载过程。
虚拟机为新生对象分配内存
对象所需要的内存在类加载完成后便可完全确定。
Java堆的内存是否规整,取决于所采用的垃圾收集器是否带空间压缩整理(Compact)功能。
Java堆的内存是规整的:用指针记录已用内存和空闲内存的分界点,分配内存将指针移动即可。这种分配方式被称为指针碰撞 Bump The Pointer。
Java堆的内存不规则:维护一个Free List,在列表中找到足够的空间分配给对象实例。
保证并发下对象创建的线程安全的两种方法:
对分配内存空间的动作进行同步处理——通过CAS(乐观锁)配上失败重试的方式。
为线程划分不同的内存分配空间,每个线程预留一个本地线程分配缓冲 (TLAB),LTAB用完后在分配新的缓存区时采用同步锁定。
将初始化的内存空间 (不包括对象头) 置0,TLAB可能会将此操作提前。
设置对象的对象头 —— 哈希码(实际上调用hashcode时才计算),类的元数据,分代年龄等。
调用程序员编写的构造方法。
创建对象的过程:
HotSpot解释器代码:
// 确保常量池中存放的是已解释的类 if(!constants->tag_at(index).is_unresolved_klass()) { // 断言确保是 klassOop opp entry = (klassOop)*constants->obj_at_addr(index); assert(entry->is_klass(), "Should be resolved klass"); // 断言确保是 instanceKlassOop KlassOop k_entry = (klassOop) entry; assert(k_entry->klass_part()->oop_is_instance(),"Should be instanceKlass"); instanceKlass* ik = (instanceKlass*) k_entry->klass_part(); // 确保对象所属类型已经经过初始化过程 if(ik->is_initialized() && ik->can_be_fastpath_allocated()) { // 获取对象长度 size_t obj_size = ik->size_helper(); oop result = NULL; // TLAB会提前置0,只有指针碰撞的分配方式时才需要置0 bool need_zore = !ZeroTLAB; // 如果是TLAB方法,则需要为其在TLAB中分配内存 if(UseTLAB) { result = (oop) THREAD->tlab().allocate(obj_size); } if (result == NULL) { need_zero = true; // 优先在eden中分配对象 // 目前主流的垃圾收集器将heap分为新生代(Eden,Survior1,urvior2)和老生代 // 获取已用内存和空闲内存分界点的指针,将其移动 retry: HeapWord* compare_to = *Universe::heap()->top_addr(); HeapWord* new_top = compare_to + obj_size(); // 如果还有足够的内存空间则进行分配 if(new_top <= *Universe::heap()->end_addr()) { // 采用CAS的方法,申请失败则retry if(Atomic::cmpxchg_ptr(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) { goto retry; } result = (oop) compare_to; } } if(result != NULL) { if(need_zero) { // oppDesc是对象头 HeapWord* to_zero = (HeapWord*)result+sizeof(oopDesc)/oppSize; // 对象头无需置0 obj_size -= sizeof(oppDesc) / oppSize; if(obj_size > 0) { memset(to_zero, 0, obj_size * HeapWordSize); } } // 根据是否启用偏向锁,设置对象头信息 if(UseBiaseLocking) { result->set_mark(ik->prototype_header()); } else { result->set_mark(markOopDesc::prototype()); } // 设置类的其他部分 result->set_klass_gap(0); result->set_klass(k_entry); // 将对象引用加入栈,继续执行下一条指令 SET_STACK_OBJECT(result, 0); UPDATE_PC_AND_TOS_AND_CONTINUE(3,1); } } }
对象的内存布局
HotSpot虚拟机中,堆的对象可以分为 对象头,实例数据 和 对齐填充 三个部分。
对象头:包含自身运行时的数据 和 指向它的类型元数据的指针。
实例数据:对象真正存储的位置。
对齐填充:不一定存在,起到占位作用。
对象头的组成:
用于存储对象自身运行时数据的数据长度为32bit或64bit(取决于虚拟机的位数),称为Mark Word。
Mark Word是动态定义的数据结构,以尽量减少占用空间
锁状态 |
25bit |
4bit |
1bit |
2bit |
|
23bit |
2bit |
是否偏向锁 |
锁标志位 |
||
无锁 |
对象的HashCode |
分代年龄 |
0 |
01 |
|
偏向锁 |
线程ID |
Epoch |
分代年龄 |
1 |
01 |
轻量级锁 |
指向栈中锁记录的指针 |
00 |
|||
重量级锁 |
指向重量级锁的指针 |
10 |
|||
GC标记 |
空 |
11 |
类型指针用于存储指向它的类型元数据的指针,JVM通过这个指针确定该对象是哪个类的实例。不过不是每一个JVM都有类型指针。
如果是数组,对象头中还有一块用于记录长度的数据。
实例数据存储部分存储对象真正的有效信息。
各类类型的字段存储顺序会受到虚拟机分配策略的影响。
HotSpot虚拟机默认按照long/double, int, short/char, byte/boolean, oop
的顺序分配
可以看到相同宽度的字段分配到一起存放,如果设置+XX:CompactFields
参数为true,那么子类中较窄的对象还可以放到父类变量的缝隙之中。
对齐填充,为了使任何对象为8的整数倍而存在。
对象大小的计算:
32位系统:Class指针4字节,MarkWord是4字节,对象头8字节。
64位系统:Class指针8字节,MarkWord是8字节,对象头16字节。
64位开启指针压缩的情况下,存放Class指针的4字节,MarkWord是8字节,对象头12字节。
数组长度4字节+数组对象头8字节(32位)+对齐4字节=16字节。
静态属性不算在对象大小内。
对象的访问定位
主流的访问方式又句柄和直接指针两种。
句柄访问:在Java堆中划分出一块内存作为句柄池,引用存储了句柄的地址,而句柄又存包含了实例数据 (Java堆)和类型数据 (方法区,元空间) 的地址信息。
优势:移动对象时,只需要修改句柄的实例数据指针,不需要修改对象本身。
直接指针访问:引用中存储的是对象地址,可以直接访问到对象本身。而对象中又有一个指向对象类型数据的指针。
优势:速度快,HotSpot主要采用直接制造访问。