Java 虚拟机总结

JVM(Java虚拟机)

JVM 是 JRE 包含的 Java 程序运行所需的程序,屏蔽各种硬件和操作系统的内存访问差异,通用一份 jar 包。 另外,JVM还提供了内存管理的功能,使得开发人员无需关心底层内存如何管理。

https://www.youtube.com/watch?v=ZBJ0u9MaKtM

类加载机制

Java虚拟机的类加载有加载 (load)、链接 (link)、初始化 (initialize) 三个步骤

当一个类被实例化时或者类中静态方法被调用时将进行类加载

加载

将不同的类数据源以字节码的形式加载到内存中以供类加载器使用,数据的来源可以是 jar 包、class 类、网络数据,一个类数据有且只有一个存在于一个类加载器中。

正常的类加载是以双亲委任机制,不同的类加载器是以继承的方式链接的,当通过某个类加载器加载一个类数据时,当这个类加载器存在父类加载器时,那么它会先从父类加载器中寻找类数据,不存在再在自身中寻找,并且父类也是遵循这个机制。

Tomcat的类加载器重写了这个加载机制,会优先在自身中查找,这跟Tomcat所加载类的所在位置有关。

链接

链接包含了验证、准备、解析三个步骤

  1. 验证确保了类加载的正确性,它校验了字节码数据是符合Class类规范,常量类型是否支持,语义分析,分析数据流和控制流校验程序语义,符号引用校验。

  2. 准备环节是为一个类的静态域分配内存空间,并赋予零值。

  3. 解析负责转化类中的符号引用,将类引用转换为直接引用,将类中的常量值转换为常量池中引用。

如果类字段的字段属性为 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 开启。

https://www.jianshu.com/p/cd85098cca39

PLAB

全名 promotion-local allocation buffers,用于 Young GC 时的空间分配及复制。

其他还有 CLAB,全名 core-local allocation buffers,用于全局的空间分配。