【JVM】深入理解Java虚拟机

第一章 概述

1.1 JVM相关知识体系

在这里插入图片描述

1.2 JVM图例

在这里插入图片描述
图片出处

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

2.1 概述

2.2 运行时数据区域

2.2.1 程序计数器

  线程私有,通过改变计数器的值选取下一条需要执行的字节码指令。执行本地方法(native)时计数器的值为空

2.2.2 Java虚拟机栈

  线程私有,每个方法需要被执行时创建一个栈帧并压入栈顶,执行完毕出栈,调用其他方法时将其他方法的栈帧压入。
  栈帧包括局部变量表、操作数栈、动态链接、方法返回地址、附加信息

(1) 局部变量表

  局部变量表可以存放基本数据类型(boolean、byte、char、short、int、float、long、double),对象引用(reference类型),returnAddress类型(执行一条字节码指令的地址)
  这些数据类型在局部变量表中的存储空间以局部变量槽Slot为单位,一个Slot具体大小由虚拟机决定。

(2) 操作数栈

  跟教科书里栈模拟计算器类似,两数相加则将两数压栈,遇到操作符则取出两数进行相加,将和再压栈。
  不过操作数栈结合了局部变量表对变量进行了存储。

public void function(){
	int a = 1;
	int b = 2;
	int c = a + b;
}

流程如下:

  • 将1压入操作数栈
  • 将1从操作栈取出然后放到局部变量表 (a=1)
  • 将2压入操作数栈
  • 将2从操作栈取出然后放到局部变量表 (b=2)
  • 将1从局部变量表取出放到操作数栈
  • 将2从局部变量表取出放到操作数栈
  • 两个数据出栈求和,将结果3放到操作数栈 (c = a + b)
  • 返回

具体处理流程看这里

(3) 动态链接

  每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有该引用是为了支持方法调用过程中的动态链接(Dynamic Linking)。
  Class文件的常量池中存在大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。
  这些符号引用一部分在类加载阶段或第一次使用时转化为直接引用,这种转化是静态解析。
  另一部分在运行期间转化为直接引用,这部分成为动态链接。

(4) 方法返回地址

  有正常完成出口和异常完成出口,如果有返回值则会压入调用方的栈帧。

(5) 附加信息

取决于虚拟机的实现,如虚拟机规范中没有的信息放到其中。

2.2.3 本地方法栈

  线程私有,与虚拟机栈类似,区别是本地方法栈作用域本地方法,会受到平台的影响。

2.2.4 方法区(永久代)、元空间、堆、直接内存

(1) Java版本与四者的变化
  • JDK6->JDK7,字符串常量池、静态变量从方法区转移到堆中,符号引用转移到本地内存
  • JDK7->JDK8,方法区转移到元空间,(主要是类型信息class metadata)
(2) 方法区

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

  • 在HotSpot虚拟机上,JDK8之前,用永久代实现方法区,能够参与GC,但导致容易发生内存溢出。
  • 永久代的垃圾收集与老年代捆绑,无论谁满了,都会触发两者的垃圾收集
  • JDK8之前,方法区和堆逻辑上隔离,物理内存上连续。JDK8时将方法区移入元空间,位于本地内存,此时方法区和堆在物理和逻辑上都是隔离的。
(3) 元空间

  方法区存在于元空间,元空间不再与堆连续,存在于本地内存,没设置限制参数时只受物理内存限制。元空间不会GC。
  存储类和类加载器的元数据信息。

(4) 堆

  线程共享
存放:

  • 对象实例
  • 数组
  • 字符串常量池(JDK8)
  • 类静态变量(JDK8)
(5) 直接内存

  元空间在直接内存,直接内存与NIO有关。

(6) 运行时常量池

  是方法区的一部分。Class文件中有常量池表,用于存放编译器生成的各种字面量和符号引用,这部分将在类加载后存放到方法区的运行时常量池中。
  Class文件的符号引用和符号引用翻译出来的直接引用也会存储在运行时常量池中。
  运行时常量池是动态的,运行期间也可以放入新的常量。

2.3 HotSpot虚拟机对象

2.3.1 虚拟机中对象的创建

【JVM】对象创建流程

2.3.2 对象的内存布局

  对象在堆中存储布局可以划分为三部分:对象头、实例数据、对其填充。

(1) 对象头

  对象头包含两部分信息。
  一类是存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID,偏向时间戳,被称为Mark Word,这部分数据长度在32位和64位虚拟机中为32比特和64比特。对象头里的信息与对象自身定义数据无关的额外存储成本,是动态的(为了节省空间)。
  举例:在32位的HotSpot虚拟机中,对象未被同步锁锁定的状态下,Mark Word32个比特存储空间的25个比特用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特存储锁标志位,1个比特固定为0.其他状态下(轻量级锁定、重量级锁定、GC标记、可偏向)下的存储内容如下:

存储内容标志位状态
对象哈希码、对象分代年龄01未锁定
指向锁记录的指针00轻量级锁定
指向重量级锁的指针10膨胀(重量级锁定)
空,不需要记录11GC标记
偏向线程ID、翩跹时间戳、对象分代年龄01可偏向
另一部分是类型指针,即对象指向他的类型元数据的指针。虚拟机通过该指针判断对象属于哪个类的实例。
(2) 实例数据

存储类中定义的各种类型的字段内容。一般情况下父类定义的变量出现在子类之前。

(3) 对其填充

非必须,占位符的作用。HotSpot虚拟机内存管理要求对象起始地址为8字节的整数倍。

2.3.3 对象的访问定位

寻找对象需要通过虚拟机栈上的对象引用(reference)定位堆中对象的位置,但如何定位由虚拟机实现决定。
两种方式:使用句柄和直接指针
句柄式
  句柄式,Java堆中划分一块内存作为句柄池,对象引用reference中存储的是对象的句柄地址。句柄包含对象的实例数据地址和对象类型地址。
指针式
  指针式,栈中reference对象引用存储的是对象地址,但无法记录对象类型的地址,所以要在对象数据中有记录对象类型所在的地址。
两者差异:

  • 指针式比句柄式少一次访问开销
  • 对象被移动时,句柄式只改变句柄中的实例数据指针,对象引用reference无需改变。而指针式需要改变对象引用。

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

3.1 判断对象是否需要回收

3.1.1 引用计数法

方法:对象中添加引用计数器,引用该对象计数器加一,失效时减一,为0则该对象未被使用。
优点:原理简单,判定效率高
缺点:占用一定额外内存,难以解决循环引用问题
循环引用问题:两个不再被使用的对象相互引用,造成两个对象的计数器都不为0,无法判定是否存活需要回收。

3.1.2 可达性分析算法

(1)介绍

方法:GC Roots的根对象作为起始节点集,沿着这些对象根据引用关系向下搜索,未被搜索到的对象被视为不可达。
优点:解决了循环引用的问题
缺点:复杂
引用链:沿着引用关系搜索的路径为引用链。

(2)GC Roots对象
  • 虚拟机栈中引用的对象(栈帧中的本地变量表)、局部变量、临时变量
  • 方法区中静态属性引用的对象(static Object a)
  • 方法区中常量引用的对象(final Object a)
  • 本地方法栈中JNI引用的对象
  • 虚拟机内部的引用,如基本数据类型对象的Class对象、常驻的异常对象、系统类加载器
  • 被同步锁持有的对象
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
  • 临时对象(局部回收)

3.1.3 再谈引用

为了能够 内存空间足够时,能保留在内存,垃圾回收后内存仍然紧张则抛弃这些对象 ,JDK1.2后Java对引用进行扩充。下面四种引用强度逐渐减弱。

  • 强引用:存在引用赋值则为强引用
  • 软引用:用于描述有用但非必须的对象。被软引用关联的对象,在系统发生内存溢出前,将这些对象列为二次回收的范围。SoftReference<String> s=new SoftReference("asdf");
  • 弱引用:描述非必须对象,只能生存到下次垃圾回收为止。用WeakReference实现。(如ThreadLocal
  • 虚引用:对象是否有虚引用不会对其生存时间构成影响,也无法通过虚引用获得对象实例。虚引用的作用是对象被收集器回收时收到系统通知。用PhantomReference实现虚引用。

3.1.4 生存还是死亡

  可达性分析算法中,至少经过两次标记才能宣告对象死亡。第一次分析后未可达的对象会被第一次标记,随后进行筛选,筛选的条件是对象是否需要执行finalize()方法。
  对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,则这两种情况都被视为不需要执行finalize()方法。
  对象被判定需要执行该方法后,对象被放入一个队列中,并在稍后由虚拟机创建的低优先级线程执行队列中对象的finalize()方法,finalize方法是对象逃脱回收的最后一次机会,可以在方法中重新建立引用。
  回收队列中对象的finalize方法不一定执行完才结束,为了防止执行缓慢或者死循环使回收系统崩溃。
  finalize如今已经被官方声明为不推荐的语法。

3.1.5 回收方法区

  • 方法区可以回收,但条件苛刻
  • 方法区主要回收废弃的常量和不再使用的类型
  • 常量的回收需要判断系统中是否有该常量的引用
  • 类的回收需要满足:
    • 该类的所有实例已经被回收,不存在该类和类的子类的实例
    • 加载该类的类加载器已经被回收(很难达成)
    • 该类对应的java.lang.Class对象没有在任何地方引用,无法通过反射访问该类的方法

3.2 垃圾回收算法

3.2.1 分代收集理论

(1)几个假说:
  • 弱分代假说:大多数对象生存时间短
  • 强分代假说:逃过垃圾收集次数越多的对象约难以死亡
  • 跨带引用假说:跨代引用相对于同代引用仅占极少数
(2)分代收集的困难:分代后对象之间的跨代引用

假如要现在进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象是完全有可 能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的GC Roots之外,再额外遍历整 个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样

最开始的疑惑是为什么要额外遍历整个老年代的对象,后来才猜测只局限新生代的收集时不会遍历整个GC Roots,只会遍历包含新生代引用的GC Roots,这时新生代里若有老年代的引用,这次垃圾收集会漏掉这些对象,所以要遍历一次老年代的对象。
解决跨代引用问题的方法:在新生代上建立全局的数据结构(记忆集Remembered Set),将老年代划分成若干块,标识有跨代引用的块,以后发生Minor GC时,将包含跨代引用的块的对象加入GC Roots扫描

(3)分代理论与堆内存分配关系

在这里插入图片描述

  • 新生代为:1个Eden和2个Survivor区域(s1/s0),默认比例为8:1:1
  • 老年代为:Old Memory
  • 新生代与老年代默认比例为:1:2

在3.2.3垃圾回收器节可以看到,新生代的回收器都是采用的标记-复制算法,回收时将80%的Eden和10%的Survivor回收后存活的对象复制到另一个10%的Survivor中

3.2.2 垃圾回收算法

解释优点缺点相关收集器
标记-清除(sweep)算法可达性分析判定是否是垃圾,然后清除简单1. 空间碎片化,空间不连续可能导致额外触发垃圾回收
2. 效率不稳定,回收时间随着垃圾数量增多而变长
CMS
标记-复制(copying)算法内存分两部分,将一部分空间存活的对象移动到另一部分,并清除整个当前空间高效且无碎片化问题1. 空间利用率不高
2. 存活对象多时,复制开销大
新生代的收集器
标记-整理(compact)算法将存活的对象移动到内存的一端,清除掉边界以外的内存空间利用率高,无碎片化问题存活对象多时,移动开销大Parallel Old、Serial Old

3.2.3 经典垃圾收集器

经典垃圾收集器
图片来源
七个垃圾收集器,连线标识垃圾收集器可以配合使用。

(1)对比
使用位置搭配算法多/单线程目标版本
Serial新生代CMS、Serial Old标记复制算法单线程停顿时间短JDK1-JDK2唯一的新生代收集器
ParNew新生代CMS、Serial Old标记复制算法多线程停顿时间短
Parallel Scavenge新生代Serial Old、Parallel Old标记复制算法多线程吞吐量优先JDK5-JDK8的默认新生代收集器
G1新生代+老年代标记整理(整体)+标记复制(局部)多线程停顿时间短JDK9-JDK19的默认收集器
CMS老年代Serial、ParNew标记清除算法多线程停顿时间短JDK5出现
Serial Old老年代Serial、ParNew、Parallel Scavenge标记整理算法单线程停顿时间短JDK5-JDK8的默认老年代收集器
Parallel Old老年代Parallel Scavenge标记整理算法多线程吞吐量优先JDK6出现
ZGC新生代+老年代JDK11引入,JDK15可以正式使用
(2)Parallel Scavenge与ParNew的其它区别

Parallel Scavenge除了关注吞吐量外,自适应调节策略是区别ParNew的重要特性。

自适应调节策略: 开启自适应策略参数后,虚拟机能够根据系统运行情况,动态调节新生代大小、晋升老年代对象大小等参数,达到最适合的停顿时间或者吞吐量。

(3)CMS收集器

在这里插入图片描述

如名称含义CMS(Concurrent Mark Swap)是并发且基于标记清除算法实现的。目标是低停顿时间

四个阶段目标时间是否需要暂停用户线程(STW)
初始标记标记GC Roots能直接关联到的对象时间短
并发标记从GC Roots直接关联到的对象遍历整个对象图时间长
重新标记修正并发标记期间用户线程造成对象变动比初始标记时间长,比并发标记时间短
并发清除清除掉标记判断死亡的对象时间长

缺点:

  • 对处理器资源敏感。并发阶段使用线程数量的计算方式是(核心数+3)/4,核心数小于4的时候,GC线程固定是1,核心数越小,用户线程数占比越低,会影响应用程序的响应速度。
  • 无法处理浮动垃圾。 浮动垃圾是在并发标记和并发清除过程中产生的新垃圾。因此需要预留一部分空间,需要设置超过一定使用比例就触发GC的阈值。(高了容易存储不足退化成Serial Old,低了频繁触发)
  • 需要解决空间碎片化问题。给出两个解决方法
    • Full GC完成后进行内存整理,但是会STW,停顿时间又会增长
    • 设置执行若干次不进行内存整理的Full GC后,下一次Full GC前进行内存整理
(4)G1收集器

介绍:

将整个堆划分成多个相等的区域(Region)每个区域可以是Eden、Survivor、Old。有一种特殊的区域是Humongous,用于存储大对象。当对象超过Region一半时,将会把它放到多个连续的Humongous中,可以看做老年代一部分。

四个阶段:

在这里插入图片描述

四个阶段目标时间是否需要暂停用户线程(STW)
初始标记标记GC Roots能直接关联到的对象时间短(借用Minor GC同步完成,无额外停顿)
并发标记从GC Roots直接关联到的对象遍历整个对象图时间长
最终标记修正并发标记期间用户线程造成对象变动比初始标记时间长,比并发标记时间短
筛选回收按照回收价值和成本排序,回收一部分区域。时间短
(5)G1与CMS

两者比较相似,但又有许多不同的地方。

G1对比CMS的

优点: G1可以指定最大停顿时间、分Region的内存布局、按收益动态确定回收集、不会产生空间碎片。

缺点: G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。

3.2.4 对象分配策略以及在新生代GC流程

(1)内存分配策略
  1. 对象优先在Eden分配
  2. 大对象直接进入老年代: 需要连续空间的大对象,如字符串及数组,如果超过-XX:PretenureSizeThreshold参数大小,则对象直接进入老年代
  3. 长期存活的对象进入老年代: 对象经过GC的次数被视为年龄,年龄超过-XX:MaxTenuringThreshold,默认15, 会进入老年代
  4. 动态对象年龄判定: Survivor中相同年龄的所有对象大小的综合大于Survivor空间的一半,则年龄大于等于该年龄的对象可以进入老年代
  5. 空间分配担保:
    1. 为了避免Minor GC后发生极端情况没有垃圾回收并且所有对象进入老年代,导致老年代空间不足,所以虚拟机每次Minor GC前先判断老年代是否有足够的连续空间保存晋升老年代的对象。
    2. 为了提升性能,设置HandlePromotionFailure 参数允许担保失败,则每次Minor GC只检测老年代是否有足够的连续空间保存历次晋升到老年代对象的平均大小
(2)新生代GC流程

在这里插入图片描述

(3)参考

3.3 HotSpot算法细节实现

3.3.1 根节点枚举

所有收集器在进行根节点枚举时都要暂停用户线程,否则分析过程中根节点集合的对象引用还在变化,无法保证分析准确性。

3.3.2 安全点

  有一个OopMap的数据结构可以维护存放对象引用的位置
  HotSpot没有为每条指令生成OopMap,在特定的位置记录这些信息,被称为安全点。用户线程在指令到达安全点后才能暂停。
  两种在垃圾回收时让所有线程跑到安全点后停顿的方式:

  • 抢占式中断:不需要线程的执行代码主动配合,垃圾收集时系统让所以用户线程中断,若用户线程中断的地方不再安全点上,就恢复执行,一会重新中断,直至安全点。
  • 主动式中断:垃圾回收时,不对线程操作,而是设置中断位,每个线程执行时主动轮询该标志,发现中断标志为真时在自己挨近的安全点主动挂起。

3.3.3 安全区域

使用安全点的设计似乎已经完美解决如何停顿用户线程,让虚拟机进入垃圾回收状态的问题了, 但实际情况却并不一定。安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入垃圾收集 过程的安全点。但是,程序“不执行”的时候呢?所谓的程序不执行就是没有分配处理器时间,典型的 场景便是用户线程处于Sleep状态或者Blocked状态,这时候线程无法响应虚拟机的中断请求,不能再走 到安全的地方去中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间。对于 这种情况,就必须引入安全区域(Safe Region)来解决。 安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任 意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。 当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时 间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。当线程要离开安全 区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的 阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以 离开安全区域的信号为止。

3.3.4 记忆集与卡表

3.3.5 写屏障

3.3.6 并发的可达性分析

  可达性分析算法要求全过程基于一个能保障一致性的快照中才能够进行分析,意味着过程必须暂停用户线程。
  根节点枚举的过程中,对象数量相对堆较少,且有优化方式(OopMap),其带来的停顿短暂且固定。
  在堆中遍历对象其停顿时间和堆容量成正比。
三色标记:

  • 白色:对象未被垃圾收集器访问过,分析的开始阶段所有对象都是白色,分析结束阶段,白色的对象意味不可达。
  • 黑色:对象已经被垃圾收集器访问过且对象的所有引用已经扫描过。其实安全存活的,如果有其他对象引用了黑色对象无需重新扫描。黑色对象不会直接指向白色对象。
  • 灰色:垃圾收集器已经访问过,但对象上至少存在一个引用还没扫描过。

收集器在对象图上标记颜色同时用户线程修改引用关系,可能出现两种情况:

  • 原本消亡的的对象标记为存活–可以容忍
  • 原本存活的对象标记为死亡–不可以
    产生对象消失问题的条件:
  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
    解决方法:
  • 增量更新:破坏第一个条件,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象 了
  • 原始快照:破坏第二个条件,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来 进行搜索

第七章 虚拟机类加载机制

7.1 概述

7.2 类加载时机

在这里插入图片描述

7.2.1 概述

  • 类加载流程如上图。其中,加载、验证、准备、初始化、卸载这五个阶段顺序是确定的。
  • 解析有可能在初始化阶段之后开始
  • 加载开始时机不确定,由虚拟机实现

7.2.2 类初始化的六种情况

(1) 遇到new、getstatic、putstatic、invokestatic字节码指令
  • new关键字实例化对象
  • 读取或设置一个类型的静态字段(被final修饰,或者在编译器把结果放入常量池的静态字段除外)
  • 调用静态方法
(2) 反射
(3) 子类初始化
(4) 虚拟机启动时主类初始化
(5) JDK7的动态语言支持
(6) JDK8的默认方法

7.3 类加载过程

7.3.1 加载

  • 通过一个类的全限定名获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在堆内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口

7.3.2 连接-验证

确保Classs文件的字节流中包含信息符合规范要求,防止危害虚拟机安全。
验证阶段会进行下面四个验证动作。

  • 文件格式验证(魔数开头、版本号、常量类型等)
  • 元数据信息语义验证(是否有父类、继承关系是否正确)
  • 字节码验证(对类的方法体内的逻辑进行验证)
  • 符号引用验证(判断该类是否缺少或被禁止访问它依赖的外部类、方法、字段等资源)

7.3.3 连接-准备

将类中定义的静态变量分配内存并设置初始值。(JDK7在方法区,JDK8在堆)

public static int value = 123;

在准备时会被设置初始值0,在初始化阶段才会设置为123

有两个前提

  • 被赋值变量为基本类型或String
  • 值为字面量而不是方法的形式

static int a = 1; // 准备阶段赋值
static int a = getA(); // 初始化阶段赋值

7.2.4 连接-解析

常量池内的符号引用转换为直接引用
常量池:
class文件解析出的二进制字节码里定义了常量池,字节码被加载到方法区后,该常量池变成了运行时常量池。常量池是class文件的一部分,保存编译时确定的数据。

常量池包含的内容:

  • 字面量
    • 文本字符创
    • 被声明为final的常量
    • 基本数据类型的值
    • 其他
  • 符号引用
    • 类和结构的完全限定名
    • 字段名称和描述符

符号引用:
用符号代表所引用的目标,类、方法、变量等完全限定名都可以被当做符号引用。

直接引用:
直接指向目标的指针、相对偏移量、间接定位到目标的句柄

7.2.5 初始化

  • 初始化是执行类构造器<clinit>方法的过程
  • <clinit>不是Java代码里编写的,而是Javac编译器的自动生成物。
  • 编译期自动收集类中所有变量的赋值动作和静态语句块,按在代码中出现的顺序合并
  • 虚拟机保证父类的<clinit>一定在子类<clinit>之间执行,因此在调用子类<clinit>时不需要显式调用父类<clinit>
  • 同一个类加载器下,一个类只会被加载一次

7.4 类加载器

7.4.1 类与类加载器

  • 对于任意一个类,必须由加载它的类加载器和这个类本身共同确立其在Java虚拟机中的唯一性
  • 每个类加载器都有独立的类命名空间
  • 如果一个Class文件被两个类加载器加载,那么这两个类对象在比较时是不同的。(instanceof、equals等)

7.4.2 双亲委派模型

在这里插入图片描述

(1) 三层类加载器

启动类加载器(Bootstrap Class Loader)

  • 是虚拟机自身的一部分
  • 加载存放在<JAVA_HOME>\lib目录下的类库
    扩展类加载器(Extension Class Loader)
  • 加载存放在<JAVA_HOME>\lib\ext目录下的类库
    应用程序类加载器(Application Class Loader)
  • 加载用户类路径(ClassPath)上的类库
  • 默认的类加载器

用户自定义类加载器

  • 通过集成java.lang.ClassLoader实现
(2) 双亲委派模型

工作过程:

  • 一个类加载器收到了类加载请求的时候,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,
  • 因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,
  • 只有当父类在自己负责范围内找不到,子类才会自己尝试去完成加载。
    好处:
  • 最后都是由启动类加载器往下找到第一个合适的类加载器去加载
  • 防止相同完全限定名的类被不同的类加载器加载,造成类名相同但实际不等的情况

7.5 Java模块化系统

第十二章 Java内存模型与线程

12.1 概述

12.1.1 每秒事务处理数(Transactions Per Second) TPS

一秒内服务端平均能相应的请求总数,和并发能力密切相关

12.1.2 现代计算机系统加入缓存的原因

计算机的存储设备和处理器的运算速度有几个数量级的操作,IO操作让处理器等待数据,会影响整体速度。

将运算的数据从内存复制到缓存中,处理器和缓存之间的IO比内存之间的IO时间小,整体处理速度快。当运算结束后再从缓存同步到内存中

12.1.3 缓存带来的问题-缓存一致性

多路处理器系统中,每个处理器有自己的高速缓存,且共享同一主存。当这些处理器运算都设计到同一个主存区域时,对主存中的同一个数据可能在各自的缓存中不相同,这时无法判断以谁的缓存数据为准。

12.1.4 并发三种问题

(1) 可见性

在这里插入图片描述

原因: 一个线程修改主内存的值后,其它线程可能还在使用工作内存的副本,导致无法感知值的变化,进而产生bug。

**思路:**需要保证其它线程在主内存的值改变时删掉工作内存该值的缓存,从主内存获取数据。
措施:

  • volatile关键字
  • 内存屏障
  • synchronized
  • Lock
  • final
  • 等待
  • 线程上下文切换
(2) 有序性

原因: JVM存在指令重排,实际执行的顺序可能与代码顺序不同,在多线程情况下会有影响。
如何保证有序性:

  • volatile
  • synchronized
  • Lock
  • 内存屏障
(3) 原子性

含义: 一个或多个操作全都执行且不被其它操作影响,或者全部不执行

保证原子性:

  • 64位机器对基础类型变量的读取和赋值是原子性的
  • synchronized关键字
  • Lock
  • CAS

12.1.5 内存屏障

概念: 内存屏障是一条指令,可以对编译器和硬件的指令重排序进行限制。

12.2 Java内存模型(Java Memory Model)

屏蔽硬件和操作系统的内存访问差异,在各个平台下达到一致的内存访问效果

关注在虚拟机中,将变量存储和取出内存的细节

为了更好的执行效率,Java内存模型没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器对代码执行顺序的优化

JDK5之后才成熟

JMM与计算机硬件架构的关系图:
在这里插入图片描述

12.2.1 主内存与工作内存

特点:

  • Java内存模型规定所有变量存储在主内存。

  • 每条线程有自己的工作内存,工作内存保存了该内存使用的变量的主内存副本。

  • 线程对变量的所有操作必须在工作内存中进行,不能直接读写主内存中的数据。

  • 不同线程无法直接访问对方的工作内存的变量。

  • 线程间变量传递需要通过主内存完成

主内存、工作内存和虚拟机之间的对应关系关系:

  • 主内存可以对应Java堆中的对象实例数据
  • 工作内存对应虚拟机栈中的部分区域

12.2.2 内存间的交互操作

讲述了一个变量从主内存到工作内存到计算再到回到主内存的过程

Java内存模型定义了八种操作,每种操作都是原子的、不可再分的

作用位置功能
lock锁定主内存把主内存的变量标识为一个线程独占的状态
unlock解锁主内存把主内存的变量解锁
read读取主内存把变量的从主内存传输到工作内存
load载入工作内存将工作内存中read获取的值放入变量副本中
use使用工作内存将工作内存中变量的值传给执行引擎(虚拟机遇到需要使用变量值的字节码指令时执行,加减乘除等)
assign赋值工作内存将执行引擎上的值赋值给工作内存的变量
store存储工作内存将工作内存变量的值传送到主内存中
write写入主内存将主内存中,store操作获取的值放入主内存的变量里

在这里插入图片描述

操作的一些规则:

  1. 不允许read和load,store和write的操作之一单独出现

  2. 工作内存的数据发生变化前(没有assign操作)不应该将数据同步回主存

  3. 不允许线程对其最近的assign操作,即变量在工作内存改变后必须同步回主内存(不太理解)

  4. 不允许在工作内存中使用(use或store操作)未被初始化的变量(load或assign可以初始化变量),

  5. 同一个变量同一时刻只允许一个线程对其lock操作,lock可以被同一个线程重复执行多次,多次lock需要同样次数的unlock才可以解锁

  6. 一个主内存中变量被lock后,所有工作内存中该变量的副本值都会被清除,在执行引擎使用这个变量前,需要重新执行load或assign操作来初始化这个变量

  7. 无法unlock没被lock的变量,也无法unlock被其线程锁定的变量

  8. unlock之前必须先将变量同步回主内存中

插一个想法,一个线程在一个变量上加锁后,其他线程还可以改变这个变量,只要访问变量的时候不去获取它的锁

12.2.3 volatile

volatile变量的两个特性:

  • 此变量对所有线程可见,当一条线程修改这个变量的值,其他线程立刻就可以知道这个变量的新值
  • 禁止指令重排序优化

volatile在三种特性上的体现:

  • 可见性:对于volatile变量的读,总是能看到任意线程对这个变量最后的写入
  • 原子性:对于volatile的读、写具有原子性
  • 有序性:对于volatile修饰的变量,在读写操作前后加上内存屏障来禁止指令重排序,保证有序性。

volatile读-写的内存语义:

  • 读volatile变量:JMM把该线程对应的工作内存该变量副本无效,从主内存读取该变量。
  • 写volatile变量:在写入工作内存后,会立即刷新到主内存

volatile实现原理:

  • JMM内存交互层面:volatile修饰的变量的read、load、use操作和assign、store、write必须是连续的。修改后立即同步回主内存,使用时必须从主内存获取。
  • 硬件层面:通过lock前缀指令,会锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

并发时,该变量参与的代码也有可能线程不安全,通过下面两点保证原子性

比较普遍的就是 i++

  • 运算结果不依赖变量当前值,或者确保只有单一线程修改变量的值。
  • 变量不需要与其他状态变量共同参与不变约束

指令重排序:

程序运行时,编译器和CPU可能对指令优化,重新排序

性能:

  • volatile读性能和普通变量相差不多
  • 写可能比正常的慢一点,但比锁开销小

Java内存模型对volatile变量的规则:

设T为线程,V和W为两个volatile变量,在进行操作时要满足一些规则

这一块没明白,见书中12.3.3

12.2.4 针对double和long类型变量的特殊规则

允许虚拟机对没有被volatile修饰的64位数据分两次32位的操作进行,导致long和double的操作可能非原子性

其它

1. 参考备忘

2. 更新

  1. 垃圾收集器部分补充【3.2.3】节
  2. 分代理论在堆中的体现
  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值