类加载机制

使用场景

对于动态可配置的功能,需要程序在运行时动态去编译和加载外部的java文件。
热部署、动态代理。

Java如何跨平台的 — 字节码

image.png

.java编译为.class,再由jvm加载字节码 —- 即类加载过程
运行时,解释器将字节码解释为一行行机器码进行执行。
在运行期间,即时编译器会针对热点代码,将该部分字节码编译成机器码已获得更高的执行效率。
在整个运行时,解释和即时编译器相互配合,使java程序可以达到几乎和编译型语言一样的执行速度。

解释执行:将编译好的字节码一行一行地翻译为机器码执行。
编译执行:以方法为单位,将字节码一次性翻译为机器码后执行。

  • 类加载流程的目的:

载入某种形式的class数据结构进入内存,程序可以调用这个数据结构来构造出object。这个过程是在运行时进行的,这也是java动态扩展的根基。

image.png

1、加载 <— 方法区(class静态结构)、堆(Class对象)

image.png
Class文件:并非特指本地文件,而是泛指各种来源的二进制流,如来源于网络、数据、即时生成class。
动态代理技术就是使用了即时生成class,然后实例化代理对象。

2、连接:验证

image.png

  • 验证方法区中的静态结构

文件格式验证元数据验证字节码验证
简单说,对class静态结构进行语法和语义上的分析,保证其不会产生危害虚拟机的行为。

注:验证其实分很多部分,分散在各个阶段,如符号引用验证在解析阶段进行。

2、连接:准备

image.png
jdk8之后,常量池、静态变量移动到了堆中,类的元信息保留在方法区中,但是方法区的存储方式由永久代改为了元空间(直接内存)。

准备阶段:该class类型中定义的静态变量赋0值。( 注意,不涉及成员变量)

2、连接:解析

image.png
解析阶段:将符号引用替换为直接引用
解析阶段可能出现在初始化之前,和初始化之后。初始之前就是静态解析,初始化之后就是动态解析。java通过后期绑定来实现多态,而后期绑定就是动态解析,后期绑定的底层对应了invokedynamic字节码指令

符号引用与直接引用的理解:当java代码被编译为class文件后,假如这里有A、B两个类,并且A中引用了B。在编译阶段,A不知道B有没有被编译,而且此时B也一定没有被加载,所以A不知道B的实际地址。此时在A的class文件中使用一个字符串S来代表B的地址,这个S就是B的符号引用。在运行时,如果A发生了类加载,到解析阶段发现B还未加载,那么将会触发B的类加载,将B加载到虚拟机中,此时A中的符号引用将会被替换为B的实际地址,这被称为直接引用,这样A就能在运行时调用B了。

静态解析:如A调用B的实现类;此时的解析目标很明确,如A调用B的实现类,那么A的class文件中的符号引用可以直接替换为直接引用。

动态解析:如A调用B的抽象类或接口,而B的实现有C和D,此时A无法确定使用B的哪个具体实现类,所以此时,在类加载中,A无法将符号引用进行替换为直接引用,直到运行过程中,发生了调用时,此时虚拟机调动栈中将会得到具体的类型信息,这时候再进行解析,就能用明确的直接引用来替换符号引用。

3、初始化

判断代码中是否存在主动的资源初始化操作,如果有那么执行。
主动的资源初始操作,是class层面的,比如成员变量的赋值动作,静态变量的赋值动作,以及静态代码块的逻辑。(容易混淆的概念,对象层面的资源初始化操作,是指显示的调用new指令后调用构造函数进行对象的实例化。)

类加载机制中,用户可以操作的部分

image.png

Java双亲委派、类加载器

image.png
image.png

Bootstrap classLoader无法被程序引用。
其他加载器继承自Java.lang.ClassLoader,可以作为对象被引用。
User ClassLoader可以加载任意来源的二进制流。

能不能使用Extension ClassLoader来加载自己的类?
可以,但是没必要,不符合规范。

类加载器的命名空间

image.png
image.png

双亲委派模型

image.png

JVM引入双亲委派机制:任何一个类加载器,它都在加载类时,都会首先传递给父亲进行加载,直到最上层的bootstrap classLoader,只有父亲加载器无法加载时,才会交给儿子加载器来尝试加载。
注意: 父亲加载器无法加载是指,根据类的限定名,类没有自己负责的加载路径中找到该类。
注意: 父亲和儿子并非继承关系,而是逻辑上的关系。

image.png
上述parent代表当前类加载器的父亲加载器,当parent==null,约定parent就是bootStrap类加载器。
上述关键是 findClass类和defineClass类,所有类加载器都有自己对应的这两个实现类,且都被final所修饰,即不能被继承。
image.png
image.png

image.png

主动破坏双亲委派

第一次破坏思路

image.png

第二次破坏思路

image.png
JDK提供一种规范,对JDK类库的加载时使用BootStrap类加载器,但当去调用JDK中的接口时,接口所在的类将会引发第三方类库的加载,即上层类加载器去寻找并调用下层类加载器

上述规范即是java的SPI(Service Provider Interface) ,一种服务提供发现机制( 为某个接口寻找服务实现的机制 ),用于启用框架扩展和替换组件, 例如JDBC场景下,java定义了java.sql.Driver接口,不同厂商可以提供不同的实现,而java的SPI机制可以为某个接口寻找服务实现。 Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦
image.png

如下,JDBC场景下,DriverManager被Bootstrap类加载器加载,加载后的class对象,再去寻找并调用下层类加载器。
image.png
下面是DriverManager类中何时调用了Application ClassLoader。
image.png

第三次破坏

用于热部署

JAVA内存模型

硬件内存模型

image.png

缓冲一致性协议:主要解决多个CPU缓存之间的数据同步问题,相关协议大致分为两类,窥探型和基于目录型。数据同步的过程很可能出现等待唤醒等措施,这将导致性能问题,解决办法是将同步改为异步

同步导致的性能问题:比如CPU-1需要读取数据D时,还需要等待CPU-2将D写回主存。
同步改异步能够提升效率:比如CPU-1需要读取数据D时,发现D正在被其他CPU修改,那么此时CPU-1可以注册一个读取D的消息,自己回头去做其他事情,当其他CPU写回数据D后会响应了该注册消息,此时CPU-1发现消息被响应后再去读取数据D。
同步改异步存在的新问题:上述同步改异步场景下,对于CPU-1来说,指令执行可能不是顺序执行的了,即先执行后面的指令,再回头执行前面的指令。解决办法是指令重排序机制,使得无论指令如何重排,最终的结果都是一样的(不展开解释)。

Java内存模型

image.png
java内存模型,实现跨平台。屏蔽了各种硬件和操作系统的内存访问差异。
image.png

每个工作线程,都拥有独占的本地内存。本地内存中,存放私有变量以及共享变量的副本。
使用一定机制来控制本地独占内存和主存之间读取数据时的同步问题。

java内存模型与硬件内存模型之间的映射:关于物理内存的抽象:本地内存—>thread stack ; 主存—>heap,如此开发者只需要关心thread stack和heap,而不需要更底层的寄存器、cpu缓存、主存等等。线程在工作时,大部分时候都在读取thread stack,需要高速的内存硬件,所以thread stack主要使用寄存器和CPU缓存来实现的。而heap用于存储对象,需要大容量的内存硬件,所以heap主要使用主存来实现的。
image.png

thread stack中有两种类型的变量,原始类型变量对象类型变量。原始类型变量存储在thread stack上;对象类型变量的引用(指针)在thread stack上,其对象本身存储在heap上。

heap中存储对象本身,持有对象引用的线程都能够访问该对象,heap并不关心哪个线程正在访问对象。

内存读写指令/线程通信问题

主存(heap)与本地内存(thread stack)之间的数据传输与同步(线程通信)

下面线程A修改x的值,再由B去读取修改后的x。
image.png

线程通信存在可见性问题原子性问题

image.png
可见性问题:上图中,本地内存A和本地内存B中都存在共享变量x的副本。当线程A修改了主存中共享变量x的值 (线程A修改本地内存的副本,并写入到主存中),但线程B却依然默认使用本地内存B中副本,也就是说线程B不知道线程A修改了主存共享变量x的值。

原子性问题:上图中,本地内存A和本地内存B中都存在共享变量x的副本。线程A和线程B分别对各自本地内存中的副本进行自增1操作,并写入到主存中,但最终主存中的共享变量x的值却不符合预期目标。

并发的三要素:可见性、原子性、有序性

可见性:

当一个线程修改共享变量的值,其他线程需要立刻得知这个修改。 有两种解读。

  • 第一种解读,由刷新主存时机引起的可见性问题;

image.png
volataile和synchronized关键字解决原因是:总是主动从主存中读写。
volataile总是使用write()向主存写入,而不是store()在本地内存存储;总是使用read()从主存读取,而不是load()从本地内存载入。

  • 第二种解读,由指令重排序引起的可见性问题。线程B需要读到被修改的变量D,线程A应该修改,但是因为重排序导致线程A没有及时修改变量D。

内存模型和java模型中都存在指令重排序,作用和约束都是一样的,1、提高执行效率,2、单线程中重排后保证程序执行的正确性。
image.png
volataile关键字的解决方法的理解:在上述代码中只要给flag变量加上volataile关键字,就能禁止代码1和代码2的重排。

image.png
上述情况中,如果对代码1和代码2变成原子化,那么无论内部如何重排,外部都只能读到一样的结果。

https://www.bilibili.com/video/BV1F64y1B7sV?p=1&t=744.2