Java 虚拟机总结
JVM(Java虚拟机)
JVM 是 JRE 包含的 Java 程序运行所需的程序,屏蔽各种硬件和操作系统的内存访问差异,通用一份 jar 包。 另外,JVM还提供了内存管理的功能,使得开发人员无需关心底层内存如何管理。
类加载机制
Java虚拟机的类加载有加载 (load)、链接 (link)、初始化 (initialize) 三个步骤
当一个类被实例化时或者类中静态方法被调用时将进行类加载
加载
将不同的类数据源以字节码的形式加载到内存中以供类加载器使用,数据的来源可以是 jar 包、class 类、网络数据,一个类数据有且只有一个存在于一个类加载器中。
正常的类加载是以双亲委任机制,不同的类加载器是以继承的方式链接的,当通过某个类加载器加载一个类数据时,当这个类加载器存在父类加载器时,那么它会先从父类加载器中寻找类数据,不存在再在自身中寻找,并且父类也是遵循这个机制。
Tomcat的类加载器重写了这个加载机制,会优先在自身中查找,这跟Tomcat所加载类的所在位置有关。
链接
链接包含了验证、准备、解析三个步骤
-
验证确保了类加载的正确性,它校验了字节码数据是符合Class类规范,常量类型是否支持,语义分析,分析数据流和控制流校验程序语义,符号引用校验。
-
准备环节是为一个类的静态域分配内存空间,并赋予零值。
-
解析负责转化类中的符号引用,将类引用转换为直接引用,将类中的常量值转换为常量池中引用。
如果类字段的字段属性为
ConstantValue
,即同时被final
、static
修饰的基础类型数据,并且在定义时即赋值,如static final String CONSTANT_COMPILE = "java";
,那么在准备阶段变量就会被初始化为属性所指定的值。
初始化
对类的静态域赋予正确的初始值,执行静态代码块为赋值静态域。
异常
-
ClassNotFoundException 这种错误是发生在加载环节,当一个类在双亲委任机制中无法获取到时,便会抛出此异常。常见情景是依赖包的冲突。
-
NoClassDefFoundError 这个错误主要是发生在链接环节的解析中,当一个类的静态域引用至另一个类中的属性,而这个类无法获取时,那么虚拟机就会抛出此异常。
运行时数据区域
Java 虚拟机在运行程序的过程中把内存数据划分为不同的区域
线程隔离的(指令区)
-
程序计数器:指向线程下一个执行的指令的地址(
本地指针
或者起始指令的偏移量
),当执行的是本地方法时为 undefined。 许多操作都需要依赖程序计数器来完成,例如在时间片抢占后切换线程能够恢复到正确的位置。 -
虚拟机栈:包含了线程生命周期的方法调用,一个 Java 方法调用即为一个栈帧,根据调用顺序压入线程栈空间。 栈帧存储当前线程运行方法所需要的局部变量表(基本数据类型、对象引用)、操作数栈、动态链接、方法返回地址,保证了多线程下调用方法的隔离性。
-
本地方法区:与虚拟机栈作用相似,区别是执行 native 方法。
线程共享的(数据区)
-
方法区/元空间 (MetaSpace):存放类信息、静态域数据(对象实例存于堆中)、类编译期间生成的各种字面量和符号引用、字节码、JIT 编译后的机器码、动态代理产生的数据,使用本地内存存储,几乎不会被回收。
-
堆 (Heap):运行时常量池、对象实例域,是垃圾收集管理的主要区域。可分为新生代、老年代。
-
直接内存:用于 NIO 数据交换的内存空间,只受实际内存及 JVM 参数限制。
内存溢出
内存溢出是由于虚拟机空间分配失败所导致的致命性错误。
-
方法栈 (StackOverFlowError) 常见原因有递归或大循环调用方法导致栈帧数量过多、线程内定义大量的本地变量。
-
堆 (OutOfMemoryError) 常量池溢出、线程持续占有对象都将可能导致堆溢出。
-
方法区 (OutOfMemoryError) 主要原因为动态创建大量的类,并且卸载无法满足新的元类存储。
执行引擎
解释器 (Interpreter)
解释字节码,执行相应的命令
分析器 (Hotspot profiler)
将频繁调用的热点方法编译成与本地平台相关的机器码
JIT (即时编译器)
优化解释器,将字节码翻译成本地平台相关的机器码执行 常见的 JIT 有 C1、C2,在 Java10 引入 Graal
常见的 JIT 优化手段有
-
公共子表达式消除 当一个大表达式已经被计算过后,再次出现已经包含了的表达式则不必重新计算,直接用结果代替。
-
数组边界检查消除 消除大循环体内对元素越界检查
-
方法内联 将频繁调用方法替换为调用方法代码
-
逃逸分析 当开启了标量替换 (-XX:+EliminateAllocations) 和逃逸分析 (-XX:+DoEscapeAnalysis) 后,会对线程栈内的对象进行分析,将只存活于栈帧内的可分解对象进行基础类型数据替换处理。
-
同步消除 (-XX:+EliminateLocks) 在开启逃逸分析和 -server 模式后,将会对无多线程竞争的锁进行消除。
-
优化技术
编译器策略:延迟编译,分层编译,栈上替换,延迟优化,程序依赖图表示,静态单赋值表示。
基于性能监控的优化技术:乐观空值断言,乐观类型断言,乐观类型增强,乐观数组增强,裁剪未被选择的分支,乐观的多态内联。分支频率预测,调用频率预测
基于证据的优化技术:精确性推断,内存值推断,内存值跟踪,常量折叠,重组,操作符退化,空值检查消除。类型检测退化,类型检测消除,代数化简,公共子表达式消除
数据流敏感重写:条件常量传播,基于六承载的类型缩减转换,无用代码消除
语言相关的优化技术:类型继承关系分析,去虚拟机化,符号常量传播,自动装箱,消除逃逸分析,锁消除,锁膨胀,消除反射
内存及代码位置交换:表达式提升,表达式下沉,冗余存储消除,相邻存储合并,交汇点分离
循环变换:循环展开,循环剥离,安全点消除,迭代分离,范围检查消除
局部代码调整:内联,全局代码提升,基于热度的代码分离,Switch 调整
控制流图变换:本地代码编排,本独代码封包,延迟槽填充,着色图寄存器分配,线性扫描寄存器分配,复写聚合,常量分裂,复写移除,地址模式匹配。指令窥孔优化,基于确定有限状态机的代码生成
- Graal [jdk10] 启动方式 -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler
GC(垃圾收集器)策略
Java虚拟机在运行时会产生大量的对象,有些对象将不会再被使用,为了使内存不被这些无用的对象占用,垃圾收集器就需要分析出已经死亡的对象,将其所用空间回收。
虚拟机从一部分称为 GC Roots
的节点开始搜索引用链,找出存活的引用 (并非特指对象),对其余对象标记为虚引用,准备下一阶段进行回收释放内存空间。对重写了 finilize() 方法对象封装并放入队列中,在 GC 结束后单线程执行方法后移除。
Serial
标记清除整理
算法,单线程进行垃圾回收,无上下文切换开销,但 cpu 利用率较低,并且无法与应用线程并行进行。
Parallel
基于吞吐量优先的并行收集器,对年轻代使用 标记复制
算法,对老年代使用 标记清除整理
算法,适用于多核处理器,有效利用系统资源。
CMS(Concurrent Mark Sweep)
以 标记清除
算法为基础,以响应时间优先的并发收集器,对年轻代使用 标记复制
算法,对老年代使用 标记清除
算法。
使用 空闲空间列表
来管理老年代内存,在老年代 GC 阶段大部分工作可以与应用线程并发执行,并且可在老年代空间内存利用率达到阀值时触发 CMS GC。
收集工作分为数个阶段:初始标记、并发标记、并发预清理、并发可取消的预清理、最终标记、并发清除、并发重置
CMS 由于不进行内存整理容易造成老年代内存碎片,并且当堆内存较大时,可能产生不可预估的暂停时间。
G1(Garbage-First)
以 复制
算法为基础,为了能够得到良好的停顿时间而产生的一款实时收集器。
将内存空间以块 (Region) 进行分配空间,一般划分 2048 个,优先回收大垃圾的回收机制。使用空闲空间列表来管理所有内存。
虚拟机在分配空间时会选定一个 Region(TLAB将分别指定一个Region),当 Region 的剩余空间不足以分配对象或者小于最小可空间时,将重新指定下一个 Region 分配空间。
HRegion
当分配对象的大小大于 Region 空间的一半,将会分配一个 Humongous Region 直接进入老年代,省略年轻代的内存复制过程,这个 Humongous Region 的大小将为能容纳对象的最小 Region 空格倍数。
SATB (snapshot-at-the-beginning)
对象初始化时的快照,引用变化时使用 write barrier
进行更新,在 GC 中利用了此列表进行扫描。
RSet
与 CMS 不同,G1 使用了记录 Region 引用关系的 Remembered Set
来记录,内部为多个 Card Table
,并且是用于记录 被引用
的对象集合。
使用 三色标记法
+ 写屏障(write barrier)
来更新其引用关系。
G1 解决了 CMS 中的各种疑难问题, 包括暂停时间的可预测性, 并终结了堆内存的碎片化。
ZGC
是一个可伸缩的低延迟垃圾收集器,暂停时间不超过10毫秒,暂停时间不会随堆或实时设置大小而增加
https://blog.csdn.net/renfufei/article/details/54885190 https://www.zhihu.com/question/53613423/answer/135743258
内存模型(JMM)
heap 根据空间利用率、垃圾回收存活年龄分为年轻代、老年代,永久代。
年轻代
年轻代又可细分为 Eden空间、S0空间 (From Survivor)、S1空间 (To Survivor) Eden Space 中存在线程私有的空间 TLAB,是每个线程的缓冲区,存放一些用过即丢弃的对象。
老年代
当新生代的对象超过设定年龄,或者同龄对象达到幸存区的一半,这些对象将被划入老年代(Old Space)。 只有当老年代空间不足分配发生 Full GC 时 (CMS 中可以为 CMS GC),才会对老年代的数据进行回收。
永久代
用于存放元类数据,在并发标记时和类加载器卸载时将会对相关数据进行回收。 当一个类加载器死亡时,相对应的元数据也被销毁,释放其块空间。
PermgenSpace
java8 之前的永久代实现,也称为方法区,为一块固定空间大小,当空间不足时会进行Full GC。 由于区空间大小固定,在大量动态创建类的程序中容易造成OOM。
MetaSpace
源自 JRockit,在 Java8 与 Hotspot 合并,将 native method area 概念加入 Hotspot,由此得来 MetaSpace 替换原有的 PermGen,容量仅受可用的本地内存限制。
MetaSpace 的空间是以块 (Chunk) 为单位,这个块的大小取决与申请空间的类加载器类型。
当类加载器加载类时,从块分配器中获取一份块内存空间存在类元数据,并映射此地址。
由于每个类加载器申请的块大小不一致,MetaspaceVM 也还未使用压缩技术,这就容易导致内存碎片的产生。
TLAB
当大量线程申请空间时,JVM 需要对并发操作保障不会发生指针碰撞,这样便增加了复杂性,降低性能。
因此,在 jdk1.6 以后便引入了 TLAB 技术。
TLAB 全名 Thread-local allocation buffers,是在线程初始化的时候在堆中新生代申请一块线程私有的分配空间(允许所有线程访问),减少同步开销,使用参数 -XX:UseTLAB
开启。
PLAB
全名 promotion-local allocation buffers,用于 Young GC 时的空间分配及复制。
其他还有 CLAB,全名 core-local allocation buffers,用于全局的空间分配。