JVM
# 《深入理解Java虚拟机》阅读笔记
# 1. 虚拟机介绍
# 虚拟机家族
Sun Classic
HostSPot VM
经典虚拟机
Mobile / Embedded VM
移动端/嵌入式使用
BEA JRockit / IBM J9 VM
BEA/IBM公司自有虚拟机
...
# 2.内存区域与OOM
# 内存区域
java内存区域遵循《JAVA虚拟机规范》并由具体虚拟机进行划分和实现
程序计数器
占用空间极小,用于指示执行时字节码行号 各线程程序计数器私有,不存在OOM异常
java虚拟机栈
描述的是JAVA方法执行的线程内存模型,方法执行时构造栈帧
栈帧包含:
- 局部变量表
- 操作数栈
- 动态连接
- 方法出口
hotSpot的虚拟机栈是不可以动态扩展的 当线程申请栈空间失败时,会出现OOM异常
可能出现的异常:
- 线程请求时的栈深度大于虚拟机所允许的最大深度, 抛出StackOverflowError
- 如栈容量动态扩展,当无法申请到组后内存时,排除OOM Error
本地方法栈
为虚拟机使用到的本地方法(Native)服务 抛出异常与虚拟机栈相同
JAVA堆
虚拟机锁管理的内存中最大的一块区域,被所用线程共享、虚拟机启动时创建,用于存放对象实例(“所有的对象实例以及数组都应当在堆上分配”) JAVA堆也是垃圾收集器管理的内存区域, 从回收内存角度,和垃圾分代收集理论,堆场被分配新生/老年/永久、eden/from/to等。HotSpot内部垃圾收集器基于经典分代设计。 从分配内存角度,所有线程共享的java堆可以划分出多个线程私有的分配缓冲区(TLAB)以提高对象分配的效率
- 经典分代 即新生代(包含一个eden区和两个survivor区)、老年代划分
JAVA方法区
各个线程共享的内存区域,主要用于存放被虚拟机加载的类型信息、常量、静态变量、即时编译后的代码缓存等 《JAVA虚拟机规范》中方法区的描述是堆的一块逻辑部分 jdk1.8以前,HotSpot虚拟机的方法区的实现称为永久代,部分数据占用堆内存 1.8及之后,HotSpot虚拟机方法区的实现称为元空间,使用本地内存
在方法区的垃圾回收主要目的是针对常量池的回收和对类型的卸载,类型的未完全回收有概率引发内存泄漏,从而造成OOM异常
# 对象的创建
- 方法区检查Class类是否被加载,如果没被加载,则进行类文件加载
- 读取Class类元数据,分配堆空间给新对象
- 堆空间规整:指针碰撞
- 堆空间杂乱:空间列表
- 多线程并发:TLAB OR CAS+失败重试
- 分配的内存空间归零处理,
- 根据类元空间设置对象头信息,
- 调用
<init>()
方法,完成对象资源的初始化
对象构成: [对象头] + [实例数据] + [对齐填充]
对象头
针对于HotSpot虚拟机,对象头由动态数据结构(Mark work)和类型指针组成
动态数据结构 一个有着动态定义的数据结构,根据对象的状态复用自己的存储空间 用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标识、偏向锁线程ID、偏向锁时间戳等等 数据长度:32比特(32位系统)、64比特(64位系统)
Mark work
对象哈希码(25bit)
对象分代年龄(4bit)
锁标志位(2bit)
固定0(1bit)
类型指针 对象指向它的类型元数据的指针,java通过这个指针来确定该对象是哪个类的实例
如果对象是一个java数组,那么在对象头中还必须有一块用于记录数组长度的数据。
对象的访问定位
- 使用句柄池间接访问堆中对象
- 使用直接指针指向堆中对象
# 调试参数
用于堆
设置堆最大值-Xms
设置堆最小值-Xmx
内存溢出时Dump出当前内存堆转储快照-XX:+HeapDumpOnOutOfMemoryError
2
3
4
虚拟机栈
设置栈容量-Xss
用于方法区
<=jdk6
设置永久代最小-XX:PermSize
>= jdk7
设置永久代最大-XX:MaxPermSize
>=jdk8
设置元空间最大值-XX:MaxMetaspaceSize
设置元空间初试大小-XX:MetaspaceSize
控制垃圾收集后元空间最小剩余容量百分比-XX:MinMetaspaceFreeRatio
2
3
4
5
6
7
8
# 3.垃圾收集器与内存分配策略
# 垃圾收集器
名词解释
垃圾收集 (Garbage Collection):GC,又可称为自动内存管理子系统
# 对象回收判定
引用计数算法
在对象中添加一个引用计数器,每当一个地方引用它时,计数器加一,引用失效时减一;当计数器值为零时即证明该对象不可能再被使用。 优势:
- 判定原理简单
- 判定效率高 劣势:
- 占用一些额外的内存空间
- 必须配合大量额外条件处理 单纯的引用计数器很难解决对象之间相互循环引用的问题
可达性分析算法
当前主流的商用程序算法的内存管理子系统所使用的算法
通过一系列被称为GC Roots
的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,搜索过程所走过的路径称为引用链
,当对象到GC Roots
间没有任何引用链相连(从GC Roots到这个对象不可达时),则证明该对象不可能再被使用。
可作为GC Roots的对象:
- 在虚拟机栈中引用的对象
- 在方法区中类静态属性引用的对象
- 在方法区中常量引用的对象
- 在本地方法栈中JNI引用的对象
- 虚拟机内部的引用
- 所有被同步锁持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
- 垃圾收集器所设置的其他
临时性
对象
PS:
当进行分代收集、局部回收时,必须考虑到内存区域是虚拟机自己的实现细节,需要将被位于堆中其他区域相关引用的对象一并加入到GC Roots集合中去,才能保证可达性分析的正确性。
引用
引用定义: 在JDK1.2以前: 如果Reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为该reference数据是代表某块内存、某个对象的引用。 在JDK1.2以后: 将引用分为强引用、软引用、弱引用、虚引用。四种引用强度依次减弱。
- 强引用
指在程序代码之中普遍存在的引用赋值,
e.g: Obeject obj = new Object()
只要该引用关系存在,垃圾收集器永远不会回收掉被引用的对象。 - 软引用
描述一些还有用,但非必须的对象。
只被软引用关联的对象,在系统将要发生内存溢出异常前,会将这些对象列进回收范围之内进行第二次回收,回收后重新进行内存溢出判断。
JDK1.2后提供
SoftReference
类实现软引用。 - 弱引用
描述一些还有用,但非必须的对象。但比软引用强度更弱。
被弱引用关联的对象只能生存到下一次垃圾收集发生前
JDK1.2后提供
WeakReference
类实现软引用。 - 虚引用
一个对象是否有虚引用的存在,不会对其生存时间构成影响。
唯一目的是为了能在引用对象被回收时收到一个系统通知
JDK1.2后提供
PhantomReference
类实现软引用。
回收前阶段
当一个对象被判断为不可达对象时,在回收前还需要经过两次标记过程。
- 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记
- 随后进行筛选,筛选条件是此对象是否有必要执行finalize()方法。
- 若该对象没有覆写finalize()方法,或finalize()方法已被虚拟机调用过,虚拟机将认为无需执行finalize()
- 在虚拟机执行finalize()方法时,该对象进入
F-Queue
队列中,稍后由虚拟机自动建立、低调度优先级的Finalizer线程去执行finalize()方法 执行指触发这个方法开始运行,但不保证一定会等待它运行结束。 - 垃圾收集器将对F-Queue中的对象进行第二次小规模标记,如果对象重新与引用链中的对象建立关联,则移出
即将回收
的集合,否则对象被回收。
注意:
- Finalize()只会被系统调用一次。
- finalize()是对象逃脱回收的最后一次机会。
- 不建议通过该方式使对象保持存活,使用
try-finally
或其他方式可以更好、更及时。
回收方法区
《Java虚拟机规范》:可以不要求虚拟机在方法区中实现垃圾收集。 在Java堆中,尤其是新生代的常规垃圾收集通常可以回收70%至99%的内存空间。 方法去垃圾收集主要回收部分
- 废弃的常量
- 当前系统中没有任何一个字符串对象引用常量池中的常量,且虚拟机中没有其他地方引用这个字面量,则该常量被系统清理出常量池。
- 常量池中的其他类、接口、方法、字段的符号引用类似。
- 不再使用的类型,如果该类同时满足三个条件,则允许虚拟机对其进行回收:
- 该类所有的实例都已经被回收,即java堆中不存在该类及其任何派生子类的实例。
- 加载该类的类加载器已经被回收。
- 该类对应的java.lang,Class对象灭有在任何地方被引用,即无法在任何地方通过反射访问该类的方法。
# 控制虚拟机是否对类型进行回收
-Xnoclassgc
# 查看类加载和卸载信息
-verbose:class #可以在Product版虚拟机中使用
-XX:+TraceClassLoading #可以在Product版虚拟机中使用
-XX:+TraceClassUnLoading #可以在FastDebug版虚拟机中使用
2
3
4
5
6
7
# 垃圾收集算法
从如何判定对象消亡的角度出发,可以被分为:
- 引用计数式垃圾收集(Reference Counting GC),即直接垃圾收集
- 追踪式垃圾收集(Tracing GC),即间接垃圾收集
分代收集理论
基于两个分代假说所建立的分代收集理论
被当前大多数的商业虚拟机的垃圾收集器所遵循。
两个分代假说:
- 弱分代假说:绝大多数对象都是朝生夕灭的;
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡;
分代假说奠定垃圾收集器的设计原则:收集器应该将java堆划分出不同区域,然后将回收对象依据年龄分配到不同的区域中存储。 (对于大多数对象,每次回收只需要关注少量存活,以较低代价回收到大量空间; 对于难以消亡对象,较低频率回收,兼顾垃圾收集的时间开销和内存的空间利用率)
根据划分出的不同区域,进而出现Minor GC、Major GC、Full GC等回收类型,并发展出不同的回收算法。
对于当前的商用java虚拟机,一般将Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。 对于新生代: 每次垃圾收集都会有大量对象死亡,每次回收后存活的少量对象逐步晋升到老年代存放; 初期HotSpot提供分代式垃圾收集器框架,但基于框架实现的收集器只有最早期的两组四款收集器;
由于存在新生代的对象可能会被老年代所引用,进而需要在固定的GC Roots之外,额外遍历争个老年代中所有对象来确保可达性分析结果的正确性,导致带来很大的性能负担,所以需要堆分代收集理论进行补充: 补充分代假说: 3. 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。 即存在相互引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。 e.g: 某个新生代的对象存在跨代引用,由于老年代对象难以消亡,该引用会使新生代对象在收集时存活,随年龄增大后晋升到老年代中,此时跨代引用随即消除。
基于补充分代假说,只需要在新生代建立一个全局数据接口(记忆集)将老年代划分为若干小块,标识出老年代的哪一块内存会存在跨代引用。当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入GC Roots中进行遍历扫描。 该方式需要在对象改变引用关系时维护记录数据的正确性,增加一些运行时开销。
收集行为
- 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集。
- 新生代收集(Minor GC/Young GC):指目标在新生代发生的垃圾收集
- 老年代收集(Major GC/Old GC):指目标在老年代发生的垃圾收集;有单独收集老年代行为的收集器:CMS收集器
- 混合收集(Mixed GC):目标是收集整个新生代以及部分老年代的垃圾收集。 有混合收集行为的收集器:G1收集器
- 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
标记-清除算法
在1960年由John McCarthy提出 算法分为:标记、清除阶段:
- 标记阶段:标记出所有需要回收或存活的对象
- 清除阶段:标记完成后,统一回收掉所有被标记或未被标记的对象。
缺陷:
- 执行效率不稳定,标记清除阶段的执行效率随对象数量增长而降低
- 内存空间碎片化,标记-清除后产生大量不连续的内存碎片,空间碎片太多可能导致之后程序运行时无法找到组后的连续内存而不得不触发另一次垃圾收集动作。
标记-复制算法
==为解决标记-清除算法面对大量可回收对象时执行效率低的问题== 1969年由Fenichel提出的一种称为半区复制的垃圾收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当当前块内存用完时就将还存活的对象复制到另一块上,然后将已使用过的内存空间一次清理掉。
分配内存时不用考虑有空间碎片的复杂情况,只需要移动堆顶指针,按顺序分配即可 实现简单,运行高效 缺陷:
- 如果内存中多数对象都是存活的,这种算法将产生大量的内存间复制的开销。
- 可用内存缩小为原来的一半,空间利用率太低
大多数虚拟机采用该算法在回收新生代时使用
在1989年, Andrew Appel针对“朝生夕灭”特点的对象,提出更优化的半区复制分代策略(Appel式回收) HotSpot的Serial、ParNew等新生代收集器均采用该策略来设计新生代的内存布局 Appel式回收:
- 把新生代分为一块较大的Eden空间和两块较小的Survivor空间
- HotSpot默认Eden和Survivor大小比例是8:1,即可用内存为新生代的90%
- 每次分配内存只使用Eden和其中一块Survivor。
- 发生垃圾收集时,将Eden和使用的Survivor中仍存活的对象一次性复制到另一块Survivor中,然后直接清理掉Eden和使用过的Survivor
- Appel式回收通过“逃生门”的安全设计解决当回收的的对象多余新生代的10%时的问题:
- 当Survivor空间不足以容纳一次Minor GC之后存活的对象时,需要依赖其他内存区域进行分配担保
- 多出的对象通过分配担保机制直接进入老年代。
标记·整理算法
# HotSpot算法细节
根节点枚举
安全点及安全区域
HotSpot只在特定的位置记录信息,这些位置被称为安全点, 也就决定了用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到安全点后才能够暂停。
记忆集与卡表
写屏障
并发可达性分析