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

看到这个题目的时候,你可能就会觉得,这不是挺简单的一个问题么
如何加载?不就是 加载,链接,初始化 这三步嘛,说白了不就是类加载过程么
那么,你知道这三步具体又做了什么嘛?这就是本篇文章想要写的

加载

加载的过程,就是查找字节流,并根据查找到的字节流来创建类的一个过程
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 虚拟机

以上,感谢您的阅读哇~

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值