7

[JVM] JVM 是如何加载 Java 类的?

 3 years ago
source link: https://www.dynamic-zheng.com/posts/743ef9a0.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

[JVM] JVM 是如何加载 Java 类的?

2020-11-15

|

2020-11-22

| JVM

| 1

看到这个题目的时候,你可能就会觉得,这不是挺简单的一个问题么

如何加载?不就是 加载,链接,初始化 这三步嘛,说白了不就是类加载过程么
那么,你知道这三步具体又做了什么嘛?这就是本篇文章想要写的

加载的过程,就是查找字节流,并根据查找到的字节流来创建类的一个过程
Java 语言的类型可以分成两大类:基本类型和引用类型.基本类型就是由 JVM 预先定义好的,所以也就没有查找字节流这一说了
对于引用类型来说的话,又可以细分为四种:类,接口,数组类和泛型参数.因为泛型参数在编译过程中会被擦除,所以在 JVM 中就只有前三种.而数组类又是由 JVM 直接生成的,所以查找字节流的话,就只有类和接口了.

那么 JVM 是怎么查找字节流的呢?如果你对这块内容比较熟的话,应该就能想起来类加载器,它主要有四类: 启动类加载器,扩展类加载器,应用程序类加载器和用户自定义类加载器
这块又有个知识点就是双亲委派机制:大概就是如果一个类加载器收到了类加载的请求,首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成.通过双亲委派机制就能保证同样一个类只被加载一次
经过类加载器之后,这个类就算是加载进来了

链接这块又分为三部分:验证,准备,解析

验证阶段就是想要看看 class 文件的前 8 位是不是 java 标识符,想看看符不符合规范什么的

准备阶段就是给静态字段分配内存.除了分配内存之外,部分 JVM 还会在此阶段构造其他跟类层次相关的数据结构,比如说用来实现虚方法的动态绑定的方法表
在 class 文件被加载到 JVM 之前,这个类没办法知道其他类和方法,字段所对应的具体地址,甚至都不知道自己的方法,字段的地址,所以如果需要引用这些成员时, Java 编译器就会生成一个符号引用,在运行阶段,这个符号引用一般都可以准确的定位到具体目标上

解析阶段主要就是将符号引用解析成实际引用.如果符号引用指向一个未被加载的类,或者没有被加载类的字段或方法,此时解析阶段就会触发这个类的加载(但不一定会触发这个类的链接以及初始化)

在 Java 代码中,如果想要初始化一个静态字段,可以在声明的时候直接赋值,也可以选择在静态代码块中对它赋值
如果直接赋值的静态字段被 final 修饰了,而且这个静态字段是基本类型或者字符串时,就会被 Java 编译器标记成常量值,初始化就直接被 JVM 完成了.除此之外的直接赋值操作,还有所有静态代码块中的代码,就会被 Java 编译器放到同一个方法中,并且把它命名为 <clinit>
类加载的最后一步就是初始化,就是给标记为常量值的字段赋值,执行 <clinit> 方法的过程.这个时候 JVM 会通过加锁来确保类的 <clinit> 方法只被执行一次
至此, JVM 成功的加载了 Java 类

类的初始化何时会被触发?

那么,类的初始化什么时候会被触发呢?
JVM 规范列举了以下几种触发情况:
1 , 当虚拟机启动时,初始化用户指定的主类;
2 ,当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
3 ,当遇到调用静态方法的指令时,初始化该静态方法所在的类;
4 ,当遇到访问静态字段的指令时,初始化该静态字段所在的类;
5 ,子类的初始化会触发父类的初始化;
6 ,如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
7 ,使用反射 API 对某个类进行反射调用时,初始化这个类;
8 ,当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类

再谈 双亲委派机制

在上面类加载机制那块,提了一下双亲委派机制
我觉得之所以有这样的机制,就是为了避免资源的浪费.上面的双亲委派机制我们在现实中也可以找到例子,比如说:公司部门有位程序员 A 发现如果做一个数据系统的话,来把公司各部门的数据打通,这样就可以减少很多交流成本,那么他可能就会和老大去说,申请去做这个系统,如果老大发现这个完全可以写成公共系统啊,就会对 A 说,这个系统我来做就可以了(公共内容父类加载器进行加载),那如果老大发现不太适合做成公共系统,就会对 A 说,想做就去做吧(父类不进行加载时,子类才进行加载)巧的是,程序员 B 也发现了,他也去找老大说,这个时候老大会说什么呢?这个事情 A 去做了,就不用太担心了(同样一个类只加载一次)
那如果程序员 A 和 B 发现了之后没有和老大交流,都自己闷头去做了,这样的话,同样的系统做了两遍,还浪费了两个人的时间精力,由此造成的资源浪费太大了
我觉得双亲委派的机制类似于这样,因为这个机制的存在,让资源浪费的现象大大减少了

但是 tomcat 打破了这种机制,这怎么说?
我们都知道 tomcat 是个 web 容器,那么它应该:

  • 支持部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,就比如两个应用程序,其中一个依赖的是一个类库的 v1.0 ,另外一个依赖的是同样一个类库的 v2.0 ,那么 tomcat 是不是应该允许这个类库的 1.0 和 2.0 版本都存在?
  • 部署在同一个 web 容器中相同的类库相同的版本是应该可以共享的.就比如,服务器上有 100 个应用程序,这些程序依赖的都是相同的类库,那 tomcat 总不能把这 100 份相同的类库都加载到虚拟机里面去吧,要是非要加载进去,那服务器不得分分钟炸了
  • web 容器需要支持 jsp 文件的修改,也就是说,当程序运行之后,我对 jsp 文件进行了修改,那么 tomcat 是不是也应该支持?如果不支持的话,那我修改一次就不能用了,不合适吧?

基于上面三点,就能看到 tomcat 其实是打破了双亲委派机制的
比如第一个问题,第三方类库就是同样一个资源,在双亲委派机制中,同样一个资源是不应该加载两次的,但是在 tomcat 里面却被允许了;但是第二个问题好像又在说双亲委派的机制,正是因为双亲委派机制的存在,所以第二个问题就不是问题了嘛;第三个问题又打破了双亲委派机制,因为如果不打破的话,原来的 jsp 文件已经加载进来了,现在对它进行了修改,那么应该还会加载原来的 jsp 文件,这样的话修改岂不是无效了?
所以, tomcat 打破了双亲委派机制,但并不是完全打破

至于 tomcat 打破双亲委派的机制,我还没搞懂,等我搞懂了再来写吧
或者你搞懂了嘛?给我讲讲~

参考: 极客时间 – 深入拆解 Java 虚拟机

以上,感谢您的阅读哇~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK