《深入理解Java虚拟机》阅读笔记

内存区域

Java运行时内存区域

  • 程序计数器:是当前线程所执行的字节码的行号指示器

  • 虚拟机栈:Java方法执行的线程内存模型

  • 本地方法栈:为虚拟机使用到的本地(Native)方法服务

  • 堆:存放对象实例

  • 方法区:于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据

    • 运行时常量池:编译期生成的各种字面量与符号引用

对象的分配、布局和访问

对象的创建

  1. 检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。

  2. 分配内存

    • 指针碰撞:若内存绝对规整,中间放着一个指针作为分界点的指示器,则只需是把那个指针向空闲空间方向挪动一段与对象大小相等的距离。

    • 空闲列表:若非规整,维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例。

  3. 分配到的内存空间(但不包括对象头)都初始化为零值

  4. 设置对象头

  5. 若new指令后面是否跟随invokespecial指令,则执行构造函数

对象的内存布局

三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

对象头

包括两类信息:

  1. 用于存储对象自身的运行时数据(Mark Word),如哈希码(HashCode)、GC分代年龄、锁状态标志等。

  2. 类型指针,用于确定该对象是哪个类的实例。

对象的访问定位

主流的访问方式:

  • 句柄访问:堆中划出一块内存作为句柄池,referenc 存储对象的句柄地址。句柄中包括实例数据与类型数据各自具体的地址信息。
  • 直接指针:reference直接存储对象地址。对象实例数据需要保存到类型数据的指针。

垃圾回收

引用的分类

  • 强引用:引用赋值引起的引用。永远不会被回收。

  • 软引用:SoftReference 实现的引用。在将要发生内存溢出前会被回收。

  • 弱引用:WeakReference 实现的引用。只能生存到下次垃圾回收。

  • 虚引用:PhantomReference 实现的引用。无法获知其生存时间,无法取得对象实例。唯一用处是在这个对象被回收时收到通知。

确定对象

GC Roots 例子:

  • 虚拟机栈中引用的对象

  • 常量引用的对象

  • JNI引用的对象

  • 虚拟机内部的引用

  • 同步锁持有的对象

第一次标记:对象在进行可达性分析后发现没有与GC Roots相连接的引用链。

第二次标记:

  • 没有必要执行 finalize() 方法的对象(没有重写 finalize() 或已被调用)会被回收。

  • 有必要执行 finalize() 方法的对象会被放置在 F-Queue 队列中,然后由虚拟机建立的低优先级的 Finalizer 线程触发方法的运行(不一定等待其结束)。稍后进行第二次可达性分析标记。两次被标记的对象会被回收。

对于方法区,主要回收废弃的常量和不再使用的类型。对于要被回收的类型,需要同时满足下面三个条件才会被允许(未必真的进行):

  • 所有实例都被回收

  • 类加载器被回收

  • 对应的 Class 对象没有被引用

分代收集理论

三个假说:

  • 弱分代假说:绝大多数对象都是朝生夕灭的。

  • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

  • 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。

垃圾收集算法

  • 标记-清除算法:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。

  • 标记-复制算法:

    • 新生代分为一块较大的 Eden 空间和两块较小的 Survivor 空间。

    • 每次分配内存只使用 Eden 和其中一块 Survivor。

    • 发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,清理掉其他两块空间。

    • 当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖老年代等进行分配担保

  • 标记-整理算法:标记后让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

HotSpot 垃圾收集细节

根节点枚举

根节点枚举至今还是必须在一个保障一致性的快照中才得以进行,即根节点的对象引用关系不会变化。

收集器可以采用准确式垃圾收集。类加载完成后会将对象内什么偏移量是什么类型的数据计算出来,存在 OopMap 中。

安全点

HotSpot 只会在“安全点”处生成 OopMap。安全点往往选取在方法调用、循环跳转、异常跳转等“长时间执行”的地方。

为使垃圾收集时所有线程都在安全点,有两种中断方法:

  • 抢先式中断:首先把所有用户线程中断,如果有线程不在安全点,恢复该线程执行一会儿再中断,直到跑到安全点。现已几乎不再使用。

  • 主动式中断:需要中断时,设置标志位,各线程执行时不断轮询标志位,一旦发现标志位为真则主动在最近的安全点上中断。

安全区域

安全区域指该区域内引用关系不会发生变化,垃圾收集时可以不去管在安全区域的线程。用户线程执行到安全区域内时,首先标识自己进入了安全区域;当要离开时,等到虚拟机完成了根节点枚举后才离开。

记忆集和卡表

记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。为节约成本,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,有以下记录粒度:字长精度、对象精度、卡精度。

卡精度最常用,指用卡表实现记忆集,可以字节数组来实现,每一元素对应一块特定大小的内存块(卡页)。

1
CARD_TABLE [this address >> 9] = 0;

写屏障

应用写屏障后,虚拟机为所有赋值操作生成相应的指令,更新维护卡表。

为了缓解伪共享问题,即多线程修改相近的卡表内容,导致的竞争缓存行的压力,可以在先检查后再修改卡表。

并发的可达性分析

三色标记:

  • 白色:对象尚未被垃圾收集器访问过。

  • 黑色:对象已被访问,且其所有引用已被扫描。

  • 灰色:对象已被访问,但还有引用没有被扫描。

当且仅当以下两个条件同时满足,会产生“对象消失”问题:

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用。

  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

两种解决方案:

  • 增量更新:对于第一个条件中的黑色对象,在并发扫描结束后作为根重新扫描一次。

  • 原始快照:将第二个条件中的引用都记录下来,将其中的灰色对象为根沿这些记录下的引用重新扫描一次。

经典垃圾收集器

Serial 收集器

新生代收集器。只有一个 GC 线程,需要 STW。采用复制算法。

ParNew 收集器

新生代收集器。多个 GC 线程,需要 STW。采用复制算法。

Parallel Scavenge 收集器

新生代收集器。多个 GC 线程,需要STW。采用复制算法。

它的目标是达到一个可控制的吞吐量(需要运算效率的程序关注),而非缩短用户线程的停顿时间(交互性强的程序关注)。

$$
吞吐量=\frac{用户代码时间}{用户代码时间+垃圾收集时间}
$$

Serial Old 收集器

Serial 的老年代版本。只有一个 GC 线程,需要 STW。采用整理算法。

Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本。多个 GC 线程。采用整理算法。

CMS 收集器

老年代收集器。采用清除算法。整个过程分为四步:

  • 初始标记:标记 GC Roots 能直接关联到的对象。STW。

  • 并发标记:从 GC Roots 的直接关联对象开始遍历整个对象图。

  • 重新标记:增量更新。STW。

  • 并发清除:清理删除掉标记阶段判断的已经死亡的对象。

缺点:

  • 对处理器资源敏感。

  • 无法处理“浮动垃圾”(收集过程中产生的垃圾);可能导致并发失败(CMS运行期间预留的内存无法满足分配新对象的需要),此时需要临时启用 Serial Old。

  • 产生空间碎片。

G1 收集器

停顿时间模型:能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。

G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region)。

让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的那些Region。

细节:

  • 跨Region引用:每个Region都维护有自己的记忆集,会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。

  • 并发标记时保证收集线程与用户线程互不干扰:原始快照。

四个步骤:

  • 初始标记:标记一下GC Roots能直接关联到的对象。修改两个 TAMS 指针的值,并发回收时新分配的对象地址都必须要在这两个指针位置以上。借用进行Minor GC的时候同步完成。

  • 并发标记:从GC Root开始对堆中对象进行可达性分析。

  • 最终标记:处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。STW。

  • 筛选回收:更新Region的统计数据,制定回收计划,组成回收集,应用复制算法。STW。

低延迟垃圾收集器

目标是高度相似的,都希望在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。

Shenandoah 收集器

同样基于 Region。不同是,支持并发的整理算法,默认不支持分代收集,使用全局连接矩阵记录跨 Region 的引用关系。

  • 初始标记:与 G1 相同。标记与 GC Roots 直接关联的对象。STW,停顿时间只与 GC Roots 数量相关。

  • 并发标记:与 G1 相同。标记可达对象。与用户线程并发。

  • 最终标记:与 G1 相同。处理剩余 SATB 扫描,标记回收集。一小段停顿。

  • 并发清理:清理那些一个存活对象都没有的 Region。

  • 并发回收:核心差异。将回收集的存活对象先复制一份到其他未被使用的 Region 之中。rooks Pointers实现与用户线程并发。

  • 初始引用更新:建立一个线程集合点,确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务。会有非常短暂的停顿。

  • 并发引用更新:线性地搜索出引用类型,把旧值改为新值即可。与用户线程并发。

  • 最终引用更新:修正存在于 GC Roots 中的引用。最后一次停顿。

  • 并发清理:清理所有回收集的 Region。

Brooks Pointer:在对象头前面统一增加一个新的引用字段,在正常不处于并发移动的情况下,该引用指向对象自己。当对象拥有了一份新的副本时,只需要修改旧对象上转发指针的引用位置,使其指向新对象。两个问题:

  • 用户线程与收集线程对对象进行并发写:采用 Compare And Swap 解决。

  • 需要同时使用读写屏障,读屏障造成很大压力:采用引用访问屏障,即只拦截引用类型。

ZGC 收集器

采用基于Region的堆内存布局,不同是,可以动态创建和销毁,以及动态的区域容量大小。

采用读屏障和染色指针实现并发整理:将指针可供使用部分的高4位存储存放三色标记状态、被移动过、只能通过 finalize() 访问。优势:

  • “自愈”特性,只慢一次,且一旦某个 Region 的存活对象移走后,该Region能够立即被释放。因此理论上只需要一个空闲 Region 用于收集。

  • 不需要任何写屏障(一方面由于染色指针不需要记录对象引用的变动情况,另一方面 ZGC 暂不支持分代收集因此没有跨代引用问题)。

  • 可扩展的存储结构。

为了实现染色指针正常寻址,采用多重映射,将多个不同的虚拟内存地址映射到一个物理内存的转发表。

ZGC 运作过程:

  • 并发标记:类似于G1、Shenandoah的可达性分析,需要经过初始标记、最终标记的停顿。标记阶段会更新染色指针中的三色标志位。

  • 并发预备重分配:统计本次收集过程要清理哪些 Region,组成重分配集。ZGC 是面向整个堆进行回收的,但是只有重分配集的存活对象会进行复制操作。而 G1 是只面向回收集进行回收。

  • 并发重分配:将重分配集中的存活对象复制到新的 Region 上,并为重分配集中的每个 Region 维护一个旧对象到新对象的转发表。如果用户线程并发访问了位于重分配集中的对象(从染色指针中得知),内存屏障截获后转发到新对象,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”能力。

  • 并发重映射:修正指向旧对象的所有引用。实际上在下一次收集中的并发标记阶段完成,从而节省一次遍历开销。

Epsilon 收集器

不进行垃圾收集。在性能测试和压力测试有用武之地。一些短暂运行的应用也可以选择 Epsion 收集器。

Class 文件与加载

Class 类文件结构

Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符。

  • 魔数:头四个字节为 0xCAFEBABE,用于确定这个文件是一个 Class 文件。

  • 版本号:两个字节次版本号,两个字节主版本号。

  • 常量池:

    • 常量计数从 1 开始,0 代表不引用常量池项目,其他计数从 0 开始。
    • 存放两大类:字面量(文本字符串,final 值)和符号引用(全限定名、字段和方法名和描述符、方法句柄和类型)。
    • 每一项常量是一个表。
  • 访问标志:是类还是接口、是否为 public、是否为 abstract、是否为 final 等。

  • 类索引、父类索引和接口索引集合:指向类描述符常量,间接指向全限定名字符串。

  • 字段表集合:包含字段的修饰符、简单名称、描述符、属性表集合。

    • 全限定名:类全名的“.”替换为“/”,使用时常在后面加入“;”表示结束。

    • 简单名称:没有类型和参数修饰的方法和字段名称。

    • 描述符:

      • 基本数据类型以及void使用一个大写字符表示。

      • 对象类型为字符L加对象的全限定名。

      • 数组类型则在每一维度前加一字符“[”。

      • 对于方法,按照先参数(按顺序放在小括号内)后返回值的顺序描述。

      • 举例:

        1
        int indexOf(char[]source, int sourceOffset, int sourceCount, char[]target, int targetOffset, int targetCount, int fromIndex)

         描述符为“([CII[CIII)I”。

  • 方法表集合:包括方法的访问标志、名称索引、描述符索引和属性表集合。方法的代码在 code 属性里。

属性表

  • Code 属性:包括操作数栈深度最大值、局部变量表所需存储空间、字节码指令、异常处理表(start_pc、end_pc、handler_pc、catch_type)。
  • Exceptions 属性:受查异常(throws 列举的异常)
  • LineNumberTable 属性:源代码和字节码行号关系。
  • LocalVariableTable 和 LocalVariableTypeTable 属性 :栈帧中局部变量表的变量与 Java 源代码中变量的关系。
  • SourceFile 属性:记录这个 Class 文件对应的源码文件名称。
  • SourceDebugExtension 属性:记录为方便调试的自定义内容。如 JSP 文件调试的行号。
  • ConstantValue 属性:通知虚拟机自动为静态变量赋值。对于static final 修饰的基本类型和字符串常量,常使用 ConstantValue 属性赋值;否则,使用<clinic>() 初始化。
  • InnerClasses 属性:记录内部类和宿主类的关联。
  • Deprecated 属性:布尔属性。
  • Synthetic 属性:布尔属性。标记该字段或方法不是由源码产生而是编译器添加的(也可以通过设置访问标志实现标记),不包括<init>()<clinit>() 。
  • StackMapTable 属性:负责在字节码验证阶段方便类型验证。包含多个栈映射帧,代表执行到该字节码时局部变量表和操作数栈的验证类型。
  • Signature 属性:记录泛型签名信息。用于反射时能够获取泛型信息。
  • BootstrapMethods 属性:保存 invokedynamic 指令引用的引导方法限定符。
  • MethodParameters 属性:记录方法的各个形参名称和信息。这是为了解决抽象方法和接口方法无法使用 LocalVariableTable 属性的问题。
  • 模块化相关属性
  • 运行时注解相关属性

Class 文件的加载

类加载的时机

初始化的时机:

  1. 遇到四条字节码指令:new、getstatic、putstatic、invokestatic。
  2. 反射调用。
  3. 其子类初始化时。
  4. 虚拟机启动时,包含 main 方法的类。
  5. 动态语言支持相关。(待补充)
  6. 一个定义了 default 方法的接口的实现类发生了初始化。

加载、验证等阶段在初始化前完成。

接口也有初始化,也会生成<clinit()>类构造器,用于初始化成员变量。主要区别在于第三个场景:不要求父接口完成初始化,只有在用到时(如引用父接口定义的常量)才会初始化。

类加载的过程

  1. 加载:通过全限定名获取字节流,转化为方法区的运行时数据结构,生成 Class 对象。对于数组:
    • 若元素类型是引用,递归加载,数组将被标识在加载元素类型的类加载器的类名称空间上。可访问性与元素类型一致。
    • 若不是引用类型,则数组标记为与引导类加载器关联。可访问性为 public。
  2. 验证:确保字节流符合规范。
    • 文件格式验证:检查是否符合 Class 文件格式规范。
    • 元数据验证:语义分析,检查是否符合 Java 语言规范。
    • 字节码验证:数据流分析和控制流分析,确保运行时不会危害虚拟机安全。
    • 符号引用验证:确保能够将符号引用转化为直接饮用。
  3. 准备:为类中静态变量分配内存并赋零值。但如果字段存在 ConstantValue 属性(public static final),则会直接初始化为 ConstantValue 指定的值。
  4. 解析:将常量池中的符号引用替换为直接引用,同时也会检查可访问性。
  5. 初始化:对类变量赋初始值,执行静态语句块。

类加载器

每一个类加载器,都拥有一个独立的类名称空间。即,类和加载器一起共同建立类在虚拟机中的唯一性。

双亲委派模型

系统提供的类加载器:

  • 启动类加载器:负责加载$JAVA_HOME\lib-Xbootclasspath 指定的目录的类。
  • 扩展类加载器:负责加载$JAVA_HOME\lib\extjava.ext.dirs系统变量制定的目录的类。
  • 应用程序类加载器(系统类加载器):加载用户类路径的所有类库。

双亲委派模型:

image-20220305194259255

父子关系由“组合”关系实现。

实现代码在java.lang.ClassLoader#loadClass(),先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的 loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父类加载器加载失败, 抛出ClassNotFoundException异常的话,调用自己的findClass()方法尝试进行加载.

破坏双亲委派模型

  • 第一次破坏:双亲委派模型出现之前。解决方案:java.lang.ClassLoader增加findClass() 方法。
  • 第二次破坏:基础类型可能要调回用户代码。解决方案:采用线程上下文类加载器,如果没有设置过,该加载器为应用程序类加载器。
  • 第三次破坏:热替换、热部署。每一程序模块都有自己的类加载器,更换模块时,将类加载器和模块一起换掉。解决方案:改为网状结构,允许交给平级的类加载器查找。

执行引擎

以下为概念模型。

运行时栈帧结构

  • 局部变量表:
    • 存放方法参数和方法内部定义的局部变量。
    • 容量以变量槽为最小单位,每个变量槽大小不固定但一个变量槽必须能存放一个32位类型数据。
    • 对于64位数据,以高位对齐的方式分配。
    • 变量槽可以重用。
  • 操作数栈:操作数栈可以重用。
  • 动态连接:每个栈帧包含指向运行时常量池中该栈帧所属方法的引用,从而支持动态连接。
  • 方法返回地址:主调方法的 PC 计数器的值。
  • 附加信息:与调试、性能收集相关的信息。

方法调用

这一阶段确定被调用方法的版本。五种方法调用字节码:

  • invokestatic:调用静态方法。
  • invokespecial:调用<init()>、私有方法和父类方法。
  • invokevirtual:调用虚方法。
  • invokeinterface:调用接口方法,运行时确定实现对象。
  • invokedynamic:运行时动态解析出调用点限定符所引用的方法,再执行该方法。

解析

静态的过程。在类加载的解析阶段,将一部分编译期能够确定的符号引用转化为直接引用。

静态方法、私有方法、实例构造器、父类方法、被 final 修饰的方法(尽管被 invokevirtual 调用)五种称之为“非虚方法”,在类加载时解析。

分派

静态分派

依赖静态类型来决定方法执行版本的分派称为静态分派。方法重载是最典型的静态分派。静态分派发生在编译阶段。

动态分派

运行期根据实际类型确定方法的执行版本的分派过程称为动态分派。重写是典型的动态分派。invokevirtual 运行过程大致分为:

  • 找到操作数栈顶部元素指向对象的实际类型,记为 C。
  • 如果 C 中找到与常量中描述符和简单名称都相符的方法,进行访问权限校验,若通过则返回直接引用类型;不通过则抛 IllegalAccessError
  • 否则,按照继承关系从下往上依次进行第二部 。
  • 若始终没有找到合适的方法,抛 AbstractMethodError 。

单分派与多分派

方法的接收者与方法的参数统称为方法的宗量。单分派是根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。

目前 Java 是一门静态多分派(需要根据对象静态类型和参数共同决定)、动态单分派(参数已经确定,只需要关注实际类型)的语言。

虚拟机动态分派的实现

常见的手段是为类在方法区中建立一个虚方法表,使用虚方法表索引来替代元数据查找以提高性能。

虚方法表中存放着各个方法的实际入口地址。若方法在子类没有重写,则子类虚方法表中的地址入口与父类相同方法的地址入口是一致的。

为了实现方便,具有相同签名的方法,在父类、子类的虚方法表中应当具有相同的索引序号。

虚方法表一般在准备了类变量初始值后进行初始化。

字节码解释执行引擎

基于栈的指令集

优点:

  • 可移植
  • 代码更加紧凑
  • 编译器实现更加简单

缺点:

  • 执行速度比较慢

执行过程

局部变量表入栈出栈。

程序编译和代码优化

前端编译与优化

javac 编译器编译过程大致可分为:

  • 准备过程:初始化插入式注解处理器。
  • 解析与填充符号表过程:
    • 词法、语法分析。将源代码的字符流转变为标记(Token)集合,构造出抽象语法树。
    • 填充符号表。产生符号地址和符号信息。
  • 插入式注解处理器的注解处理过程。Lombok 就是依赖插入式注解处理器实现的。
  • 语义分析与字节码生成过程:
    • 标注检查。对语法的静态信息进行检查。
      • 标注检查步骤要检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配。
      • 在标注检查中,还会顺便进行一个称为常量折叠的代码优化。
    • 数据流及控制流分析。对程序动态运行过程进行检查。
      • 对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题。
      • 其目的与类加载的数据及控制流分析一致,但有一些校验项只有在编译器或运行期才能执行。
    • 解语法糖。包括泛型、变长参数、自动装箱拆箱等。
    • 字节码生成。

执行插入式注解有可能产生新的符号,此时需要转会到第二阶段。

image-20220305221537041

Java 语法糖

泛型

Java选择的泛型实现方式叫作“类型擦除式泛型”,只在程序源码中存在,在编译后的字节码文件中,全部泛型被替换为裸类型,并且在需要的地方插入了强制转型代码。

自动装箱、拆箱与遍历循环

自动装箱、拆箱在编译之后被转化成了对应的包装和还原方法,遍历循环把代码还原成了迭代器的实现,变长参数在调用的时候变成了一个数组类型的参数。

条件编译

根据布尔常量值的真假,编译器将会把 if 分支中不成立的代码块消除掉,这一工作将在编译器解除语法糖阶段完成。

后端编译与优化

即使编译器

HotSpot 内置了“客户端编译器”(C1)“服务端编译器”(C2)和目前处于试验阶段的 Graal 编译器。

分层编译:

  • 第 0 层:程序纯解释执行,并且解释器不开启性能监控功能。
  • 第 1 层:使用客户端编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化,不开启性能监控功能。
  • 第 2 层:仍然使用客户端编译器执行,仅开启方法及回边次数统计等有限的性能监控功能。
  • 第 3 层:仍然使用客户端编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如分支跳转、虚方法调用版本等全部的统计信息。
  • 第 4 层:使用服务端编译器将字节码编译为本地代码,相比起客户端编译器,服务端编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。

热点代码:

  • 被多次调用的方法
  • 被多次执行的方法体

两种情况,编译的目标对象都是整个方法体。

热点探测判定方式:

  • 基于采样的热点探测:周期性地检查各个线程的调用栈顶,如果发现某个方法经常出现在栈顶,那这个方法就是“热点方法”。
  • 基于计数器的热点探测:为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”。HotSpot 采用。
    • 方法调用计数器:一段时间之内方法被调用的次数,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那该方法的调用计数器就会被减少一半。
      • 当一个方法被调用时,虚拟机会先检查该方法是否存在被即时编译过的 版本,如果存在,则优先使用编译后的本地代码来执行。
      • 如果不存在已被编译过的版本,则将该方法的调用计数器值加一,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值。
      • 一旦已超过阈值的话,将会向即时编译器提交一个该方法的代码编译请求。
      • 执行引擎默认不会同步等待编译请求完成,而是继续执行字节码,直到提交的请求被编译完成。当编译工作完成后,这个方法的调用入口地址就会被系统自动改写成新值。
    • 回边计数器:统计一个方法中循环体代码执行的次数。没有计数热度衰减的过程。

并发

Java 内存模型

主内存与工作内存

Java 内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据。

volatile

两项特性:

  • 此变量对所有线程的可见性。当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。
  • 禁止指令重排序优化。通过内存屏障,在将本处理器的缓存写入了内存时,引起别的处理器无效化其缓存。

在以下场景中保证原子性:

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。

  • 变量不需要与其他的状态变量共同参与不变约束。

happens-before

Java 内存模型保障的先行发生关系:

  • 程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行 发生于书写在后面的操作。
  • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
  • volatile 变量规则:对一个volatile变量的写操作先行发生于后面对这个变量 的读操作。
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程 的代码检测到中断事件的发生。
  • 对象终结规则:一个对象的初始化完成先行发生于它的 finaliz e()方法的开始。
  • 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出 操作A先行发生于操作C的结论。

《深入理解Java虚拟机》阅读笔记

https://blog.wavesyj.top/2022/02/16/jvm-notes/

作者

Wave-SYJ

发布于

2022-02-16

更新于

2022-03-06

许可协议