《深入理解Java虚拟机》ch2 Java内存区域
方法区 和 堆 是随虚拟机启动而启动的;
虚拟机栈、本地方法栈和程序计数器是线程私有的;
程序计数器 (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堆的结构:
-
Eden(伊甸园)用来存放新对象,Survivor区中分为两个区域,其中一个用于存储新生代,另一个用于存储GC后任存活的对象。
-
当to区存储的对象达到阈值时,对象被移动old区,等到old区达到阈值时进行垃圾回收。
-
新生代中Eden和两个Survivor区的内存占比约为 8 : 1 : 1 ;而新生代和老年代的内存占比为 1 : 2。
在 100MB 的内存中,可用内存为 96 M。新生代占 33MB,老生代占 67MB。新生代中,Eden区占 25 MB,而两个Survivor区各占 4 MB (这个比例是不一定的)。
-
-
几乎所有的对象实例和数组都应当在堆上分配。
-
由于即使编译技术的进步和逃逸分析技术的强大,栈上分配和标量替换优化并不在堆上申请内存。
-
-
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。
-
HotSpot虚拟机
对象的创建
-
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());
}
-