Java类加载机制是Java面试中最常见的面试题之一,虽然我们在日常开发中很少关注Java它的类加载具体是如何进行,但是如果了解了它的加载机制了原理,可以让我们更好的掌握Java这门语言。
我们都知道Java是一门跨平台语言,它的代码运行在Java虚拟机Jvm上,源文件是.java的文件,在经过编译后就会生成一个个.class的类文件。而类加载时,JVM加载的就是.class文件,那么具体是怎么实现的呢,让我们来接着讨论。
对于Java类加载来说,主要有以下几个主要步骤,分别是加载、链接、初始化。其中链接又可以细分为验证、准备、解析散步。可以参考以下下图,让我一个一个来看。
1.1、加载(loading)
加载这个环节,顾名思义主要是讲Java已经编译过得.class类文件通过java的类加载器加载到JVM中去。JVM会查找对应类的全限定名称来找到对应的二进制字节流,然后加载进JVM内存并创建对象。
1.2、验证(Verification)
此环节主要是对加载进来的二进制字节流进行验证,防止出现错误,是否符合JVM的要求,主要是文件格式验证、元数据验证、引用符号验证以及字节码验证。
文件格式验证:验证导入进来的二进制字节流是否符合JVM的规定要求。
元数据验证:对字节码中类的相关属性进行验证,例如类有什么方法、哪些属性等等。
符号引用验证:确保解析动作可以正常执行。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
1.3、准备(Ready)
准备阶段中做的最重要的一件事就为类变量(即被static修饰的变量)分配内存以及设置初始化值,值得注意的是这里的初始化值不是指程序里面写好的初始值,而是JVM默认的对应变量初始值,例如public staitc int a=111,则这里a被初始化为0,而非111.
1.4、解析(Resolution)
解析环节主要是将之前字节流中所有的符号引用都替换成相对应的直接引用,主要针对类、接口、类方法、类变量等等。
1.5、初始化(Initialization)
这是类加载的最后一个环节,在此环节中Jvm才开始真正意义上执行程序猿们编写的代码。它会调用Jvm的clinit方法,而该方法则主要是由静态代码块和所有的类变量的赋值语句收集组合而来,按照程序猿编写的顺序和一定的规则逐个初始化,此时如果该类有父类,同时也会初始化它的父类。
2、类加载器之双亲委派模式
类加载的根加载器是BootStrap ClassLoader引导类加载器,这个加载器是由C++代码启动的,默认保存在<JAVA_HOME>/lib目录下。
拓展类加载器Extension ClassLoader继承了BootStrap ClassLoader加载器,它是保存在<JAVA_HOME>/lib/ext目录下的,可以被用户直接启动。
Application ClassLoader系统类加载器继承自Extension ClassLoader加载器,其主要负责加载用户类路径<CLASSPATH>上所以的类库,供开发者调用。
最后一级则是用户通过继承Application ClassLoader后自己实现的自定义类加载器,用以用户自定义加载类。
为什么要这么设置呢,一个主要的原因是因为在JVM的加载过程中其实是自上而下的,也就是说在一开始其实所有的类都会由BootStrap ClassLoader来尝试加载,只有当其加载失败了之后,才会转而再由其子类加载。所以自定义的类加载器在最下层,才会被其自定义类加载器顺利加载。
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,则会发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。