Java类加载机制,这篇大概、也许、可能就够了

写在前面

关于Java类加载机制一至有没办法说的痛苦。因为当初我在学习这方面的内容时,多多少少有一些懵逼,所以这次的文章,将尽可能的把概念性的东西转化成容易理解的内容,所以希望各位看到文章的童鞋可以有所收获~

正文开始

第一步,先让咱们看一段代码:


public class Main {
    static{
        System.out.println("我是静态代码块");
    }
    
    {
        System.out.println("我是实例代码块");
    }
    
    public static void main(String[] args) {
        Main main1=new Main();
        Main main2=new Main();
    }
}

各位小伙伴,这段代码run起来之后会是什么样的结构?这里就不卖关子了,直接贴结果。

image

OK,如果小伙伴们,知道这个结果,并且也理解这个结果,那么接下来的内容就可以跳过啦。如果有疑问的话,那就让我们带着这个答案,往下看,内容很少,。重在理解~


Java类加载机制

先看一下概念

虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。(来源《深入理解Java虚拟机.第二版》以下简称为深入JVM)

概念总是枯燥的,让我们开始对这个概念进行一些便于理解的分析和梳理。

image

梳理

1、加载

首先是加载阶段:此阶段是Java将字节码(.class文件)数据从不同的数据源(我们的jar文件、class 文件,甚至是网络数据源等;只要结构正确即可)读取到JVM中,并映射为JVM认可的数据结构(Class 对象,可以理解成就是java.lang.Class) 。
按照《深入JVM》的描述,这个过程有三步(有部分用词的加工):

  • 1、通过一个类的全限定名来获取定义此类的二进制字节流。(也就是先通过路径找到这个类的.class文件)
  • 2、将这个.class字节流所代表的的代码结构转化为方法区的运行时数据结构。(可以理解为此时已经在虚拟机中成了能够被识别的代码结构)
  • 3、在内存生成能够代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。(这里很不好理解,我对此的理解是:虽然代码结构已经存在,但是我们没有办法直接去使用。因此这里抽象出了java.lang.Class对象作为方位.class文件字节码的接口。)

在这里,我曾经有些疑惑那就是字节码和二进制文件。其实都是.class文件,我们简单编译一下上述的Main.java

image

首先我们移动到Main所在的目录,编译并查看.class的字节码。

image

ok,这是字节码,如果我们使用一些文本编辑器,比如Sublime,我们看到就是二进制形式的文件内容:

image

Tips:加载阶段,我们可以自定义类加载器,去实现自己的类加载过程。

2、连接

连接和加载过程是交叉进行的,也就是说加载阶段没有完成,连接阶段可能就已经开始了。

第二阶段是连接 ,这是核心的步骤,简单说是把原始的类定义信息平滑地转化入JVM运行的过程中。这里可进一步细分为三个步骤:

  • 1、验证 :这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合Java虚拟机规范的,验证阶段有可能触发更多class的加载。
  • 2、准备:创建类或接口中的静态变量,并初始化静态变量的初始值。这里的初始化重点在于分配所需要的内存空间,不会进行赋值,也就是说这里初始化的值是默认值,比如public static int value = 666,此时的value等于0,而非666。而真正的赋值操作在初始化阶段。
  • 3、解析:在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。这段很短的文字我理解了很久,因为它包含了大量的概念:常量池、符号引用、直接引用。接下来逐条解释:

常量池:常量池里除了String对象,final类型的常量,还有符号引用
符号引用:用于描述字节码文件中各字段,各方法、各接口等。我是这么理解的:如果字段、方法都是想象想要旅游的游客的话,那么符号引用就是旅游公司,但是旅游公司只负责收钱组织游客,他们不负责真正带游客出去玩,真正带他们去玩的是导游(直接引用)。也就是说符号引用就是一个能够代表所有字段、方法的这么一个角色。
直接引用:直接引用想到于能够找到对应内存地址的角色,也就是上述例子中的导游。


PS:不知道这么解释能不能理解解析的过程,如果还是迷糊,可以查看知乎大佬对此的专业回答:https://www.zhihu.com/question/30300585

3、初始化

最后是初始化阶段 , 这一步开始执行静态字段赋值的动作,静态初始化块内的逻辑,编译器在编译阶段就已经把该执行的代码逻辑整理好了,这里需要注意的是:父类的初始化逻辑优先于子类的逻辑。

这里有一个细节需要注意:静态代码块中只能访问到定义在静态代码块之前的变量。如果此静态变量在静态代码块后边,那么静态代码块里只能对其赋值,不可访问:

image

加载结束

一直走到这,我们的类正式加载完毕,也是生成了我们对应的Class对象。但是请留意,这里还没有涉及到类的实例化,也就是说此时还没有开始new操作。

当执行new的时候,而且类已经经历过加载,那么才会执行对应的实例化,比如分配内存,执行代码块,构造方法之类的(如果有父类要先对应执行这些内容)。

触发类加载的操作

  • new关键字;get/set一个static变量(final、在编译期进入常量池的静态字段除外);调用static方法。
  • 使用反射,如果此类没有被加载会先进行加载操作。
  • main方法对应的类,会在JVM启动是加载。
  • 使用一些动态代理方式时。

双亲委派机制

关于双亲委派机制,MDove的文章说的简单明了,其实就是一张图:

image

用《深入JVM》的话,解释一下:
双亲委派模型的工作过程:如果一个类加载器收到了一个类的加载请求时,首先它不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层级的类加载器都是如此。因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

尾声

关于类加载的的梳理,到此就结束了,不知道各位小伙伴们有没有理解呢?如果小伙伴们有自己的理解,或者文中有不当之处,欢迎评论区留言~此致敬礼!

我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,已经我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~

个人公众号:IT面试填坑小分队

推荐阅读更多精彩内容