类加载过程和类加载器

字节码

在聊 Java 类加载机制之前,需要先了解一下 Java 字节码,因为它和类加载机制息息相关。

计算机只认识 0 和 1,所以任何语言编写的程序都需要编译成机器码才能被计算机理解,然后执行,Java 也不例外。

Java 在诞生的时候喊出了一个非常牛逼的口号:“Write Once, Run Anywhere”,为了达成这个目的,Sun 公司发布了许多可以在不同平台(Windows、Linux)上运行的 Java 虚拟机(JVM)——负责载入和执行 Java 编译后的字节码。
在这里插入图片描述
到底 Java 字节码是什么样子,我们借助一段简单的代码来看一看。

源码如下:

package com.cmower.java_demo;

public class Test {

    public static void main(String[] args) {
        System.out.println("沉默王二");
    }

}

代码编译通过后,通过 xxd Test.class 命令查看一下这个字节码文件。

xxd Test.class
00000000: cafe babe 0000 0034 0022 0700 0201 0019  .......4."......
00000010: 636f 6d2f 636d 6f77 6572 2f6a 6176 615f  com/cmower/java_
00000020: 6465 6d6f 2f54 6573 7407 0004 0100 106a  demo/Test......j
00000030: 6176 612f 6c61 6e67 2f4f 626a 6563 7401  ava/lang/Object.
00000040: 0006 3c69 6e69 743e 0100 0328 2956 0100  ..<init>...()V..
00000050: 0443 6f64 650a 0003 0009 0c00 0500 0601  .Code...........
00000060: 000f 4c69 6e65 4e75 6d62 6572 5461 626c  ..LineNumberTabl

感觉有点懵逼,对不对?

懵就对了。

这段字节码中的 cafe babe 被称为“魔数”,是 JVM 识别 .class 文件的标志。文件格式的定制者可以自由选择魔数值(只要没用过),比如说 .png 文件的魔数是 8950 4e47。

至于其他内容嘛,可以选择忘记了。

类加载过程

了解了 Java 字节码后,我们来聊聊 Java 的类加载过程。

Java 的类加载过程可以分为 5 个阶段:载入、验证、准备、解析和初始化。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。

  1. Loading(载入)
    JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class 对象
  2. Verification(验证)
    JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查。
  • 确保二进制字节流格式符合预期(比如说是否以 cafe bene 开头)。
  • 否所有方法都遵守访问控制关键字的限定。
  • 方法调用的参数个数和类型是否正确。
  • 确保变量在使用之前被正确初始化了。
  • 检查变量是否被赋予恰当类型的值
  1. Preparation(准备)
    JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。

也就是说,假如有这样一段代码:

public String chenmo = "沉默";
public static String wanger = "王二";
public static final String cmower = "沉默王二";

chenmo 不会被分配内存,而 wanger 会;但 wanger 的初始值不是“王二”而是 null。

需要注意的是,static final 修饰的变量被称作为常量,和类变量不同。常量一旦赋值就不会改变了,所以 cmower 在准备阶段的值为“沉默王二”而不是 null。
4. Resolution(解析)

该阶段将常量池中的符号引用转化为直接引用

在编译时,java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。
比如org.simple.People类引用org.simple.Tool类,在编译时People类并不知道Tool类的实际内存地址,因此只能使用符号org.simple.Tool(假设)来表示Tool类的地址。
而在类装载器装载People类时,此时可以通过虚拟机获取Tool类 的实际内存地址,因此便可以既将符号org.simple.Tool替换为Tool类的实际内存地址,及直接引用地址。

总结:JVM对于直接引用和符号引用的处理是有区别的,可以看到符号引用时,JVM将使用StringBuilder来完成字符串的 添加,而直接引用时则直接使用String来完成;直接引用永远比符号引用效率更快,但实际应用开发中不可能全用直接引用,要提高效能可以考虑按虚拟机的思维来编写你的程序。
直接引用:

public class StringAndStringBuilder{
   public static void main(String[] args){    
       System.out.println ("s=" + "asdfa");
   }
}

符号引用:

public class StringAndStringBuilder{
   public static void main(String[] args){    
      String s="asdfa";
        System.out.println ("s=" + s);
   }
}

/** 
 *  
 */  
package com.bzu.csh;  
  
import java.util.Random;  
  
/** 
 *  
 * 项目名称:Test1 类名称:Test2 类描述: 创建人:admin 创建时间:2017年1月7日 下午9:41:36 修改人:admin 
 * 修改时间:2017年1月7日 下午9:41:36 修改备注: 
 *  
 * @version 
 *  
 */  
class Test4  extends Test6 {  
    public static final int i = 6 / 2;   								// 编译时就处理了,即i =  3;不初始化类,static代码块和静态初始化块不执行  
    public static final int j = new Random().nextInt();	// 运行时处理,需要初始化类,都执行  
    static {  
        System.out.println("static 静态代码块");  
    }  
    {  
        System.out.println("初始化块");  
    }  
}  
  
public class Test3 {  
  
    /** 
     * @param args 
     *            2017年1月7日 
     */  
    public static void main(String[] args) {  
        System.out.println(Test4.i);  
    }  
}  

// Test4 t = new Test4();结果如下:执行了初始化块  
        // Test3  
        // Test6 static 静态代码块3:9  
        // Test4 static 静态代码块3:0  
        // Test6 初始化块  
        // Test4 初始化块  
        /***********************************************************/  
        // System.out.println(Test4.j);// 这个不执行初始化块  
        // Test3  
        // Test6 static 静态代码块3:4  
        // Test4 static 静态代码块3:8  
        // 8  
        /***********************************************************/  
        System.out.println(Test4.b);// 不会初始化Test4,因为b定义在了Test6  
        // Test3  
        // Test6 static 静态代码块3:8  
        // 8  
  
    }  
}  
  1. Initialization(初始化)

该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。
oh,no,上面这段话说得很抽象,不好理解,对不对,我来举个例子。

String cmower = new String("沉默王二");

上面这段代码使用了 new 关键字来实例化一个字符串对象,那么这时候,就会调用 String 类的构造方法对 cmower 进行实例化。

类加载器

聊完类加载过程,就不得不聊聊类加载器。

一般来说,Java 程序员并不需要直接同类加载器进行交互。JVM 默认的行为就已经足够满足大多数情况的需求了。不过,如果遇到了需要和类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就不得不花大量的时间去调试
ClassNotFoundException 和 NoClassDefFoundError 等异常。

对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 equals)。

站在程序员的角度来看,Java 类加载器可以分为三种。

1)启动类加载器(Bootstrap Class-Loader), 加载 jre/lib 包下面的 jar 文件,比如说常见的 rt.jar。

2)扩展类加载器(Extension or Ext Class-Loader), 加载 jre/lib/ext 包下面的 jar 文件。

3)应用类加载器(Application or App Clas-Loader),根据程序的类路径(classpath)来加载 Java 类。

来来来,通过一段简单的代码了解下。

public class Test {

    public static void main(String[] args) {
        ClassLoader loader = Test.class.getClassLoader();
        while (loader != null) {
            System.out.println(loader.toString());
            loader = loader.getParent();
        }
    }

}

每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 类名.class.getClassLoader() 可以获取到此引用;然后通过 loader.getParent() 可以获取类加载器的上层类加载器。

这段代码的输出结果如下:

sun.misc.Launcher$AppClassLoader@73d16e93
sun.misc.Launcher$ExtClassLoader@15db9742

第一行输出为 Test 的类加载器,即应用类加载器,它是 sun.misc.Launcher A p p C l a s s L o a d e r 类 的 实 例 ; 第 二 行 输 出 为 扩 展 类 加 载 器 , 是 s u n . m i s c . L a u n c h e r AppClassLoader 类的实例;第二行输出为扩展类加载器,是 sun.misc.Launcher AppClassLoadersun.misc.LauncherExtClassLoader 类的实例。那启动类加载器呢?

按理说,扩展类加载器的上层类加载器是启动类加载器,但在我这个版本的 JDK 中, 扩展类加载器的 getParent() 返回 null。所以没有输出

双亲委派模型

如果以上三种类加载器不能满足要求的话,程序员还可以自定义类加载器(继承 java.lang.ClassLoader 类),它们之间的层级关系如下图所示。
在这里插入图片描述
这种层次关系被称作为双亲委派模型:如果一个类加载器收到了加载类的请求,它会先把请求委托给上层加载器去完成,上层加载器又会委托上上层加载器,一直到最顶层的类加载器;如果上层加载器无法完成类的加载工作时,当前类加载器才会尝试自己去加载这个类

PS:双亲委派模型突然让我联想到朱元璋同志,这个同志当上了皇帝之后连宰相都不要了,所有的事情都亲力亲为,只有自己没精力没时间做的事才交给大臣们去干。

使用双亲委派模型有一个很明显的好处,那就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,这对于保证 Java 程序的稳定运作很重要。

上文中曾提到,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等——双亲委派模型能够保证同一个类最终会被特定的类加载器加载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值