JVM学习笔记3-类加载器

      类加载器用来把类加载到JVM中. 从JDK1.2开始,类的加载过程采用父亲委托机制(保证Java平台的安全).在此委托机制中,除了JVM自带的根类加载器外,其余的类加载器都有一个父加载器.
      当Java程序请求加载器loader1加载Sample类时,loader1首先委托紫的父加载器去加载Sample类,若父类加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载Sample类

有两种类型的类加载器

  • JVM自带的加载器

    • 根类加载器(Bootstrap)–启动类加载器
    • 扩展类加载器(Extension)
    • 系统(应用)类加载器(System)
  • 用户自定义的类加载器

    • java.lang.ClassLoader的子类
    • 用于可以定制类的加载方法

  • 类加载器并不需要等到某个类被"首次主动使用"时再加载它

    • JVM规范允许类加载器在预料到某个类将要被使用是就预先加载它.如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用时才报告错误(LinkageError错误)
    • 如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
  • 根类加载器(Bootstrap)–启动类加载器

    • 无父加载器
    • 负载加载虚拟机的核心类库,如java.lang.*
    • 根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库
    • 根类加载器的实现依赖于底层OS,属于VM实现的一部分
    • 他并没有继承java.lang.ClassLoader
    • 内建于JVM中的启动类加载器会加载java.lang.ClassLoader以及其他的java平台类. 当JVM启动时,一块特殊的机器码会执行,它会加载扩展类加载器和系统类加载器,这块特殊的机器码叫做启动类加载器(bootstrap ClassLoader)
    • 启动类加载器并不是Java类,而其他的加载器都是Java类
    • 启动类加载器是特定于平台的机器指令,它负责开启整个加载过程
    • 所有类加载器(除了启动类加载器)都被实现为Java类,不过,总归要有一个组件来加载第一个Java类加载器,从而让整个加载过程能够顺利进行下去,加载第一个纯Java类加载器就是启动类加载器的职责
    • 启动类加载器还会负责加载供JRE正常运行所需的基本组件,这包括java.utiljava.lang包中的类等等

  • 扩展类加载器(Extension)

    • 它的父类为根类加载器,且由由启动类加载器加载
    • java.ext.dirs系统属性所指定的目录中加载类库
    • 或从JDK安装的目录的jre/lib/ext子目录(扩展目录)下加载类库
    • 如果把用户创建的jar文件放在这个目录下,也会自动由其加载
    • 是纯Java类,是java.lang.ClassLoader的子类

  • 系统类加载器(System)—也称为应用类加载器

    • 父类为扩展类加载器,由由启动类加载器加载
    • 从环境变量classpath或者系统属性java.class.path所指向的目录中加载类
    • 是用户自定义的类加载器的默认父加载器
    • 是纯Java类,是java.lang.ClassLoader的子类

      除了以上JVM自带的加载器外,用户还可以定制自己的类加载器.Java还提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类

用户自定义类加载器
系统类加载器
扩展类加载器
根类加载器

类加载器的双亲委托机制

      在父亲委托机制中,各个加载器按照父子关系形成了树形结构,除了根类加载器外,其余的类加载器都有且只有一个父加载器

加载
load2
load1
系统类加载器
Simple类
扩展类加载器
根类加载器
  • 加载器之间的父子关系实际上指的是加载器对象之间的包装关系,而不是类之间的继承关系.
  • 一对父子加载器可能是同一个类加载器类的两个实例,也可能不是.
  • 在子加载器对象中包装了一个父加载器对象.
  • 如上图所示,如果loader1和loader2都是MyClassLoader类的实例,并且loader2包装了loader1,loader1就是loader2的父加载器.

类加载器结构

由C++实现
启动类加载器
不是ClassLoader的子类
扩展类加载器
应用加载器
自定义加载器
BootStrap ClassLoader
加载JRE\lib\rt.jar里所有的class或者
-Xbootclasspath选项指定的jar包
Extension ClassLoader
加载JRE\lib\ext\*.jar或者
-Djava.ext.dirs指定目录下的jar包
App ClassLoader
加载classpath或者
-Djava.class.paht指定目录下的类和jar包
Custom ClassLoader
通过java.lang.ClassLoader的子类自定义加载class
  • 左边—自底向上检查类是否已经加载
  • 右边—自顶向下尝试加载类
public class MyTest13 {
    public static void main(String[] args) {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        System.out.println(classLoader);
        while (classLoader != null){
            classLoader = classLoader.getParent();
            System.out.println(classLoader);
        }
    }
}

sun.misc.Launcher A p p C l a s s L o a d e r @ 135 f b a a 4 s u n . m i s c . L a u n c h e r AppClassLoader@135fbaa4 sun.misc.Launcher AppClassLoader@135fbaa4sun.misc.LauncherExtClassLoader@2503dbd3
null


public class MyTest18 {
    public static void main(String[] args) {
		//获取启动类加载器加载class文件路径
        System.out.println(System.getProperty("sun.boot.class.path"));
        //获取扩展类加载器加载class文件路径
        System.out.println(System.getProperty("java.ext.dirs"));
        ////获取系统类加载器加载class文件路径
        System.out.println(System.getProperty("java.class.path"));
    }
}

定义类加载器和初始类加载器

  • 定义类加载器–能够成功加载自定义类的类加载器
  • 初始类加载器–能成功返回Class对象引用的类加载器(包括定义类加载器)
public class MyTest7 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> clazz = Class.forName("java.lang.String");
        ClassLoader classLoader = clazz.getClassLoader();
        System.out.println(classLoader);

        Class<?> class2 = Class.forName("main.jvm.classloader.C");
        System.out.println(class2.getClassLoader());
    }
}
class C{
}

执行结果:

null
sun.misc.Launcher$AppClassLoader@135fbaa4

public class MyTest12 {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        Class<?> clazz = loader.loadClass("main.jvm.classloader.CL");
        System.out.println(clazz);
        System.out.println("+++");
        clazz = Class.forName("main.jvm.classloader.CL");
        System.out.println(clazz);
    }
}
class CL{
    static {
        System.out.println("CL static block");
    }
}

class main.jvm.classloader.CL
+++
CL static block
class main.jvm.classloader.CL

结论:

  • 反射会导致类的初始化,即对类进行主动使用
  • 加载类则不是对类的主动使用,不会导致类的初始化

获得类加载器的途径

  • 获得当前类的ClassLoader
    clazz.getClassLoader
  • 获得当前线程上下文的ClassLoader
    Thread.currentThread().getContextClassLoader()
  • 获得系统的ClassLoader
    ClassLoader.getSystemClassLoader
  • 获得调用者的ClassLoader
    DriverManager.getCallerClassLoader()

ClassLoader实例分析

  • 数组类的类对象不是由类加载器创建的,而是根据Java运行时自动创建的
  • 数组类的类加载器(由class.getClassLoader()返回)与元素类型的类加载器相同
  • 如果元素类型是基本元素类型,则数组类没有类加载器
public class MyTest15 {

    public static void main(String[] args) {
        String[] strings = new String[2];
        System.out.println(strings.getClass().getClassLoader());
        System.out.println("++++");

        MyTest15[] test15s = new MyTest15[2];
        System.out.println(test15s.getClass().getClassLoader());
        System.out.println("++++");
        
        int[] ints = new int[2];
        System.out.println(ints.getClass().getClassLoader());
    }
}

执行结果:

null
++++
sun.misc.Launcher$AppClassLoader@135fbaa4
++++
null

对执行结果的说明

  • String数组对应的ClassLoader为空是因为String对象是由启动类加载器加载,故为空
  • int数组对应的ClassLoader为空是因为int是基本元素类型,其对于的数组没有类加载器,故为空

类加载器双亲委托模型的好处


  1. 可以确保Java核心库的类型安全
  • 所有的Java应用都至少会引用java.lang.Object类,即,java.lang.Object这个类会被加载到JVM中
  • 如果这个加载过程由Java应用自己的类加载器所完成,那么很可能就会在JVM中存在多个版本的java.lang.Object类,而且这些类之间还是不兼容且不可见(正是命名空间在发挥着作用)
  • 借助于双亲委托机制,Java核心雷虎中的类加载工作都是有启动类加载器来统一完成,从而确保Java应用所使用的都是同一个版本的Java核心类库,他们之间是相互兼容
  1. 可以确保Java核心类库所提供的类不会被自定义的类锁替代
  2. 不同的类加载器可以为相同的名称(binary name)的类创建额外的命名空间,相同名称的类可以并存在JVM中—只需要用不同的类加载器来加载它们即可
  • 不同类加载器所加载的类之间是不兼容的,这就相当于在JVM中创建了一个又一个相互隔离的java类空间,这类技术在很多框架中都得到了实际应用

类加载器加载路径和用于加载该类的定义类加载器(define class loader)所共同决定的, 如果同样名字(即相同的完全限定名)的类是由两个不同的类加载器锁加载,那么这些类就是不同的,即便.class文件的字节码完全一样,并且从相同的位置加载亦如此

在JVMHotSpot的实现中,系统属性sun.boot.class.path如果修改错了,则运行会出错,提示如下错误信息:

Error occurred during initialization of VM
java/lang/NoClassDefFoundError: java/lang/Object


不同类加载器的命名空间关系

每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成

  • 在同一个命名空间中,不会出现类的完整名字(包括类的报名)相同的两个类
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
    补充两点:
  • 子加载器其所加载类能够访问到父加载器所加载的类
          因此由子加载器加载类能看见父加载器加载的类—系统类加载器加载的类能看见根加载器加载的类
  • 父加载器所加载的类不能访问到子加载器所加载的类
          如果两个加载器加密没有直接或间接的父子关系,那么它们各自加载的类相互不可见

Jar hell问题以及解决办法

当一个类或者一个资源文件存在多个jar中,就好存在jar hell问题
可以通过一下代码来真的问题

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
String resourName = "java/lang/Stirng.class";
Enumeration<URL> urls = ClassLoader.getResource(resourName);
while(urls.hasMoreElements()){
	URL url = urls.nextElement();
	System.out.println(url)
}

你可能感兴趣的:(JVM学习笔记3-类加载器)