java类加载机制知乎,今天我们来聊聊JVM类加载机制

hello 我是宝哥,今天我们来聊聊JVM的类加载过程

71470829587ec49737963ea659632cc5.png

要搞清楚JVM,首先要搞清楚几个问题:

jvm起到什么作用?

怎么加载class文件的?

加载类时会重复吗?顺序是什么样的?

说到jvm 那么不得不提类的加载过程.我们先来了解下类是如何被一步一步加载到jvm的

类的加载过程

我们先笼统的了解一下类加载的整个过程:

bbcfcb52ce6560c0734a6bb9434682aa.png

如上图所示,Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀)

然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。

在整个程序执行过程中,JVM用一段空间来存储程序执行期间需要用到的数据和相关信息,被称作为Runtime Data Area(运行时数据区),也就是我们常说的JVM内存。

此时,我们可以片面的理解为:JVM为我们的class字节码提供了加载,存储,执行的环境.(jvm是java可跨平台运行的基石,因为不同的系统有不同的jvm实现,都可以加载.class字节码文件)

Java的类加载机制

那么ClassLoader都做了什么呢?

我们先来看看 类加载机制的定义:

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制

这里有几个阶段比较重要: 1.加载  2.连接  3.初始化

根据这3个阶段,我们可以剖析出,类的生命周期:

类的生命周期

799da970c4eafd57505efb5e3a12ae4a.png

加载: 加载类的二进制字节流,并且将静态存储结构转化为方法区的运行时数据结构,然后在内存中生成一个代表此类的Class对象,作为方法区这个类各种数据的访问入口

验证: 验证是在连接(Linking)部分的第一步,验证的目的是验证Class文件中的字节流符合当前虚拟机的要求,保证不会危害虚拟机.

准备: 为类变量分配内存,并且设置类变量初始值,此时这此类变量所使用的内存都是在方法区中进行分配.

解析: 解析是将符号引用替换为直接引用,解析动作针对类或接口,字段,类或接口的方法进行解析。

初始化:初始化类或接口并且执行类或接口的初始化方法,此时,它的生命周期就开始了

虚拟机规范规定了有且只有

5 种情况必须

立即对类进行初始化

1.遇到new、getstatic 和 putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应场景是:使用 new 实例化对象、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法。

2.对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

3.当初始化类的父类还没有进行过初始化,则需要先触发其父类的初始化。(而一个接口在初始化时,并不要求其父接口全部都完成了初始化)

4.虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。

5.当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

初始化的时机:

使用: 此时我们可以通过new关键字,创建实例对象.顺带一提,一个Class对象总是会引用它的类加载器。调用Class对象的getClassLoader()方法,就能获得它的类加载器。由此可见,Class实例和加载它的加载器之间为双向关联关系。

卸载: 当类不再被引用或被垃圾回收器标记为已死对象时,将会被回收,但是Java虚拟机本身会始终引用类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的,也就是说jvm自带的类加载器所加载的类,在虚拟机还没有退出时,始终不会被卸载,当然也有特例 如:我们自己定义的类加载器的类是可以被卸载的.

ClassLoader 类加载器

类的唯一性

任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性

这句定义怎么理解呢?

fe861b0202df9a8d3f7e40def877c9cf.png

两个类来源于同一个 Class 文件,被同一个虚拟机加载,但是加载它们的类加载器不同,那这两个类也不相等

那有的小伙伴就有疑惑了,还有很多类加载器吗? emm..那加载的顺序呢?会不会重复加载了?

别急,我们了解下双亲委派原则.

双亲委派原则

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是向上访问,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

这里举例几个面试会问的classloader职责:

Bootstrap ClassLoader:根类加载器,负责加载java的核心类,它不是java.lang.ClassLoader的子类,而是由JVM自身实现;

Extension ClassLoader:扩展类加载器,扩展类加载器的加载路径是JDK目录下jre/lib/ext,扩展类的getParent()方法返回null,实际上扩展类加载器的父类加载器是根加载器,只是根加载器并不是Java实现的;

System ClassLoader:系统(应用)类加载器,它负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性或CLASSPATH环境变量所指定的jar包和类路径。程序可以通过getSystemClassLoader()来获取系统类加载器;

User Define ClassLoader: 用户自定义的classloader,自定义的加载器必须继承ClassLoader。

它们的加载顺序:

68829633723e59a7ab5d749f4e86e6cd.png

show me the code:

// 代码摘自《深入理解Java虚拟机》

protected synchronized Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {

// 首先,检查请求的类是否已经被加载过了,同时也解决了小伙伴的疑虑

Class c = findLoadedClass(name);

if (c == null) {

try {

if (parent != null) {

c = parent.loadClass(name, false);

} else {

c = findBootstrapClassOrNull(name);

}

} catch (ClassNotFoundException e) {

// 如果父类加载器抛出ClassNotFoundException

// 说明父类加载器无法完成加载请求

}

if (c == null) {

// 在父类加载器无法加载的时候

// 再调用本身的findClass方法来进行类加载

c = findClass(name);

}

}

if (resolve) {

resolveClass(c);

}

return c;

}

那么我们不禁要思考一下为何要用这种原则有何优点?

1,一个当然是避免重复加载,提升性能

2,避免了核心类被用户篡改(例如我在用户自定义的classloader中加载一个String类去覆盖自带的String类,由于先让父类加载,我定义的顺序在后.不会出现覆盖成功的问题)

这里有几个点小伙伴需要注意,不然要被面试官吊打了:

类加载器之间的父子关系不会以继承的关系来实现,他们虽然都继承于抽象类

java.lang.ClassLoader但是他们的关系是组合关系,使用组合关系复用父类的加载器.

Bootstrap 类加载器是用 C++ 实现的,

不继承于抽象类java.lang.ClassLoader,它是虚拟机自身的一部分,如果获取它的对象,将会返回 null;

jvm自带的类加载器所加载的类,在虚拟机还没有退出时,始终不会被卸载,我们自己定义的类加载器,加载的类是可以被卸载的.

破坏双亲委派原则

当然类加载器的双亲委派原则是可以被破坏的,破坏它是由于双亲委派模型自身缺陷导致,他没有办法解决用户基础类又要重新调用户类的代码。为了解决这个问题就有了线程上下文加载器,例如JNDI、JDBC、JCE等

举个Tomcat的例子:

8debd1a6025fd8dc92b7dafc6bfbcffc.png

每个Tomcat的webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。tomcat之所以造了一堆自己的classloader,大致是出于下面三个原因:

对于各个 webapp中的 class和 lib,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况,而对于许多应用,需要有共享的lib以便不浪费资源。

与 jvm一样的安全性问题。使用单独的 classloader去装载 tomcat自身的类库,以免其他恶意或无意的破坏;

热部署。相信大家一定为 tomcat修改文件不用重启就自动重新装载类库而惊叹吧。

破坏双亲委派的方式

双亲委派机制原则在loadclass方法中。只需要绕开loadclass方法中即可。

自定义类加载器 ,重写loadclass方法

使用SPI机制绕开loadclass 方法。当前线程设定关联类加载器

关于SPI在我另外一篇文章https://mp.weixin.qq.com/s/2UFHJ_i09APy-VS8oeiUIQ

讲了那么久的加载,此时我们才刚刚一只脚踏进JVM的大门,后面我们将分析JVM运行时数据区.大家持续关注java宝典公众号,我们下章再聊

最近我创建了一个学习群,有热爱学习的小伙伴可以进群讨论~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值