《深入理解Java虚拟机:JVM高级特性与最佳实践》笔记(一)
Java 内存区域与内存溢出异常
- 运行时数据区
- 所有线程共享的数据区
- 方法区(Method Area)
- 堆(Heap)
- 执行引擎
- 本地库接口
- 线程隔离的数据区
- 程序计数器(Program Counter Register)
- 本地方法栈(Native Method Stack)
- 虚拟机栈(VM Stack)
- 所有线程共享的数据区
程序计数器
当前线程所执行的字节码的行号指示器。如果执行Java方法,记录正在执行的虚拟机字节码指令的地址;如果执行Native方法,值为Undefined。唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域。
Java虚拟机栈
每个Java方法执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表所需内存在编译期间完成分配,运行期间不会改其大小。
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度时
OutOfMemoryError:虚拟机栈可以动态扩展,扩展时无法申请到足够的内存时
本地方法栈
类似虚拟机栈,区别是执行虚拟机用到的Native方法。方法使用的语言、使用方式与数据结构没有强制规定,可自由实现。
Java堆
存放对象实例,虚拟机启动时创建。随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术使得有些对象不会分配到堆上。
垃圾收集器管理的主要区域,也称“GC堆”(Garbage Collected Heap)。
可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果没有内存完成实例分配也无法扩展时将抛出OutOfMemoryError。
方法区
用于存储已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码数据。内存回收目标主要是针对常量池的回收和对类型的卸载。
当内存无法满足分配需求时将抛出OutOfMemoryError。
运行时常量池
方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
无法再申请到内存的时候会抛出OutOfMemoryError。
直接内存(Direct Memory)
JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,避免在Java堆和Native堆中来回复制数据。
受机器总内存大小以及处理器寻址空间、操作系统的限制,超出时将导致动态扩展出现OutOfMemoryError。
HotSpot虚拟机对象探秘
对象的创建
- 遇到new指令
- 检查是否能在常量池中定位到一个类的符号引用
- 检查类是否已被加载、解析和初始化过
- 分配内存,内存大小在类加载完成后便完全确定
- 设置对象头(Object Header)
- 由字节码中是否跟随Invokeespecial指令决定,执行new指令之后会接着执行
方法。
分配堆内存,如果Java堆中内存是绝对规整的,使用“指针碰撞(Bump the Pointer)”分配方式;否则使用“空闲列表(Free List)”方式。
并发分配两种方案,一种对分配内存空间的动作进行同步处理——实际采用CAS配上失败重试的方式保证更新操作的原子性;另一种是本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。TLAB用完并分配新的TLAB时才需要同步锁定。
对象的内存布局
三块区域:对象头(Object Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头包括两部分,第一部分存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称它为“Mark Word”。另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据部分是对象真正存储的有效信息。
对齐填充并不是必要的,只是占位符,使对象的大小必是8字节的整数倍。
对象的访问定位
主流的访问方式有两种,使用句柄和直接指针。
- 使用句柄则Java堆专门划分一块内存作为句柄池,reference中存储对象的句柄地址,句柄中包含对象实例数据与类型数据各自的具体地址信息。好处:对象移动时,reference不需要修改。
- 直接指针则Java堆对象的布局就需要放置访问对象类型数据的指针。优点:访问速度快。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!