2.30 读书<深入理解Java虚拟机>

7 minute read

第2章-Java内存区域与内存溢出异常

2.2 运行时数据区域

JVM_runtime_data_areas

2.2.1 程序计数器

  • 程序计数器(Program Counter Register)是一块较小的内存控件, 它可以看作是当前线程所执行的字节码的行号指示器。字节码的解释器工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令, 分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的, 在任何一个确定的时刻, 一个处理器都只会执行一条线程中的指令, 因此为了线程切换后能恢复到正确的执行位置, 每条线程都需要有一个独立的程序计数器。
  • 线程私有, 如果线程正在执行的是Java方法, 这个计数器记录的是正在执行的虚拟机字节码指令的地址; 如果执行的是Native方法, 这个计数器值则为空, 此内存区域是唯一没有规定任何OOM情况的区域

2.2.2 Java虚拟机栈

  • 虚拟机栈描述的是Java方法执行的内存模型, 每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息
  • 局部变量表存放了编译器可知的各种基本数据类型、对象引用和returnAddress类型
  • 如果线程请求的栈深度大于虚拟机所允许的深度, 将抛出StackOverflowError异常, 如果扩展时无法申请到足够的内存, 就会抛出OOM异常
  • 线程私有

2.2.3 本地方法栈

  • 本地放发展为虚拟机使用的Native方法服务
  • 也会抛出StackOverflowError和OOM异常
  • 线程私有

2.2.4 Java堆

  • 线程共享, 用于存储大多数对象实例
  • 从内存回收的角度来看, 由于现在收集器采用分代收集算法, Java堆中还细分为: 新生代, 老年代; 再细分为Eden空间, From Survivor空间, ToSurvivor空间。从内存分配的角度看, 线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)
  • 如果在堆中没有内存完成实例分配, 且堆无法再扩展时, 将抛出OOM

2.2.5 方法区

  • 线程共享, 用于存储虚拟机加载的类信息, 常量, 静态变量, 即时编译器编译后的代码等数据
  • 当方法区无法满足内存分配时, 抛出OOM

2.2.6 运行时常量

  • 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本, 字段, 方法, 接口等描述信息外, 还有一项信息是常量池(Constant Pool Table), 用于存放编译期生成的各种字面量和符号引用, 这部分内容将在类加载后进入方法区的运行时常量池中存放

2.3.1 对象的创建

  • 虚拟机遇到一条new指令时, 首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用, 并且检查这个符号引用代表的类是否已被加载, 解析和初始化过, 如果没有, 那必须现执行相应的类加载过程
  • 在类加载检查通过后, 接下来虚拟机将为新生对象分配内存
  • 对象创建在虚拟机的线程安全问题的解决方案
    1. 对分配内存空间的动作进行同步处理, 实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
    2. 把内存分配的动作按照线程划分为不同的空间之中进行, 即每个线程在Java堆中预先分配一小块内存, 称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)
  • 内存分配完成后, 虚拟机需要将分配到的内存空间都初始化到零值(不包括对象头), 如果使用TLAB, 这一工作过程也可以提前到TLAB分配进行; 接着虚拟机要对对象进行必要的设置, 例如对象是哪个类的实例。如何才能找到类的元数据信息, 对象的哈希吗, 对象的GC分代年龄等信息, 并存放在对象的对象头

2.3.2 对象的内存布局

  • 对象再内存存储的布局包括: 对象头、实例数据(Instance Data)和对齐填充(Padding)
  • 对象头
    • Mark Word(存储运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳)
    • 类型指针, 对象指向他的类元数据的指针, 虚拟机通过这个指针来确定对象是哪个类的实例

2.3.3 对象的访问定位

  • 引用访问和定位堆中的对象的具体为止, 主流的访问方式有使用句柄和直接指针
    • 使用句柄访问, 则Java堆会划出一块内存作为句柄池, reference中存储的就是对象的句柄地址, 而句柄中包含了对象实例数据与类型数据各自的具体地址信息, 它的最大好处是reference中存储的是稳定的句柄地址, 在对象被易懂时只会改变句柄中的实例数据指针, 而reference本身不需要修改
    • 使用直接访问, 则Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息, 而reference中存储的直接就是对象地址, 它的最大好处是速度更快, 节省指针定位的时间开销

2.4.2 虚拟机栈和本地方法栈溢出

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度, 将抛出StackOverflowError异常
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间, 则抛出OOM

2.4.3 方法区和运行时常量池溢出

  • String.intern()是Native方法, 它的作用是: 如果字符串常量池中已包含一个等于此String对象的字符串, 则返回代表池中这个字符串的String对象; 否则将此String对象包含的字符串添加到常量池中, 并返回此String对象的引用

第3章-垃圾收集器与内存分配策略

3.2.1 引用计数算法

  • 引用计数算法, 给对象增加引用计数器, 当有地方引用它, 计数器加1; 当引用失效, 计数器减1; 缺点是很难解决对象间互相循环引用的问题

3.2.2 可达性算法

  • 通过可达性分析(Reachability Analysis)来判定对象是否存活, 算法的基本思想是通过一系列的称为GC ROOTS的对象作为起始点, 从这些节点开始往下搜索, 搜索所走过的路径称为引用链(Reference Chain)
  • 可作为GC ROOTS的对象包括下面几种
    1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
    2. 方法区中类静态属性引用的对象
    3. 方法区中常量引用的对象
    4. 本地方法区栈中JNI引用的对象

3.2.3 再谈引用

  • 只要强引用还存在, 垃圾回收器永远不会回收掉被引用的对象
  • 软引用, 描述还有用但非必须的对象, 系统将要发出内存溢出异常之前, 将该对象进行回收范围之中进行第二次回收
  • 弱引用, 描述非必要对象, 当垃圾收集器工作(触发GC)时, 都会被回收
  • 虚引用, 目的能再对象被收集器回收时收到系统通知

3.2.4 生存还是死亡

  • 如果对象再进行可达性分析后发现没有与GC ROOTS相连接的引用链, 那它将会被第一次标记并且进行一次筛选, 筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法, 或者finalize()方法已经被虚拟机调用过, 虚拟机将这两种情况都视为没有必要执行
  • finalize(), 如果对象要在finalize()‘救活’自己, 只需要重新把引用链上的任何对下建立连接, 即给该对象加个强引用

3.2.5 回收方法区

  • 永久代的垃圾收集主要回收两部分内容 : 废弃常量和无用的类
  • 满足无用的类的3个条件
    1. 该类所有的实例都已经被回收, 也就是JAVA堆中不存在该类的任何实例
    2. 加载该类的ClassLoader已经被回收
    3. 该类对应的java.lang.Class对象没有在任何地方被引用, 无法在任何地方通过反射访问该类的方法

3.3.1 标记-清除算法

  • 首先标记出所有需要回收的对象, 在标记完成后统一回收所有被标记的对象
  • 不足: 一是效率问题, 标记和清除两个过程的效率都不高; 另外则是空间问题, 标记清除之后会产生大量不连续的内存碎片

3.3.2 复制算法

  • 为了解决效率问题, 将内存进行划分成多块, 当一片用完(Eden区)则将存活对象复制到另外一块(Survivor区), 再将用完区域(Eden区)清理; 优势: 简单, (对于年轻代)效率高
  • 当Survivor空间不够用时, 依赖其他内存(老年代)进行分配担保(Handle Promotion)

3.3.3 标记-整理算法

  • 类似标记-清除算法, 再清理后加上移动, 解决碎片化问题

3.3.4 分代收集算法

  • 把Java堆分为新生代和老年代, 根据各个年代的特点采用最适当的收集算法

3.4.1 枚举根节点

  • 可达性分析对执行时间的敏感还体现在GC停顿上, 因为这项分析工作必须在一个能确保一致性的快照中进行-这里”一致性”的意思是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上, 不可出现分析过程中对象引用关系还在不断变化的情况, 该点不满足的话分析结果准备性就无法得到保证
  • 在HotSpot的实现中, 是使用一组称为OopMap的数据结构知道哪些地方存放着对象引用

3.4.2 安全点

  • HotSpot的确没有为每条指令都生成OopMap, 只是在特定的为止记录了这些信息, 这些位置称为安全点(Safepoint), 即程序执行时并非在所有地方都能停顿下来开始GC, 只有在到达安全点时才能暂停
  • 抢占式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension), 其中抢先式中断不需要线程的执行代码主动去配合, 在GC发生时, 首先把所有线程全部中断, 如果发现有线程中断的地方不在安全点上, 就恢复线程, 让它跑到安全点上
  • GC需要中断线程的时候, 不直接对线程操作, 仅仅简单地设置一个标记, 各个线程执行时主动去轮询这个标记, 发现中断标记为真时就自己中断挂起

3.4.3 安全区域

  • 安全区域是指在一段代码片段之中, 引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的

3.5.1 Serial收集器

  • 单线程, 新生代使用复制算法, 老年代使用标记-整理算法, GC时需要暂停所有用户线程(Stop The World)

Serial收集器

3.5.2 ParNew收集器

  • Serial多线程版本, 新生代使用复制算法, 新生代GC时多线程收集, 老年代使用标记-整理算法, 老年代GC时单线程收集; GC时需要暂停所有用户线程(Stop The World)

ParNew收集器

3.5.3 Parallel Scavenge收集器

  • 新生代收集器, 关注点是达到一个可控制的吞吐量(Throughput), 吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值

ParNew收集器

3.5.4 SerialOld收集器

  • 老年代单线程收集器, 使用标记-整理算法

Serial收集器

3.5.6 CMS收集器

  • CMS收集器, 以获得最短回收停顿时间为目标的收集器, 使用标记-清除算法, 整个过程包含4个步奏
    • 初始标记(CMS initial mark)
    • 并发标记(CMS concurrent mark)
    • 重新标记(CMS remark)
    • 并发清除(CMS concurrent sweep)

Serial收集器

  • 初始标记, 重新标记这两个步骤仍需要Stop The World, 初始标记仅仅只是标记一下GC Roots能直接关联到的对象, 速度很快, 并发标记阶段就是进行GC Roots Tracking的过程, 而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录, 这个阶段的停顿时间一般会比初始标记阶段稍长一些, 但远比并发标记的时间短
  • 整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作
  • CMS的缺点
    1. CMS收集器对CPU资源非常敏感
    2. CMS收集器无法处理浮动垃圾(Floating Garbage)
    3. 由于CMS是一款基于”标记-清除”算法实现的, 收集结束会有大量空间碎片产生

3.5.7 G1收集器

  • G1收集器, 包含并行与并发(G1能充分利用多CPU, 多核环境下的硬件优势, 使用多个CPU来缩短Stop The World停顿的时间), 分代收集, 空间整合(整体使用标记-整理算法, 局部使用复制算法), 可预测的停顿, 过程包含4个步奏
    • 初始标记(Initial Marking)
    • 并发标记(Concurrent Marking)
    • 最终标记(Final Marking)
    • 筛选回收(Live Data Counting and Evacuation)
  • G1将Java堆划分为多个大小相等的独立区域(Region), 虽然保留有新生代核老年代的概念, 但新生代和老年代不再是物理隔离的
  • 初始化标记阶段仅仅只是标记一下GC Roots能直接关联到的对象, 并只修改TAMS(Next Top at Mark Start)的值, 让下一阶段用户程序并发运行时, 能在正确可用的Region中创建新对象, 这阶段需要停顿线程, 但耗时很短, 并发标记阶段是从GC Root开始对堆中对象进行可达性分析, 找出存活的对象, 这个阶段耗时较长, 但可与用户程序并发执行, 最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录, 最终标记阶段需要停顿线程, 但可并发执行, 最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序, 根据用户所期望的GC停顿时间来指定回收计划

Serial收集器

3.6 内存分配与回收策略

  • 内存分配即在堆分配, 且主要分配在新生代的Eden区, 如果启动了本地线程分配缓冲, 将按线程有限在TLAB上分配, 少数情况下也可能会直接分配在老年代
  • 对象优先在Eden分配, 当Eden区没有足够空间进行分配时, 虚拟机将发起一次Minor GC
  • 大对象直接进入老年区(大量连续内存空间的Java对象)
  • 长期存活的对象将进入老年代(被Minor GC 15次)

3.6.5 空间分配担保

  • 为了内存利用率, 只使用其中一个Survivor空间来作为轮换备份, 因此当出现大量对象在Minor GC后仍然存活的情况, 就需要老年代进行分配担保, 把Survivor无法容纳的对象直接进入老年代

第6章-类文件结构

6.3 Class类文件的结构

  • Class文件是一组以8位字节为基础单位的二进制流
  • Class文件格式采用一种类似于C语言结构体的伪结构来存储数据, 这种伪数据结构中只有两种数据类型: 无符号数和表

Class文件格式表

6.3.1 魔数与Class文件的版本

  • 每个Class文件的头4个字节称为魔数, 它的唯一作用是确定是否能被虚拟机接受的Class文件
  • 使用魔数进行标识主要是基于安全方面的考虑
  • Class文件的版本(minor_version、major_version)

6.3.2 常量池

  • 存放字面量和符号引用, 字面量包含如文本字符串、声明为final的常量值, 而符号引用则属于编译原理方面的, 包含类和接口的全限定名(Fully Qualified Name), 字段的名称和描述符(Descriptor), 方法的名称和描述符
  • 在Class文件中不会保存各个方法、字段的最终内存布局信息, 因此这些子串、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址, 也就无法直接被虚拟机使用。当虚拟机运行时, 需要从常量池获得对应的符号引用, 再在类创建时或运行时解析, 翻译到具体的内存地址之中

constant_pool

constant_pool_1

constant_pool_1

6.3.3 访问标识

  • 用于识别类或接口层次的访问信息(Class是类还是接口, 是否public, 是否abstract, 是否声明final)

6.3.4 类索引、父类索引与接口索引集合

  • 类索引、父类索引与接口索引集合(this_class、super_class、interfaces_Count、interfaces), Class文件用这三项数据确定类的继承关系
  • 类索引用于确定这个类的全限定名, 父类索引用于确定这个类的父类的全限定名
  • 接口索引集合就用来描述这个类实现了哪些接口, 这些被实现的接口将按implements语句

6.3.5 字段表集合

  • 字段表集合(fields、fields_count), 用于描述接口或类中声明的变量(字段作用域(public、private、protected), 实例/类变量(static), 可变性(final), 并发可见性(volatile), 可否序列化(transient), 字段数据类型, 字段名称), 字段表结构包括: 访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)
  • ACC_FINALACC_VOLATILE不能同时选择。接口之中的字段必须有ACC_PUBLICACC_STATICACC_FINAL标记
  • 字段表集合中不会列出从超类或者父接口中继承而来的字段, 但有可能列出原本Java代码之中不存在的字段, 譬如在内部类中为了保持对外部类的访问性, 会自动添加指向外部类实例的字段

6.3.6 方法表集合

  • 用于描述类或接口声明的方法, 类似字段表, 结构包括: 访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)

6.3.7 属性表集合

attributes_1

attributes_2

  • Code属性

code

  • Java程序方法体中的代码经过javac编译器处理后, 最终变成字节码指令存储在Code属性内; 接口或者抽象类中的方法就不存在Code属性
  • 虚拟机规范中明确限制了方法不允许超过65535条字节码指令, 即它实际只使用了u2的长度
  • 如果把一个Java程序中的信息分为代码(Code)和元数据(Metadata)两部分, Code属性即用于描述代码, 其他数据项目用于描述元数据
  • 实例构造器”"方法的Code属性。它的操作数栈的最大深度和本地变量表的容量都为0x0001, 字节码区域所占空间的长度为0x0005。虚拟机读取到字节码区域的长度后, 按照顺序依次读入紧随的5个字节, 并根据字节码指令表翻译出所对应的字节码指令。翻译"2A B7 00 0A B1"的过程为 1). 读入2A, 查表得0x2A对应的指令为aload_0, 这个指令的含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶 2). 读入B7, 查表得0xB7对应的指令为`invokespecial`, 这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接受者, 调用此对象的实例够着方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法, 它指向常量池中的一个`CONSTANT_Methodref_info`类型常量, 即此方法的方法符号引用。 3). 读入00 0A, 这是`invokespecial`的参数, 查常量池得0x000A对应的常量为实例构造器""方法的符号引用。 4). 读入B1, 查表得0xB1对应的指令为`return`, 含义是返回此方法, 并且返回值为`void`。这条指令执行后, 当前方法结束

  • this通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问, 在虚拟机调用实例方法时自动传入此参数而已

  • 异常处理流程
    • 如果try语句块中出现属于Exception或其子类的异常, 则转到catch语句处理
    • 如果try语句块中出现不属于Exception或其子类的异常, 则转到finally语句块处理
    • 如果catch语句块中出现任何异常, 则转到finally语句块处理
  • Exceptions属性
    • Exceptions属性的作用是列出方法中可能抛出的受检查异常(Checked Exceptions)

6.4.2 加载和存储指令

  • 加载和存储指令, 将数据在栈帧中的局部变量表和操作数栈之间传输
    • 将局部变量加载到操作数栈: iload、iload_、lload、lload_...
    • 将操作数栈存储到局部变量表: istore、istore_、lstore、lstore_...
    • 将常量加载到操作数栈: bipush、sipush、ldc、ldc_w…

6.4.3 运算指令

  • 算术指令: 对整型数据进行运算的指令与对浮点类型数据进行运算的指令
  • 加法指令: iadd、ladd、fadd、dadd
  • 减法指令: isub、lsub、fsub、dsub
  • 乘法指令: imul、lmul、fmul、dmul
  • 除法指令: idiv、ldiv、fdiv、ddiv
  • 求余指令: irem、lrem、frem、drem
  • 取反指令: ineg、lneg、fneg、dneg
  • 位移指令: ishl、ishr、iushr、lshl、lshr、lushr
  • 按位或指令: ior、lor
  • 按位与指令: iand、land
  • 按位异或指令: ixor、lxor
  • 局部变量自增指令: iinc
  • 比较指令: dcmpg、dcmpl、fcmpg、fcmpl、lcmp

6.4.4 类型转换指令

  • 类型转换指令可以将两种不同的数值类型进行相互转换
  • 处理窄化类型转换(Narrowing Numeric Conversions)时, 必须显式地使用转换指令来完成, 转换指令包括i2b, i2c, i2s, l2i, f2i, f2l, d2i, d2ld2f

6.4.5 对象创建与访问指令

  • 创建类实例的指令: new
  • 创建数组的指令: newarrayanewarraymultianewarray
  • 访问类字段(static字段, 或者称为类变量)和实例字段(非static字段, 或者称为实例变量)的指令: getfieldputfieldgetstaticputstatic
  • 把一个数组元素加载到操作数栈的指令: baloadcaloadsaloadialoadlaloadfaloaddaloadaaload
  • 将一个操作数栈的值存储到数组元素中的指令: bastorecastoresastoreiastorefastoredastoreaastore
  • 取数组长度的指令: arraylength
  • 检查类实例类型的指令: instanceofcheckcast

6.4.6 操作数栈管理指令

  • 将操作数栈的栈顶一个或两个元素出栈: poppop2
  • 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶: dupdup2dup_x1dup2_x1dup_x2dup2_x2
  • 将栈最顶端的两个数值互换: swap

6.4.7 控制转换指令

  • 条件分支: ifeqifleifltifneifgeifnullifnonnullif_icmpeqif_icmpneif_icmpltif_icmpgtif_icmpleif_icmpgeif_acmpeqif_acmpne
  • 复合条件分支: tableswitchlookupswitch
  • 无条件分支: gotogoto_wjsrjsr_wret

6.4.8 方法调用和返回指令

  • invokevirtual指令, 用于调用对象的实例方法, 根据对象的实际类型进行分派(虚方法分派)
  • invokeinterface指令, 用于调用接口方法, 它会在运行时搜索一个实现了这个接口方法的对象, 找出适合的方法进行调用
  • invokespecial指令, 调用需要特殊处理的实例方法, 如实例初始化方法, 私有方法和父类方法
  • invokestatic指令, 调用类方法
  • invokedynamic指令, 用于运行时动态解析出调用点限定符所引用的方法, 并执行该方法, 前面4条指令的分派逻辑固化在Java虚拟机内部, 该方法由用户所设定的引导方法决定

6.4.9 异常处理指令

  • Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现

6.4.10 同步指令

  • Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步, 这两种同步结构都是使用管程(Monitor)来支持
  • 方法级的同步是隐式的, 即无须通过字节码指令来控制
  • 同步方法, 虚拟机通过常量池的方法表结构中的ACC_SYNCHRONIZED访问标识确认方法是否为同步, 如果设置, 执行线程持有管程(Monitor), 执行完成则释放管程, 当有执行线程持有管程, 其它线程无法再获得同一管程
  • synchronized, Java虚拟机的指令通过monitorenter和monitorexit支持synchronized语义, 正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持

第7章-虚拟机类加载机制

7.2 类加载的时机

code

  • 类从加载到虚拟机内存开始, 到卸载出内存为止. 包括: 加载(Loading) -> 验证(Verification) -> 准备(Preparetion) -> 解析(Resolution) -> 初始化(Initialization) -> 使用(Using) -> 卸载(Unloading); 其中验证、准备、解析统称为链接
  • 初始化阶段, 虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行初始化
    1. 遇到newgetstaticputstaticinvokestatic这4条字节码指令时, 如果类没有进行过初始化, 则需要先触发其初始化
    2. 使用java.lang.reflect包的方法对类进行反射调用的时候, 如果类没有进行过初始化, 则需要先触发其初始化
    3. 当初始化一个类的时候, 如果发现其父类还没有进行初始化, 则需要先触发其父类的初始化
    4. 当虚拟机启动时, 用户需要指定一个要执行的主类, 虚拟机会先初始化这个类
    5. 当使用JDK 1.7的动态语言支持时, 如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStaticREF_putStaticREF_invokeStatic的方法句柄, 并且这个方法句柄所对应的类没有进行过初始化, 则需要先触发其初始化
  • 对于静态字段, 只有直接定义这个字段的类才会初始化, 因此通过其子类来引用父类中定义的静态字段, 只有触发父类的初始化而不会触发子类的初始化

7.3.1 加载

  • 加载, 加载是类加载过程的一个阶段, 包含:
    • 通过类的全限定名来获得定义此类的二进制字节流
    • 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
    • 在内存中生成代表这个类的java.lang.Class对象

7.3.2 验证

  • 验证, 确保Class文件的字节流包含的信息符合虚拟机的要求
  • 文件格式验证
    • 第一阶段要验证字节流是否符合Class文件格式的规范
      • 是否以魔数0xCAFEBABE开头
      • 主, 次版本号是否在当前虚拟机处理范围之内
      • 常量池的常量是否有不背支持的常量类型 …
    • 该验证阶段的主要目的时保证输入的字节流能正确地解析并存储于方法区内, 格式上符合描述一个Java类型信息的要求
  • 元数据验证
    • 第二阶段是对字节码描述的信息进行语义分析
      • 这个类是否有父类
      • 这个类的父类是否继承了不允许被继承的类
      • 如果这个类不是抽象类, 是否实现了其父类或接口之中要求实现的所有方法 …
    • 第二阶段的主要目的是对类的元数据信息进行语义校验, 保证不存在不符合Java语言规范的元数据信息
  • 字节码验证
    • 通过数据流和控制流分析, 确定程序语义是合法的, 符合逻辑的
    • 这个阶段将对类的方法体进行校验分析, 保证被校验类的方法在运行时不会做出危害虚拟机安全的事件
  • 符号引用验证
    • 符号引用验证可以看做是对类自身以外的信息进行匹配性校验
      • 符号引用中通过字符串描述的全限定名是否能找到对应的类
      • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
      • 符号引用中的类、字段、方法的访问性(privateprotectedpublicdefault)是否可被当前类访问

7.3.3 准备

  • 准备, 为类变量分配内存并设置类变量初始值的阶段

7.3.4 解析

  • 解析, 虚拟机将常量池内的符号引用替换为直接引用
    • 符号引用(Symbolic Reference): 符号引用以一组符号来描述所引用的目标, 符号可以是任何形式的字面量, 只要使用时能无歧义地定义到目标即可。符号引用与虚拟机实现的内容布局无关
    • 直接引用(Direct Reference): 直接引用可以是直接指向目标的指针, 相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的
    • 类或接口的解析
      • 当前代码所处的类为D, 如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接用用
        1. 如果C不是一个数组类型, 那虚拟机将会把代表N的全限定名传递给D的类加载器取加载这个类C
        2. 如果C是一个数组类型, 并且数组的元素类型为对象, 也就是N的描述符会是类似[Ljava/lang/Integer]的形式, 那将会按照第1点的规则加载数组元素类型
        3. 如果上面的步骤没有出现任何异常, 那么C在虚拟机中实际上已经称为一个有效的类或接口了, 但在解析完成之前还要进行符号引用验证, 确认D是否具备对C的访问权限
    • 字段解析
      • 如果CONSTANT_Class_info符号引用解析成功, 那将这个字段所属的类或接口用C表示, 虚拟机规范要求按照如下步骤对C进行后续字段的搜索
        1. 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段, 则返回这个字段的直接引用, 查找结束
        2. 否则, 如果在C中实现了接口, 将会按照继承关系从下往上递归搜索各个接口和它的父接口, 如果接口包含了简单名称和字段描述符都与目标匹配的字段, 则返回这个字段的直接引用, 查找结束
        3. 否则, 如果C不是java.lang.Object的话, 将会按照继承关系从下往上递归搜索其父类, 如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段, 则返回这个字段的直接引用, 查找结束
        4. 否则查找失败, 抛出java.lang.NoSuchFieldError异常
    • 类方法解析
      • 类方法解析的第一个步骤与字段解析一样, 也需要先解析出类方法表的class_ index项中索引的方法所属的类或接口的符号引用, 如果解析成功, 依然用C表示这个类, 虚拟机进行后续的类方法搜索
        1. 类方法和接口方法符号引用的类常量类型定义是分开的, 如果在类方法表中发现class_index中索引的C是个接口, 那就直接抛出IncompatibleClassChangeError异常
        2. 如果通过了第1步, 在类C中查找是否有简单名称和描述符都与目标相匹配的方法, 如果有则返回这个方法的直接引用, 查找结束
        3. 否则, 在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法, 如果有则返回这个方法的直接引用, 查找结束
        4. 否则, 在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法, 如果存在匹配的方法, 说明类C是一个抽象类, 这时查找结束, 抛出AbstractMethodError异常
        5. 否则, 宣布方法查找失败, 抛出NoSuchMethodError
    • 接口方法解析
      • 接口方法也需要先解析出接口方法表的class_index项中索引的方法所属的类或接口的符号引用, 如果解析成功, 依然用C表示这个类, 虚拟机进行后续的接口方法搜索
        1. 与类方法解析不同, 如果在接口方法表中发现class_index中的索引C是个类而不是接口, 则抛出IncompatibleClassChangeError异常
        2. 否则, 在接口C中查找是否有简单名称和描述符都与目标相匹配的方法, 如果有则访问这个方法的直接引用, 查找结束
        3. 否则, 在接口C的父接口中递归查找, 直到Object类为止, 看是否有简单名称和描述符都与目标相匹配的方法, 如果有则返回这个方法的直接引用, 查找失败
        4. 否则, 宣布方法查找失败, 抛出NoSuchFieldError

7.3.5 初始化

  • 初始化, 类初始化阶段是类加载过程的最后一步, 到了初始化阶段, 才真正执行Java程序代码
  • 初始化阶段是执行类构造器<clinit>()方法的过程
  • <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生
  • <clinit>方法与类的构造函数不同, 它不需要显式地调用父类构造器
  • 由于父类的<clinit>方法先执行, 意味着父类中定义的静态语句块要优先于子类

7.4 类加载器

  • 类加载器, 类加载阶段中的”通过类的全限定名来获得定义此类的二进制字节流”由Java虚拟机外部实现, 即ClassLoader
  • 双亲委派模型
    • 启动类加载器(Bootstrap ClassLoader), C++实现, 加载\lib的类
    • 扩展类加载器(Extension ClassLoader), 加载\lib\ext的类
    • 应用程序类加载器(Application CLassLoader), 负责加载用户类路径(ClassPath)上执行的类库
    • 双亲委派模型(Parents Delegation Model)要求除了顶层的启动类加载器外, 其余的类加载器都应有自己的父类加载器

    • 双亲委派模型的工作过程: 类加载器收到类加载的请求, 先把请求委派给父类加载器去完成, 每层往上, 最终传送到父类加载器完成, 当父类加载器反馈无法完成加载请求, 则交给子类加载器处理加载

7.4.3 破坏双亲委派模型

  • 破坏双亲委派模型
    • 线程上下文类加载器(Thread Context CLassLoader)

2.30.3 类文件结构

  • 魔数(magic)与Class文件的版本(minor_version、major_version), 确定是否能被虚拟机接受的Class文件
  • 常量池(constant_pool_count、constant_pool), 存放字面量和符号引用(类和接口的全限定名, 字段的名称和描述符, 方法的名称和描述符)
  • 访问标志(access_flags), 用于识别类或接口层次的访问信息(Class是类还是接口, 是否public, 是否abstract)
  • 类索引、父类索引与接口索引集合(this_class、super_class、interfaces_Count、interfaces), Class文件用这三项数据确定类的继承关系
  • 字段表集合(fields、fields_count), 用于描述接口或类中声明的变量(字段作用域(public、private、protected), 实例/类变量(static), 可变性(final), 并发可见性(volatile), 可否序列化(transient), 字段数据类型, 字段名称), 字段表结构包括: 访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)
  • 方法表集合(methods、methods_count), 用于描述类或接口声明的方法, 类似字段表, 结构包括: 访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)
  • 属性表集合
  • 加载和存储指令, 将数据在栈帧中的局部变量表和操作数栈之间传输
    • 将局部变量加载到操作数栈: iload、iload_、lload、lload_...
    • 将操作数栈存储到局部变量表: istore、istore_、lstore、lstore_...
    • 将常量加载到操作数栈: bipush、sipush、ldc、ldc_w…
  • 运算指令
    • 加法指令: iadd、ladd、fadd、dadd
    • 减法指令: isub、lsub、fsub、dsub
    • 乘法指令: imul、lmul、fmul、dmul
    • 除法指令: idiv、ldiv、fdiv、ddiv
    • 求余指令: irem、lrem、frem、drem
    • 取反指令: ineg、lneg、fneg、dneg
    • 位移指令: ishl、ishr、iushr、lshl、lshr、lushr
    • 按位或指令: ior、lor
    • 按位与指令: iand、land
    • 按位异或指令: ixor、lxor
    • 局部变量自增指令: iinc
    • 比较指令: dcmpg、dcmpl、fcmpg、fcmpl、lcmp
  • 方法调用和返回指令
    • invokevirtual指令, 用于调用对象的实例方法, 根据对象的实际类型进行分派(虚方法分派)
    • invokeinterface指令, 用于调用接口方法
    • invokespecial指令, 调用需要特殊处理的实例方法, 如实例初始化方法, 私有方法和父类方法
    • invokestatic指令, 调用类方法
    • invokedynamic指令, 用于运行时动态解析出调用点限定符所引用的方法, 并执行该方法, 前面4条指令的分派逻辑固化在Java虚拟机内部, 该方法由用户所设定的引导方法决定
  • 同步指令(方法的同步和方法内部一段指令序列的同步, 使用管程(Monitor)来支持)
    • 同步方法, 虚拟机通过常量池的方法表结构中的ACC_SYNCHRONIZED访问标识确认方法是否为同步, 如果设置, 执行线程持有管程(Monitor), 执行完成则释放管程, 当有执行线程持有管程, 其它线程无法再获得同一管程
    • synchronized, Java虚拟机的指令通过monitorenter和monitorexit支持synchronized语义

2.30.1 Java内存区域与内存溢出异常

  • 运行时数据区域
    • 程序计数器(PC), 线程私有, 用于记录正在执行的虚拟机字节码指令地址
    • Java虚拟机栈, 线程私有, 描述Java方法执行的内存模型, 每个方法执行时会创建栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等
    • 本地方法栈, 类似Java虚拟机栈, 为Native方法服务
    • Java堆, 线程共享, 用于存储绝大多数对象实例
    • 方法区, 线程共享, 存储类信息、常量、静态变量、即时编译后的代码和数据
      • 运行时常量池, 方法区的一部分, 存储版本、字段、方法、接口以及常量池(编译期生成的各种字面量和符号引用)
  • 对象的内存布局
    • 对象再内存存储的布局包括: 对象头、实例数据(Instance Data)和对齐填充(Padding)
    • 对象头
      • Mark Word(存储运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳)
      • 类型指针, 对象指向他的类元数据的指针

2.30.2 垃圾收集器与内存分配策略

  • 引用计数算法, 给对象增加引用计数器, 当有地方引用它, 计数器加1; 当引用失效, 计数器减1; 缺点是很难解决对象间互相循环引用的问题
  • 可达性算法, 通过可达性分析(Reachability Analysis)来判定对象是否存活
  • 再谈引用
    • 软引用, 描述还有用但非必须的对象, 系统将要发出内存溢出异常之前, 将该对象进行回收范围之中进行第二次回收
    • 弱引用, 描述非必要对象, 当垃圾收集器工作(触发GC)时, 都会被回收
    • 虚引用, 目的能再对象被收集器回收时收到系统通知
  • finalize(), 如果对象要在finalize()‘救活’自己, 只需要重新把引用链上的任何对下建立连接, 即给该对象加个强引用
  • 垃圾收集算法
    • 标记-清除算法, 算法分为标记和清除阶段, 标记需要回收的对象, 标记完成统计回收被标记对象; 缺点: 1). 效率问题, 标记和清除过程效率不高; 2). 空间问题, 标记清除后产生大量不连续的内存碎片
    • 复制算法, 将内存进行划分成多块, 当一片用完(Eden区)则将存活对象复制到另外一块(Survivor区), 再将用完区域(Eden区)清理; 优势: 简单, (对于年轻代)效率高
    • 标记-整理算法, 类似标记-清除算法, 再清理后加上移动, 解决碎片化问题
    • 分代收集算法, 即年轻代、老年代
  • 垃圾收集器
    • Serial收集器, 单线程, 新生代使用复制算法, 老年代使用标记-整理算法, GC时需要暂停所有用户线程(Stop The World)
    • ParNew 收集器, Serial多线程版本, 新生代使用复制算法, 新生代GC时多线程收集, 老年代使用标记-整理算法, 老年代GC时单线程收集; GC时需要暂停所有用户线程(Stop The World)
    • Parallel Scavenge收集器, 新生代收集器, 关注点是达到一个可控制的吞吐量(Throughput)
    • Serial Old收集器, 老年代单线程收集器, 使用标记-整理算法
    • Parallel Old收集器, 老年代多线程收集器, 使用标记-整理算法
    • CMS收集器, 以获得最短回收停顿时间为目标的收集器, 使用标记-清除算法, 整个过程包含4个步奏
      • 初始标记(CMS initial mark)
      • 并发标记(CMS concurrent mark)
      • 重新标记(CMS remark)
      • 并发清除(CMS concurrent sweep)
    • G1收集器, 包含并行与并发, 分代收集, 空间整合(整体使用标记-整理算法, 局部使用复制算法), 可预测的停顿, 过程包含4个步奏
      • 初始标记(Initial Marking)
      • 并发标记(Concurrent Marking)
      • 最终标记(Final Marking)
      • 筛选回收(Live Data Counting and Evacuation)
    • 内存分配与回收策略
      • 内存分配即再堆分配, 且主要分配在新生代的Eden区
      • 对象优先在Eden分配
      • 大对象直接进入老年区(大量连续内存空间的Java对象)
      • 长期存活的对象将进入老年代(被Minor GC 15次)

2.30.3 类文件结构

  • 魔数(magic)与Class文件的版本(minor_version、major_version), 确定是否能被虚拟机接受的Class文件
  • 常量池(constant_pool_count、constant_pool), 存放字面量和符号引用(类和接口的全限定名, 字段的名称和描述符, 方法的名称和描述符)
  • 访问标志(access_flags), 用于识别类或接口层次的访问信息(Class是类还是接口, 是否public, 是否abstract)
  • 类索引、父类索引与接口索引集合(this_class、super_class、interfaces_Count、interfaces), Class文件用这三项数据确定类的继承关系
  • 字段表集合(fields、fields_count), 用于描述接口或类中声明的变量(字段作用域(public、private、protected), 实例/类变量(static), 可变性(final), 并发可见性(volatile), 可否序列化(transient), 字段数据类型, 字段名称), 字段表结构包括: 访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)
  • 方法表集合(methods、methods_count), 用于描述类或接口声明的方法, 类似字段表, 结构包括: 访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)
  • 属性表集合
  • 加载和存储指令, 将数据在栈帧中的局部变量表和操作数栈之间传输
    • 将局部变量加载到操作数栈: iload、iload_、lload、lload_...
    • 将操作数栈存储到局部变量表: istore、istore_、lstore、lstore_...
    • 将常量加载到操作数栈: bipush、sipush、ldc、ldc_w…
  • 运算指令
    • 加法指令: iadd、ladd、fadd、dadd
    • 减法指令: isub、lsub、fsub、dsub
    • 乘法指令: imul、lmul、fmul、dmul
    • 除法指令: idiv、ldiv、fdiv、ddiv
    • 求余指令: irem、lrem、frem、drem
    • 取反指令: ineg、lneg、fneg、dneg
    • 位移指令: ishl、ishr、iushr、lshl、lshr、lushr
    • 按位或指令: ior、lor
    • 按位与指令: iand、land
    • 按位异或指令: ixor、lxor
    • 局部变量自增指令: iinc
    • 比较指令: dcmpg、dcmpl、fcmpg、fcmpl、lcmp
  • 方法调用和返回指令
    • invokevirtual指令, 用于调用对象的实例方法, 根据对象的实际类型进行分派(虚方法分派)
    • invokeinterface指令, 用于调用接口方法
    • invokespecial指令, 调用需要特殊处理的实例方法, 如实例初始化方法, 私有方法和父类方法
    • invokestatic指令, 调用类方法
    • invokedynamic指令, 用于运行时动态解析出调用点限定符所引用的方法, 并执行该方法, 前面4条指令的分派逻辑固化在Java虚拟机内部, 该方法由用户所设定的引导方法决定
  • 同步指令(方法的同步和方法内部一段指令序列的同步, 使用管程(Monitor)来支持)
    • 同步方法, 虚拟机通过常量池的方法表结构中的ACC_SYNCHRONIZED访问标识确认方法是否为同步, 如果设置, 执行线程持有管程(Monitor), 执行完成则释放管程, 当有执行线程持有管程, 其它线程无法再获得同一管程
    • synchronized, Java虚拟机的指令通过monitorenter和monitorexit支持synchronized语义

2.30.4 虚拟机类加载机制

  • 类加载的时机
    • 类从加载到虚拟机内存开始, 到卸载出内存为止. 包括: 加载(Loading) -> 验证(Verification) -> 准备(Preparetion) -> 解析(Resolution) -> 初始化(Initialization) -> 使用(Using) -> 卸载(Unloading); 其中验证、准备、解析统称为链接
  • 类加载的过程
    • 加载, 加载是类加载过程的一个阶段, 包含:
      • 通过类的全限定名来获得定义此类的二进制字节流
      • 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
      • 在内存中生成代表这个类的java.lang.Class对象
    • 验证, 确保Class文件的字节流包含的信息符合虚拟机的要求
      • 文件格式验证
      • 元数据验证
      • 字节码验证(通过数据流和控制流分析, 确定程序语义是合法的, 符合逻辑的)
      • 符号引用验证
    • 准备, 为类变量分配内存并设置类变量初始值的阶段
    • 解析, 虚拟机将常量池内的符号引用替换为直接引用
      • 类或接口的解析
      • 字段解析
      • 类方法解析
      • 接口方法解析
    • 初始化, 真正执行Java程序代码, 如执行类构造器()
  • 类加载器, 类加载阶段中的”通过类的全限定名来获得定义此类的二进制字节流”由Java虚拟机外部实现, 即ClassLoader
  • 双亲委派模型
    • 启动类加载器(Bootstrap ClassLoader), C++实现, 加载\lib的类
    • 扩展类加载器(Extension ClassLoader), 加载\lib\ext的类
    • 应用程序类加载器(Application CLassLoader), 负责加载用户类路径(ClassPath)上执行的类库
    • 双亲委派模型(Parents Delegation Model)要求除了顶层的启动类加载器外, 其余的类加载器都应有自己的父类加载器
    • 双亲委派模型的工作过程: 类加载器收到类加载的请求, 先把请求委派给父类加载器去完成, 每层往上, 最终传送到父类加载器完成, 当父类加载器反馈无法完成加载请求, 则交给子类加载器处理加载
  • 破坏双亲委派模型
    • 线程上下文类加载器(Thread Context CLassLoader)

2.30.5 虚拟机字节码执行引擎

  • 运行时栈帧结构
    • 栈帧(Stack Frame)用于支持虚拟机进行方法调用和方法执行的数据结构, 存储方法的局部变量表、操作数栈、动态链接和方法返回地址
      • 局部变量表, 存放方法参数和方法内部定义的局部变量
      • 操作数栈, 方法执行过程, 通过字节码指令往操作数栈写入和提取内容, 即出栈/入栈
      • 动态链接, 栈帧包含的运行时常量池中栈帧所属方法的引用, 支持方法调用过程中的动态链接; 类加载阶段或者第一次使用转化为直接引用即为静态解析; 运行期间转化为直接引用则为动态链接
      • 方法返回地址, 方法退出包括方法返回的字节码指令/方法过程遇到异常
    • 方法调用, 方法调用在Class文件存储的都是符号引用
      • 解析, 在类加载解析阶段, 将部分符号引用转化为直接引用
      • 分派
        • 静态分派, 再编译阶段, 典型应用是方法重载
          public class StaticDispatch {
            static abstract class Human {}
            static class Man extends Human {}
            static class Woman extends Human {}
            public void sayHello(Human guy) {
              System.out.println("hello, guy!");
            }
            public void sayHello(Man guy) {
              System.out.println("hello, gentleman!");
            }
            public void sayHello(Woman guy) {
              System.out.println("hello, lady!");
            }
            public static void main(String[] args) {
              Human man = new Man();
              Human woman = new Woman();
              StaticDispatch sd = new StaticDispatch();
              // 运行结果:
              // hello, guy!
              // hello, guy!
              sd.sayHello(man);
              sd.sayHello(woman);
            }
          }
        
        • 代码中Human成为变量的静态类型(Static Type)或外观类型(Apparent Type), 而Man为变量的实际类型(Actual Type), 静态类型和实际类型在程序中都可以发生一些变化, 区别是静态类型的变化在使用时发生, 变量本身的静态类型不会被改变, 且最终静态的类型在编译期可知; 实际类型变化的结果在运行期才可确定, 编译器在编译程序的时候并不知道对象的实际类型.
        • 动态分派, 典型应用方法重写; invokevirtual指令即把常量池的类方法符号引用解析到不同的直接引用上, 该过程即Java方法重写的本质; invokevirtual指令的多态查找过程: 1). 找到操作数栈顶元素锁指向的对象的实际类型, 记为C. 2). 如果在类型C中找到与常量中的描述符和简单名称都相符的方法, 则进行权限校验, 如果通过则返回这个方法的直接引用; 不通过则抛IllegalAccessException异常 3). 否则, 按照继承关系从下到上依次对C的各个父类进行第2步的搜索和验证过程 4). 如果找不到合适的方法, 则抛AbstractMethodError异常
        • 虚拟机动态分派的实现, 在方法区执行时建立虚方法表(Virtual Method Table)
  • 基于栈的字节码解释执行引擎
    • 基于栈的指令集主要的优点是可移植(Java), 缺点是执行相对慢一点

Java内存模型与线程

  • volatile
    • 可见性指当一条线程修改了这个变量的值, 新值对于其它线程可以立即得知; volatile只保证可见性, 在不符合以下两条规则的运算场景中, 我们仍然要加锁, 1). 运算结果并不依赖变量的当前值, 或者能够确保只有单一的线程改变量的值; 2). 变量不需要与其他的状态变量共同参与不变约束
    • 禁止重排序
  • long和double类型变量特殊规则
    • 对于64位的数据类型(long和double), 在JMM模型中特别定义一条相对宽松的规定: 允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作进行, 但虚拟机一般不会有读取一半情况出现, 因此long和double作为变量的时候不需要专门声明为volatile
  • 原子性, 可见性, 有序性
    • 原子性, 由JMM保证变量操作包括read, load, assign, use, store, write等读写操作是具备原子性
    • 可见性, 线程修改了共享变量的值, 其它线程可以立刻得知这个修改; 除了volatile, synchronized和final也保证了可见性;synchronized是由一个变量执行unlock操作之前, 必须先把变量同步回主内存中; final修饰的字段在构造器中一旦初始化完成, 并且构造器没有把”this”传递出去, 那在其它线程中就能看到final字段的值
    • 有序性, 如果在本线程内观察, 所有操作都是有序的; 如果在一个线程观察另外一个线程, 则所有操作都是无序的
  • happens-before(JMM定义的两项操作之间的偏序关系)
    • 程序次序规则(Program Order Rule): 在一个线程内, 按照程序代码顺序, 书写在前面的操作先行发生于(happens-before)书写在后面的操作。更确切地说是控制流顺序而不是程序代码顺序
    • 管程锁定规则(Monitor Lock Rule): 一个unlock操作先行于(happens-before)后面对同一个锁的lock操作。
    • volatile变量规则(Volatile Variable Rule): 对一个volatile变量的写操作先行发生于(happens-before)后面对这个变量的读操作
    • 线程启动规则(Thread Start Rule): Thread对象的start()方法先行发生于此线程的每一个动作
    • 线程终止规则(Thread Termination Rule): 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生, 可通过Thread.interrupted()方法检查是否有中断
    • 对象终结规则(Finalizer Rule): 一个对象的初始化完成(构造函数执行结束)先行发生于(happens-before)它的finalize()方法的开始
    • 传递性(Transitivity): 如果操作A先行发生于(happens-before)操作B, 操作B先行发生于(happens-before)操作C, 那么操作A先行发生于(happens-before)操作C

参考