Understand JVM Part I
自动内存管理机制
Java的内存区域
1、程序计数(寄存)器
- 可看作当前线程执行的字节码行数指示器,各线程私有,线程切换时可以恢复当前的状态
- 此内存区域是唯一一个不定义OutOfMemoryError的区域
- 如果执行的是Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址,本地方法则为空
2、Java虚拟机栈
- 线程私有,生命周期与线程相同
- 此区域描述的是Java方法执行的内存模型 - 每个方法执行同时在栈中创建一个新的栈元素,即栈帧(Stack Frame), 用于存储局部变量表、操作数栈、动态链接、方法出口等信息(递归调用如果没有终止条件或次数过多,每次递归调用都会创建栈帧,栈就会无法容纳足够的栈帧而溢出)
- 局部变量表在编译期间完成分配,进入方法时,这个方法在栈帧中的局部变量表分配空间完全确定,运行期间不会改变
- 当线程请求的栈深度超过允许抛出StackOverflowError
- 若是虚拟机允许动态扩展此区域(大多数都可以),那么扩展时申请不到足够的内存,抛出OOM
3、本地方法栈
- 类似于虚拟机栈,为Native方法服务
- 也会抛出两种异常SOE、OOM
4、Java堆
- JVM管理的最大一块内存,所有线程共享
- 分配所有的对象实例与数组
- JVM的优化技术如逃逸分析、栈上分配、标量替换会导致上条规则不绝对
- 进一步可分为新生代与老年代或Eden、FromSurvivor、ToSurvivor空间
- 内存分配时也可能会分出线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)
- Java堆划分的目的都是为了更好更快的回收与分配内存
- 堆分配可抛出OOM
- 多线程创建过程中,如果每个线程分配的内存过大,更容易OOM,此时需要减少最大堆与栈容量来换取更多线程(每个进程OS分配的内存是有上限的)
5、方法区
- 线程共享,存储被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码
- HotSpot虚拟机使用永久代来实现方法区,这样可以使得GC一起管理这部分内存
- 未来逐步将采用Native Memory来实现,避免内存溢出,Java8已经使用元空间来实现这一部分
- 分配可抛出OOM
6、运行时常量池
- 方法区的一部分
- 存放Class文件中的各种字面量和符号引用,以及翻译出来的直接引用
- 具备动态性,运行期间也可以将新的常量放入
- 可抛出OOM
7、直接内存
- 不属于运行时数据区,不是Java虚拟机规范中定义的内存区域
- 此部分内存也频繁的使用且会抛出OOM
- NIO类可以使用Native函数直接分配堆外内存,通过存储在Java堆中的DirectByteBuffer对象作为内存的引用进行操作,这样可以避免在Native堆与Java堆复制数据
- 此区域内存不受Java堆限制,当各个区域内存总和大于物理内存时,动态扩展时出现OOM
Java对象
1、对象的创建
2、对象的内存布局
- 对象结构包含对象头(Header)、示例数据(Instance Data)、对齐填空(Padding)
- 32位机器上,非数组对象,对象头一共4 + 4 = 8字节,64为机器为8 + 8 = 16字节,数组长度始终为4字节
其中MarkWord结构如下
由于HotSpot要求对象的起始地址为8字节的整数倍,所有需要Padding来补全
3、对象的访问
- 直接指针访问
本地变量表中存储Java堆中的对象引用,堆中对象存储指向方法区类型数据
- 句柄访问
本地变量表中存储指向句柄池中的对象句柄地址,句柄中包含实例数据地址与类型数据地址,对象移动时,只改变句柄池中的指针,本地变量表指向的是稳定的句柄地址
垃圾收集器与内存分配
1、需要收集的内存
- 程序计数器、虚拟机栈、本地方法栈随线程消亡而消亡(栈帧入栈与出栈)
- 堆(包括方法区)需要垃圾回收器参与
2、寻找可回收的对象
- 引用计数:难以解决循环引用问题
- 可达性分析:Java、C#使用, 通过GC Roots系列对象进行可达性分析,不可用的对象就可以回收,GC Root对象主要包括:栈中的引用(本地变量表)、方法区类静态属性引用、方法区常量引用、本地方法栈中JNI(Native方法)的引用
- 引用的加强:JVM对引用进行了进一步划分
- 强引用:一般情况下的引用,
Object o = new Object()
- 软引用:系统发送OOM之前将这些引用再次回收,如果还不够才OOM, 参见类
SoftReference
- 弱引用:存活至下一次垃圾回收,参见类
WeakReference
- 虚引用:无法通过虚引用来获取一个对象,为一个对象设置虚引用关联唯一目的是在其回收时收到一个通知,参见类
PhantomReference
- 强引用:一般情况下的引用,
3、最后一次挣扎
- 对象判定不可达之后不会马上回收,会进行判定需要不需要执行
finalize()
方法,如果需要执行,进入F-Queue,Finalizer线程会进行处理,如果finalize()
方法中对象成功“自救”则不会回收(重新与引用链挂钩,例如将自己挂在某个类的静态变量下) - 强制!!finalize方法不允许使用!
4、关于方法区
- 存在垃圾回收,主要有废弃常量与无用类
- 废弃常量判断条件:如果没有地方引用某个常量,则垃圾回收时可以回收
- 废弃类判断条件按:该类所有实例被回收、该类的ClassLoder被回收、该类对应的Class对象没有被引用,无法在任何地方通过反射该类的方法
- JVM创建的3个默认的类加载器(BootStrap Classloader、ExtClassLoader、AppClassLoader)加载的系统类(如String)或应用类都不能再运行时释放,无法满足上面的条件
5、垃圾回收算法
1. 标记清扫(Mark-Sweep)
* 效率低、碎片化
* 分配大对象找不到连续内存引发二次清扫
2. 复制(Copying)
* 现代商业虚拟机都采用这种方式
* 将内存分成两块,把存活的对象复制到另一块,整体清理前一块内存
* IBM研究表明大部分对象“朝生夕死”,将内寸分为一大(Eden)二小(Survivor),清理时
将Eden和S1存活的对象复制到S2中,清理掉Eden和S1,HotSpot默认的比例是8:1
* 上条提到的S2不足时,需要老年代进行分配担保,即将无法存放的对象放置老年代
* 两个Survivor区域的操作逻辑目的是为了始终保留一个S区域是空的
3. 标记整理(Mark-Compact)
* 标记后,让存活的对象都向一端移动,直接清理端边界以外的内存
4. 分代回收
* 当前商业虚拟机均采用
* 将内存分代,不同代内存采用不同方式,新生代有大量对象死亡,采用复制算法,而老年代
对象存活率高,没有额外的空间做分配担保,使用“标记清理”或“标记整理”方式
6、HotSpot算法实现
1. 枚举根节点
* 在枚举根节点时,需要停顿所有Java执行线程以保证结果的准确性(Stop The World)
* 使用OopMap数据结构快速直接得知那些地方存在对象引用,类加载完成的时候,HotSpot把对象什么偏移量是什么类型计算出来,
JIT编译过程也会记录栈与寄存器哪些位置是引用,GC扫描时直接获取该信息
2. 安全点(Safepoint)
* 对于具备长时间执行特征的程序,程序执行在到达安全点时才暂停,依靠指令序列重复判定(方法调用、循环跳转、异常跳转)
* 采用主动式中断,在执行指令中加入test执行使得线程自陷,进而中断
3. 安全区域(Saferegion)
* 对于无法进入安全点的程序,如Sleep、Block中的线程,通过判定当前是否进入安全区并标识自身,此时GC时将跳过这些线程
* 线程离开安全区时,检查系统是否完成根节点枚举或GC过程,完成则继续执行,否则等待完成的信号
7、垃圾收集器
- 垃圾收集器是搭配使用的
收集器 | 算法 | 用于分代 | 多线程 | 配对收集器 | 暂停所有用户线程 | 备注 |
---|---|---|---|---|---|---|
Serial | 复制 | 新生代 | N | Serial Old; CMS | Y | |
ParNew | 复制 | 新生代 | Y | Serial Old; CMS | Y | |
Parallel Scavenge | 复制 | 新生代 | Y | Serial Old; Parallel Old | Y | |
Serial Old | 标记整理 | 老年代 | N | Serial; ParNew; Parallel Scavenge; | Y | ( CMS 在Server模式下的备用预案) |
Parallel Old | 标记整理 | 老年代 | Y | Parallel Scavenge | Y | |
CMS(Concurrent Mark Sweep) | 标记清扫 | 老年代 | Y | Serial; ParNew | part | 初始标记与重新标记停所有用户线程 |
G1 | 标记整理(整体) 复制(局部) |
新生代; 老年代 | Y | - | part | 初始标记、最终标记、筛选回收暂停所有用户线程 |
8、内存分配与回收策略
- 对象首先在Eden上分配,空间不足时,JVM发起Minor GC(新生代GC 对应的老年代GC称为 Major/Full GC)
- 大对象直接进入老年代(例如大的字符串或数组)
- 长期存活的对象将进入老年代(每个对象定义了一个Age计数器,每次Minor GC存活Age+1,超过阀值进入老年代)
- Survivor中相同年龄所有对象大小总和超过一半的,年龄大于该数的进入老年代,不需要阀值条件
- JDK 1.6U24之后,分配对象时若老年代连续空间大于新生代对象总大小或每次晋升的平均值就会发生Minor GC,否则进行Full GC, 之前版本依赖HandlePromotionFailure参数进行判断
- Perm区域的回收由Full GC触发