ClassLoader串烧

传送门:阿里开源框架JVM-Sandbox源码梳理


类的生命周期

[加载-链接-初始化-使用-卸载]

执行和加载的基本认知
ClassLoader串烧_第1张图片
image.png

1.JVM运行时,在创建类/接口前必须先将其加载在JVM中;ClassLoader 是JVM的一个子系统,负责将类信息(通常是.class文件)放入jvm的方法区/原数据区中,并在堆里创建一个与之对应的Class对象,以便让程序运行使用.
2.数组是JVM创建的,不用ClassLoader创建(数组对象.getClassLoader()是null),没有外部的二进制表示.数组中的元素需要被加载;

JVM启动-main方法的调用

1.当我们通过java 命令来启动一个JVM后,其后的参数会有初始类信息,要么是直接指定一个初始类要么是指定一个jar包,jar包规范中在manifest配置文件中,通过 Main-Class 指定main函数所在类;
2.JVM的启动是通过类加载器 加载创建一个初始类(inital class)
3.JVM对这个初始类执行 链接和初始化
4.调用初始类中的 main(String []) 方法
5.执行main方法中的指令;链接(预加载)或加载使用其他的类/接口.

类/接口的创建 将 触发加载

如果类(B)的创建是由别的类(A)所触发的,那可能是如下的方式:
1.A类的运行时常量池引用了B
2.A类中反射操作B

主动加载 vs 手动加载

传送门:类的自动加载和非自动加载

类加载器的预加载和按需加载
  • 预加载 :类加载器并不需要等到某个类被“首次主动使用”时才加载它;在预料某个类将要被使用时会预先加载它,如果预先加载时遇到.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报LinkageError错误;不主动使用的话,不报错。JVM启动时也会直接加载一些核心类,比如java.lang.String类。
  • 按需加载 :JVM 并不是在启动时就把所有的“.class”文件都加载一遍,而是程序在运行过程中用到了这个类才去加载。简单理解程序执行和cl系统的协作关系,如我们的一段代码运行中遇到一个未知的类型,就会让cl系统去加载这个类型,类型加载成功后,程序方可使用
类资源加载的分工

1.ClassLoader是个抽象类,其子类UrlClassLoader 引入URL资源概念,以此方式来约束自己只加载url覆盖范围内的类文件.
2.ExtClassLoader,AppClassLoader 都是UrlClassLoader的子类,各自配置资源路径不同,即最终url不同,管辖范围不同

Bootstrap:Java自带的核心类;JAVA_HOME/JRE/lib/rt.jar或者-Xbootclasspath指定的jar包。
Ext : JRE的扩展目录(JAVA_HOME/jre/lib/ext或者由java.ext.dirs系统属性指定的)中JAR的类包;
App : java中的-classpath或者java.class.path系统属性 或者 CLASSPATH*作系统属性所指定的JAR类包和类路径

3.我们自定义的ClassLoader ,也是负责指定路径下的类文件的加载.
4.获取类的全限定名 所对应的类文件资源

String className = "com/rock/Demo/class";
Enumeration urls =  classLoader.getResources(className);
while (urls.hasMoreElements()){
    URL url = urls.nextElement();//得到基于文件系统的资源信息。
}
双亲委托机制

1.从安全考虑,bootstrap所加载的类(jdk核心类),不能被随意修改替换的;对于核心类,在系统里只能有一份,而且只能是通过bootstrap classloader 在正确的路径下所加载的;
2.双亲委托模型下用户自定义的类装载器不可能装载应该由父亲装载器装载的可靠类,从而防止不可靠甚至恶意的代码代替由父亲装载器装载的可靠代码。
3.实际上,类装载器的编写者可以自由选择不用把请求委托给parent,但正如上所说,会带来安全的问题。
4.了解更多,查看专题 双亲委托机制,

双亲委托就可靠了吗?还要命名空间

定义和初始 类加载器: 双亲委托机制中,发起类载入的并非一定是实际加载类的,可能会由父类加载器实际载入,实际载入的classloader 叫做定义类加载器
举例说明:load2要加载MyClass,但loader1实际装载了MyClass,则loader1为MyClass的定义类装载器

ClassLoader串烧_第2张图片
image.png

发起类加载的ClassLoader叫 initiating loader ,中文叫初始类加载器
两者的关联在于:一个 Java 类的定义类加载器是该类所导入的其它 Java 类的初始类加载器。比如类 A 通过 import 导入了类 B,那么由类 A 的定义类加载器负责启动类 B 的加载过程。如果调用loadClass方法也会作为初始类加载器吗? 查看 定义类加载器、初始类加载器

虚拟机会为每一个类装载器维护一张列表叫做namespace,中文叫命名空间,此处叫做类名空间感觉更合适,namespace中存储的是已经被请求过的类型的全限定名。如load2的的namespace中就会有MyClass的名字.
这个类加载器被标记为这些类型的初始类加载器.
虚拟机在调用loadClass()时,会先调用findLoadedClass方法,方法的作用如下:

Returns the class with the given binary name if this loader has been recorded by the Java virtual machine as an initiating loader of a class with that binary name. Otherwise null is returned.

其实findLoadedClass就是检查这个namespace,如果这个类装载器已经被标记为是这个具有该全限定名的类型的初始类装载器,就会返回表示这个类型的Class实例.

namespace是个类名字典.由该加载器及所有的父加载器所加载类组成。
java虚拟机从安全角度出发,借助名字空间的这个字典容器,来实现容器之间的隔离.和容器内的唯一性检查,在用户视角来看是这样的:

  • 在同一个namespace内的类可以直接进行交互,
  • 同一namespace的类只有一份.
  • 同一个类,可以在每个namespace中都加载一份。
  • 不同namespace的类是不可见的;除非显示地提供了允许它们进行交互的机制如得到类所对应的Class对象的引用,使用反射来操作类.
String path = System.getProperty("user.dir");
URL[] us = {new URL("file://" path "/sub/")};
ClassLoader loader = new URLClassLoader(us);
Class c = loader.loadClass("LoaderSample3");
Object o = c.newInstance();
Field f = c.getField("age");
int age = f.getInt(o);

因为namespace的关系,import方式下,子加载器能访问父加载器所加载的类,但反之则不行;除非直接调用子加载器的.loadClass,得到Class后,也只能使用反射来实例化和操作方法。
如果两个类加载器互相之间没有父子关系,那么他们各自所加载的类相互不可见。
类加载器的命名空间 实例验证

基于namespace所提供的隔离互动,我们可以做模块隔离,依赖隔离等功能.

浑水摸鱼也不行 运行时包(runtime package)

每个类装载器都有自己的命名空间,其中维护着由它装载的类型。所以一个JAVA程序可以多次装载具有同一个全限定名的多个类型。这样一个类型的全限定名就不足以确定在一个JAVA虚拟机中的唯一性。因此,当多个类装载器都装载了同名的类型时,为了唯一表示该类型,还要在类型名称前加上装载该类型的类装载器来表示-[classloader class]。
只有属于同一运行时包的类才能互相访问包可见的类和成员。这样的限制避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况。如装载一个java.lang.virus(病毒),由于父类加载器中找不到,应用类加载器有机会加载这个病毒类(基于同一个包内类型可见性(即默认访问级别),暗示这个类是java api的一部分,将得到访问java.lang中被信任类的特殊访问权限,实施破坏),运行时包限制了这种行为,因为是不同的类装载器.

类加载器的传递性

  • 类加载器的传递性 即是当一个classloader作为定义类加载器加载一个Class的时候,这个Class所依赖的和引用(即import)的所有 Class也由这个classloader负责发起载入(作为初始类加载器),非import的情况,需要显式的使用一个classloader载入,如调用loadClass方法;
  • java应用运行时的初始线程的上下文类加载器 是ClassLoader#getSystemClassLoader();其他情况线程的上下文类加载器,默认是继承其父线程的线程上下文类加载器,可通过SetContextClassLoader方式显示的指定。
    SystemClassLoader是可自定义的,默认未自定义时为AppClassLoader,否则是自定义的ClassLoader;
显式加载(指定类加载器去加载)
  • forName
    用于加载用户定义的类型;不能加载原生类型和void类型。
    forName在JVM走一会儿,还会绕到我们ClassLoader的loadClass方法上(要不然双亲委派怎么保证)?
  1. java.lang.Class#forName(String className,boolean initialize,ClassLoader loader)
    参数1:执行目标类的二进制名称。
    参数2:参数表示类是否需要初始化
    一旦初始化,就会触发目标对象的 static块代码执行,static参数会被赋初值。
    参数3,指定一个执行加载动作的classLoader,若为null,则通过bs加载器来加载。
public static Class forName(String className,boolean initialize,ClassLoader loader) throws ClassNotFoundException {
}
  1. java.lang.Class#forName() 获取并使用调用者的类加载器来加载类的。
public static Class forName(String className) throws ClassNotFoundException {
    //反射方式获取调用者.
    Class caller = Reflection.getCallerClass();
    //获取并使用调用者的类加载器来加载类的。
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

// Returns the class's class loader, or null if none.
static ClassLoader getClassLoader(Class caller) {
    // This can be null if the VM is requesting it
    if (caller == null) {
        return null;
    }
    // Circumvent security check since this is package-private
    return caller.getClassLoader0();
}

/** Called after security checks have been made. */  
private static native Class forName0(String name, boolean initialize,  ClassLoader loader)  
     throws ClassNotFoundException;  
  • java.lang.ClassLoader#loadClass()
    内部实际调用的方法是 ClassLoader.loadClass(className,false);
    第2个 boolean参数,表示目标对象是否进行链接,false表示不进行链接,由上面介绍可以,
    不进行链接意味着不进行包括初始化等一些列步
    骤,那么静态块和静态对象就不会得到执行
破坏双亲委托的边界 -

保证启动类加载器和扩展类加载器的加载职责以及所加载类的安全


相关系列笔记

破坏双亲委托- SPI 线程上下文类加载器
自定义类加载器-依赖隔离-TomcatClassLoader
自定义类加载器-Springboot classloader
自定义类加载器-sofa-ark 模块隔离

你可能感兴趣的:(ClassLoader串烧)