1 对象
1.1 对象的内存布局
1.1.1 对象头
对象头由两部分组成,第一部分是用来储存运行时数据(哈希码、GC年龄分代、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等)的"Mark Word",这部分有动态定义、空间复用的特点。第二部分是类型指针(使用句柄进行访问时则没有)。另外数组对象还需要记录其长度(可扩展型数组长度不确定)(普通对象可通过元数据信息获取对象大小)。
1.1.2 实例数据
这部分数据的存储顺序受策略参数和其在源码中定义顺序的影响,一般父类变量在子类变量前面存储,子类较窄的变量可以插入父类变量的空隙之中。
1.1.3 对齐填充
对象的大小必须为8字节的整数倍,否则要通过对齐填充来补全。
1.2 对象的创建
1.2.1 类加载检查
如果已经被加载、解析、初始化过则直接进入内存分配的步骤,如果没有则要先执行类加载的过程,然后再进入内存分配的步骤。
1.2.2 内存分配
1.2.2.1 “指针碰撞”和“空闲列表”
内存分配有两种方式,如果虚拟机堆内存是绝对规整的,则可以使用“指针碰撞”的方式为对象分配内存(以指针为界,移动指针分配内存)。如果堆内存不规整,则通过“空闲列表”的方式为对象分配内存(维护一个列表,用来记录内存)。堆内存是否规整,由其所采用的垃圾收集器是否带有空间压缩整理能力决定。
1.2.2.2 分配内存时的线程安全问题
为对象分配内存时可能出现多个线程竞争一个指针的情况。解决这种问题的第一种方式是采用CAS配上失败重试的方式保证更新操作的原子性,第二种方式是为本地线程分配缓冲(TLAB)(线程私有的堆空间)。
1.2.3 初始化零值
对象的实例字段因此可以不赋初值而直接使用。
1.2.4 对象头的设置
这步完成以后,在虚拟机的视角中一个新的对象已经产生。
1.2.5 执行初始化方法
执行代码中的赋值操作。
1.3 对象的访问定位
1.3.1 使用句柄进行访问
在堆中分配一块内存作为句柄池。句柄中包含了指向实例地址和指向类型信息地址(类型信息在元空间中)的指针。实例对象因此不需要放置指向类型信息的指针。使用句柄访问对象,当对象因为某种原因(带有空间压缩能力的垃圾收集器在垃圾收集时经常移动对象)而移动时,只需要改变句柄中的实例指针。
1.3.2 使用直接指针进行访问
实例对象中需要包含指向类型信息的指针,不需要间接访问对象的开销,效率更高。
2 线程共享的空间
2.1 堆(heap)
2.1.1 所有对象都应在堆上分配
随着逃逸分析技术的成熟,对象也变得不一定要在堆上进行分配。当确定一个对象不会逃逸出线程时,可以在栈上分配对象,这样对象会随着栈帧出栈而销毁(栈上分配)。当确定一个对象不会逃逸出方法时,可以将其拆散,将其用到的成员变量恢复为原始类型来访问(标量替换)(不会创建这个对象,而直接创建它的成员变量)。
2.1.2 Java堆可以处于物理上不连续的空间
大对象如数组为了实现简单、存储高效的目的扔有可能要求连续的空间进行存储。
2.1.3 线程私有的堆(Thread Local Allocation Buffer,TLAB)
这是为了更好地回收内存、更快的分配内存,同时这样还能解决对象分配时的线程安全问题。
2.1.4 Java堆溢出异常分析
2.1.4.1 获取堆转储快照
通过设置 -XX:+HeapDumpOnOutOfMemoryError参数可以上虚拟机在出现内存溢出时Dump出内存转储快照。
2.1.4.2 分析内存异常类型
可以使用内存映像分析工具进行分析(如:Eclipse Memory Analuzer)。
2.1.4.2.1 内存泄漏
对象无法被回收。可以根据泄露对象的类型信息以及它到GC Roots引用链的信息定位到这些对象创建时的位置。
2.1.4.2.2 内存溢出
存活对象过多。可以调整堆大小、优化对象生命周期。
2.2 方法区(Method Area)
在JDK8之后放在了背地内存中实现的元空间之中,之前是放在永久代之中。方法区主要包含了类型信息、静态变量、即时编译器编译后的代码缓存、常量池这几部分。字符串常量池从JDK7起被从永久代移动到了Java堆中。
2.2.1 运行时常量池
主要是Class文件中的常量池表中的各种字面量与符号引用,还有运行期加入的常量(String::inturn()方法可以向运行期常量池中加入字符串常量)。方法区的回收主要是对常量池的回收和对类型的卸载。
2.2.2 元空间防范内存溢出的手段
-XX:MaxMetaspaceSize(默认值为-1,即不限制),-XX:MetaspaceSize(初始大小-在垃圾收集后根据剩余空间大小自动增大或者减小),-XX:MinMetaspaceFreeRatio(垃圾收集后最小的剩余空间百分比)。
3 线程私有的区域
3.1 虚拟机栈和本地方法栈
虚拟机栈为Java方法服务(字节码),本地方法栈为本地方法服务(Native)。一个方法从被调用到执行结束就对应着一个栈帧从入栈到出栈的过程。
3.1.1 栈帧
栈帧用于存放局部变量表、操作数栈、动态连接、方法出口等信息。
3.1.1.1 局部变量表
存放着(编译期可知的)基本数据类型(8种)、对象的引用(直接引用地址或者句柄地址)、returnAddress类型(指向一条字节码指令的地址)。局部变量表所需的变量槽(Slot)数量在编译期就完全确定。64位长度的long和double类型的数据会占用两个变量槽,其余只会占用一个。
3.1.2 栈溢出
3.1.2.1 StackOverflowError异常
线程请求的栈深度大于虚拟机允许的最大深度或者新的栈帧无法分配内存时抛出这个异常。
3.1.2.2 OutOfMemoryError异常
当允许动态扩展时,扩展栈容量申请不到足够的内存抛出这个异常。另一种原因是建立多线程时,新线程的栈申请不到足够内存而抛出这个异常(减少堆内存为栈留出更多内存或者减少栈容量)。
3.2 程序计数器
程序计数器是一块较小的空间,是当前线程所执行的字节码的行号指示器。
3.2.1 程序控制流的指示器
分支、循环、跳转、异常处理器和线程恢复都依赖程序计数器实现。Java线程切换后依赖程序计数器进行恢复,因此每个线程都需要独立的程序计数器。
4 直接内存
JDK1.4中新加入了NIO(New Input/Output)类,它可以使用Native函数库直接分配堆外内存。
可以使用存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作(直接操作堆外内存)。
这样避免了直接在Java堆和Native堆中来回复制数据,在某些场景中能显著提高效率。