JVM:类加载 & 类加载器

System virtual machine: a.k.a. full virtualization VM. Provides a substitute for a real machine (functionality needed to execute entire OS). Allows for multiple environments which are isolated from one another yet exist on the same physical machine.

 

Process Virtual Machine: a.k.a. application virtual machine or Managed Runtime Environment.

Process VM runs as a normal application inside a host OS and support a single process, is created when that process is started and destroyed when it exits.

Process VM’s purpose is to provide a platform-independent programming environment that abstracts away details of the underlying hardware and operating system and allows a program to execute in the same way on any platform.

e.g. JVM; Parrot virtual machine; .Net Framework.

 

*使用工具:

  Source Insight - 查看openjdk源码

    -> new project->选择openjdk所在目录->add project files时选择add all->add to project中“include top level sub-dirs”和“recursively add lower sub-dirs"全勾选

  clion - 编写底层程序

  idea / netbeans - 单步调试jdk

    -> 新建项目->C++/基于现有源代码的C++项目->选定openjdk所在目录>

  hsdb

 *注:使用jclasslib时,如果源代码有改动,在idea中先build再在jclasslib中点刷新以更新字节码。

jps, jstack, jconsole等。

写JVM的步骤顺序:内存池 -> 垃圾回收器 -> 让JVM能跑单线程的Java程序 -> 让JVM能跑多线程的Java程序

类加载

Klass

Klass: (存在于元空间,)Java的每个类的对象在JVM中都有一个对应的Klass类实例,用于存储类的元信息(e.g. 常量池,属性信息,方法信息)

*元信息包含字面量 (e.g. 类名、方属性名、属性签名、方法名等)

Klass的继承结构

MasterspaceObj

    |- Metadata

        |- Klass

            |- InstanceKlass

                |- InstanceMirrorKlass

                |- InstanceRefKlass

                |- InstanceClassLoaderKlass

            |- ArrayKlass

                |- TyperArrayKlass

                |- ObjArrayKlass

InstaneKlass: 表示普通(非数组)Java类。类加载器将.class文件加载进系统,将.class文件解析生成类的元信息,存储在InstanceKlass中。子类包括InstanceMirrorKlass、InstanceRefKlass和InstanceClassLoaderKlass。

InstanceMirrorKlass:镜像类,表示Java代码中的java.lang.Class类,存储在堆区。 

* 因为静态变量存储在InstanceMirrorKlass中,所以静态变量也存储在堆区。

InstanceRefKlass: 表示java.lang.ref.Reference类的子类。

InstanceClassLoaeder: 用于遍历某个加载器加载的类。

Java中的数组不是静态数据类型(e.g. JVM内置的8种数据类型),是动态数据类型(i.e. 在运行期生成的)。

ArrayKlass: 存储数组类的元信息。

TyperArrayKlass: 表示基本类型的数组。

ObjArrayKlass: 表示引用类型的数组。

 

实验

证明java数组是动态数据类型

-> java main方法中new一个int/对象数组,编译运行

-> IDEA使用插件jclasslib查看字节码(idea->view->show bytecode with Jclasslib),main中显示newarray/anewarray,对应字节码手册中含义:“创建一个原始类型/引用型数组并将其引用至压入栈顶”。

查看java类对应的klass

-> HSDB -> Tools/Class Browser->找到目标类名即可找到对应内存地址

-> HSDB->Tools/Inspector->输入内存地址->可查看java类在内存中对应的klass类

or

-> while true维持程序运行,terminal输入jps –l获取当前运行进程id;

-> HSDB->file/attach to hotspot process->输入目标进程ID

-> 选中main线程,工具栏第二个按钮查看线程堆栈->可查看java对象底层内存地址

-> 复制内存地址->HSBD->tool/inspector->可查看java对象底层的实现类

HSDB attach后记得detach。

类加载的过程

* openjdk源码中ClassState记录class的状态(分配内存、加载、链接、正在初始化、完全初始化、初始化出错)。

/openjdk/hotspot/src/share/vm/oops/instanceKlass.hpp

enum ClassState {
    allocated,                          // allocated (but not yet linked)
     loaded,                             // loaded and inserted in class hierarchy (but not linked yet)
    linked,                             // successfully linked/verified (but not initialized yet)
    being_initialized,                  // currently running class initializer
    fully_initialized,                  // initialized (successfull final state)
    initialization_error                // error happened during initialization

};

加载 -- 可以随便使用任何语言实现类加载器,只要能够达到这三个效果。

 -> 通过类的全限定名获取存储该类的class文件(没有指明必须从哪获取)

 -> 解析成运行时数据(instanceKlass实例),存放在方法区;

 -> 在堆区生成该类的Class对象(instanceMirrorKlass实例)。

*方法区包含了ExtClassLoader区域和AppClassLoader区域,因为一般是AppClassLoader加载,类加载器解析时将得到的类的元信息存放在方法区的AppClassLoader区域。

JVM加载类是懒加载模式。-- 根加载器加载jar文件时并没有把其中所有的类都进行加载,而是只加载了一部分(预加载模式,只先加载常用的String,Thread,Integer等类)。

类加载的时机? --主动使用时

  1) new, getstatic, putstatic, invokestatic字节码 i.e. java代码中使用new关键字实例化对象、读取或设置类的静态字段(final修饰的常量除外)、调用类的静态方法

  2) 反射

  3) 初始化子类时会去加载其父类

  4) 启动类(main函数所在类)

  5) 当使用JDK1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

 

从哪加载? - 因为没有指明从哪获取class文件,可采用的思路:

1) 从压缩包中读取 e.g. jar, war

2) 从网络中获取 e.g. Web Applet

3) 动态生成 e.g. 动态代理、CGLIB

4) 由其他文件生成 e.g. JSP

5) 从数据库读取

6) 从加密文件读取

 

验证 – 检查klass文件是否符合规范,判断版本, jvm能否正常运行klass,…etc.

1. 文件格式验证

2. 元数据验证

3. 字节码验证

4. 符号引用验证

//参考《深入理解java虚拟机》

 

准备

为静态变量分配内存和赋初值。实例变量没有赋初值一说,而是在创建对象时完成赋值。

例外:如果被Final修饰,编译时会给添加ConstantValue属性,准备阶段直接完成赋值,没有赋初值步骤。

不同数据类型对应不同的初值:

解析 – 间接引用转为直接引用

*常量池:包括静态常量池(class文件常量池)、运行时常量池(可在HSDB中查看)和字符串常量池(StringTable)。

字符串存储在StringTable,在堆区。

间接引用a.k.a. 符号引用:指向运行时常量池的引用

直接引用:内存地址

1. 类或接口的解析

2. 字段解析

3. 方法解析

4. 接口方法解析

解析后的信息存储在ConstantPoolCache类实例中。

何时解析? -- 思路有:

1) 加载阶段解析常量池时

2) 用时 // openjdk采用,在执行特定字节码指令(e.g. anewarray, checkcast, getfield, …)前进行解析。

 

初始化 – 执行静态代码段,完成静态变量的赋值。

java代码中定义一个static属性(静态字段、静态代码段),编译时字节码层面就会自动生成clint方法(只有一个)

clint方法(i.e.字节码中生成的静态块)中语句顺序跟定义静态属性的java代码的编写顺序是保持一致的。

E.g. java代码:

public static int a=10;
public static int b=10;

字节码:

 

实验

证明final成员没有赋初值而是直接赋值

-> 演示类中声明成员static final int a=10, static int b=10;

-> 查看字节码,a中有属性ConstantValue,代表没有赋初值,准备阶段就完成赋值。(b在准备阶段被赋初值0)

查看常量池

-> idea terminal切换到classes目录下, javap –verbose 对象全限定名(全限定名可在ide中选中类名右键copy reference得),输出的Constant pool:部分表示静态常量池

e.g. 静态常量池中Class对应#25,翻阅下方对应当前类名字符串。’#25’即为一个符号引用(指向常量池的引用)。

-> 运行程序,HSDB attach到目标进程,class browser点击目标对象,下方可查看动态常量池

e.g. 动态常量池中Class不再指向常量池,而是指向内存地址@0x000….,即为一个直接引用。//解析后符号引用转为直接引用

初始化实验1

public class Test {
  public static void main(String[] args) {
      TestA obj=TestA.getInstance();
      System.out.println(TestA.val1);
      System.out.println(TestA.val2);
  }
}
class TestA {
  public static int val1;
  public static val2=1;
  public static TestA instance=new TestA();
  TestA() {
      val1++;
      val2++;
  }
  public static TestA getInstance() {
      return instance;
  }
}

  输出1  2

  原因:初始化时执行静态代码段,val1赋初值为0,val1赋值为1;执行构造函数后都+1;输出1 2.

初始化实验2 //将static val2定义移到构造方法后

public class Test {
  public static void main(String[] args) {
      TestA obj=TestA.getInstance();
      System.out.println(TestA.val1);
      System.out.println(TestA.val2);
  }
}
class TestA {
  public static int val1;
  public static TestA instance=new TestA();
  TestA() {
      val1++;
      val2++;
  }
  public static val2=1;
  public static TestA getInstance() {
      return instance;
  }
}

  输出:1  1

  原因:生成的静态块中语句顺序跟定义静态属性的java代码的编写顺序是保持一致的,所以val2经过构造函数后被定义语句覆盖回1;

           程序的执行顺序: 1) clint方法 2) 默认构造方法(执行完++后val1=1,val2=1);静态块(val2又被赋值为1)。

加载试验1 

public class Test {
  public static void main(String[] args) {
      System.out.printf(TestB.str);
  }
}
class TestA {
  public static String str=”A str”;
  static {
      System.out.println(“A Static Block”);
  }
}
class TestB extends TestA {
  static {
      System.out.println(“B Static Block”);
  }
}

  输出:Astatic Astr

  原因:A是B的父类,会被主动加载;B没有被使用,不会被加载

加载试验2 

public class Test {
  public static void main(String[] args) {
      System.out.printf(new TestB().str); //new了B对象
  }
}
class TestA {
  public String str=”A str”;//去掉static
  static {
      System.out.println(“A Static Block”);
  }
}
class TestB extends TestA {
  static {
        System.out.println(“B Static Block”);
  }
}

输出:Astatic Bstatic Astr

原因:AB都被使用,都会被加载(主动使用子类,就是间接在主动使用父类)

加载试验3 

public class Test {
  public static void main(String[] args) {
      System.out.printf(new TestB().str); 
  }
}
class TestA {
  static {
      System.out.println(“A Static Block”);
  }
}
class TestB extends TestA {
  public String str=”A str”;//str从A移到B
  static {
      System.out.println(“B Static Block”);
  }
}

输出:Astatic Bstatic Astr

原因:同上

加载试验4 

public class Test {
  public static void main(String[] args) {
      System.out.printf(testB.str); 
  }
}
class TestA {
  static {
      System.out.println(“A Static Block”);
  }
}
class TestB extends TestA {
  public static String str=”B str”; //static成员
  static {
      System.out.println(“B Static Block”);
  }
}

输出:Astatic Bstatic Bstr

原因:静态字段在子类里,子类会被加载

加载试验5 

public class Test {
  public static void main(String[] args) {
      System.out.println(TestA.str);
  }
}
class TestA {
  public static final String str=”A Str”;
  static {
      System.out.println(“A Static Block”);
  }
}

输出:AStr

原因:虽然AStr在A类里,但是是被final修饰的常量,此常量被写入到Test类的常量池中

加载试验6 

public class Test {
  public static void main(String[] args) {
      System.out.println(TestA.uuid);
  }
}
class TestA {
  public static final String uuid=UUID.randomUUID().toString();
  static {
      System.out.println(“A Static Block”);
  }
}

输出:Astatic uuid

原因: 虽然uuid是final修饰,但randomUUID().toString()是动态运行的,uuid需要动态生成,不能写入到Test的常量池。所以类A会被加载。

加载试验7

public class Test2 {
  static {
    System.out.println(“Test2 Static Block”);
  }
  public static void main(String[] args) throws ClassNotFoundException {
    Class<?> clazz=Class.forName(“com.xxxx.Test1”);
  }
}

输出:Test2Static Test1Static

原因:因为反射,类12都会被加载

加载试验8

public class Test {
  public static void main(String[] args) {
      System.out.printf(B.str);
  }
}
class A {
  public static String str=”str”;
  static {
      System.out.println(“A Static Block”);
  }
}
class B extends A {
  static {
      str+=”###”;
      System.out.println(“B Static Block”);
  }
}

输出:Astatic str

原因:JVM先判断是否加载,后面才会有初始化动作发生。案例中B的内部没有任何东西被使用,所以没有加载B,B的静态块不会被执行。

 

读取静态变量的底层实现  涉及InstanceKlass, instanceMirrorKlass, ConstantPoolCache

实验 证明静态属性存储在镜像类中

public class Test {
  public static void main(String[] args) {
      System.out.printf(B.str);
      while (true) {}
  }
}
class A {
  public static String str=”A str”;
  static {
      System.out.println(“A Static Block”);
  }
}
class B extends A {
  static {
      System.out.println(“B Static Block”);
  }
}

输出:Astatic Astr

-> 运行,查出进程ID,在HSDB中attach

-> HSDB classbrowser找到类A的内存地址,输入到inspector

-> 可以在inspector类A中找到静态属性str是存储在oop Klass: java.mirror中。说明jdk8静态属性是存储在镜像类(instanceMirrorKlass)中的。(而不是存储在instanceKlass, jdk6之前是)

-> 同样操作在inspector类B的oop Klass:java.mirror中并没有找到静态属性str,说明静态属性str只存放在父类A。

- 既然静态属性str存放在父类A中,main中调用B.str是怎么找到它的?

- 两种实现思路:

  1) 先从子类B的镜像类中取,如果有直接返回,没有则沿着继承链往上找;-- O(n)

  2) 借助另外的数据结构,使用K-V格式存储。-- O(1)。

  Hotspot采用的就是思路2,借助另外的数据结构ConstantPoolCache,常量池类ConstantPool有属性_cache指向该结构,每条数据对应一个类ConstantPoolCacheEntry。

* ConstantPoolCache: 用于存储某些字节码指令所需的解析(resolve)好的常量项,例如给[get|put]static, [get|put]field, invoke[static|special|virtual|interface|dynamic]等指令对应的常量池项用。

ConstantPoolCacheEntry的获取?

参考openjdkhotspotsrcsharevmoopcpCache.hpp通过ConstantPoolCache的地址加上偏移量

类加载器

JVM的类加载器包含两种类型:

1) 由C++编写的;-- 启动类加载器(Bootstrap Class Loader)

2) 由Java编写的 --其他继承java.lang.ClassLoader的类加载器

JVM也支持自定义类加载器。

 

各种类加载器之间逻辑上的父子关系不是真正的父子关系,没有直接从属关系。

 启动类加载器

启动类加载器:JVM将C++处理类的一套逻辑定义为启动类加载器。启动类加载器没有实体。

因为由C++编写,无法被Java程序调用,在Java程序中显示null。

启动类加载器的加载路径

URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
    System.out.println(urL);
}

openjdk源码

JavaMain中调用了LoadMainClass,启动类加载器就是在这时加载的。

Openjdk/jdk/src/share/bin/java.c / JavaMain()

JavaMain(void * _args) {
    …
    mainClass = LoadMainClass(env, mode, what);
    …
}

LoadMainClass中需要先找到Launcherhelper类。启动类加载器所做的事情就是加载类”sun/launcher/LauncherHelper”,checkAndLoadMain就是在LauncherHelper类里面。

Openjdk/jdk/src/share/java.c / GetLauncherHelperClass()

GetLauncherHelperClass(JNIEnv *env) {
    …
    NULL_CHECK0(helperClass = FindBootStrapClass(env,
            "sun/launcher/LauncherHelper"));
    …
}

GetLauncherHelperClass主要调用FindBootStrapClass。FindBootStrapClass中用GetProcessAddress调用JVM动态链接库的JVM_FindClassFromBootLoader方法。

openjdk/jdk/src/windows/bin/java_md.c / FindBootStrapClass()

jclass FindBootStrapClass(JNIEnv *env, const char *classname) {
    …
    findBootClass = (FindClassFromBootLoader_t *)GetProcAddress(hJvm,
                "JVM_FindClassFromBootLoader");
    …
} 

找到LauncherHelper类后,通过JNI执行LauncherHelper类的checkAndLoadMain方法。

用于加载main class(main方法所在的类),三种类加载器的父子链(->启动扩展类加载器->应用类加载器)也是在这次调用中完成的。

LoadMainClass返回的result其实就是调用checkAndLoadMain的结果 – main class。

Openjdk/jdk/src/share/bin/java.c / LoadMainClass()

LoadMainClass(JNIEnv *env, int mode, char *name) {
    …
    jclass cls = GetLauncherHelperClass(env);
    …
    NULL_CHECK0(mid = (*env)->GetStaticMethodID(env, cls,
                "checkAndLoadMain",
                "(ZILjava/lang/String;)Ljava/lang/Class;"));
    …
}

从checkAndLoadeMain开始都是java代码。checkAndLoadMain方法检查了运行模式,如果是class就直接命名,如果是jar包则从jar包中找,其他则报错。然后调用scloader.loadClass方法获得main class来返回。

openjdk/jdk/src/share/classes/sun/launcher/LauncherHelper.java / checkAndLoadMain()

public static Class<?> checkAndLoadMain(boolean printToStderr,
                                            int mode,
                                            String what) {
    …
        switch (mode) {
            case LM_CLASS:
                cn = what;
                break;
            case LM_JAR:
                cn = getMainClassFromJar(what);
                break;
            default:
                // should never happen
                throw new InternalError("" + mode + ": Unknown launch mode");
        }
    …
    mainClass = scloader.loadClass(cn);
    …
}

scloader是由ClassLoader.getSystemClassLoader()得到的,其中调用了initSystemClassLoader方法。

Openjdk/jdk/src/share/classes/java/lang /ClassLoader.java / getSystemClassLoader()

public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();
    …
}

initSystemClassLoader中调用了Launcher.getLauncher方法。

Openjdk/jdk/src/share/classes/java/lang /ClassLoader.java / initSystemClassLoader

private static synchronized void initSystemClassLoader() {
    …
    sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
    …
}

Launcher构造函数中初始化了ExtClassLoader,再以ext为参数初始化appClassLoader。Ext其实就是parent。

线程上下文类加载器contextClassLoader也是在这时赋值的。

openjdk/jdk/src/share/classes/sun/misc/Launcher.java

public class Launcher {
    …
    private static Launcher launcher = new Launcher();
    …
    public static Launcher getLauncher() {
        return launcher;
    }
    public Launcher() {
        …
        // Create the extension class loader
        extcl = ExtClassLoader.getExtClassLoader();
        …
        // Now create the class loader to use to launch the application
        loader = AppClassLoader.getAppClassLoader(extcl);
        …
       Thread.currentThread().setContextClassLoader(loader);
    }
    …
}

从getAppClassLoader()和其中调用的AppClassLoader构造函数可看出传入的extcl参数为parent。

openjdk/jdk/src/share/classes/sun/misc/Launcher.java /AppClassLoader

static class AppClassLoader extends URLClassLoader {
    public static ClassLoader getAppClassLoader(final ClassLoader extcl) throws IOException {
        …
        return new AppClassLoader(urls, extcl);
    }
    AppClassLoader(URL[] urls, ClassLoader parent) {…}
}

为什么Ext的parent是null?

从ExtClassLoader构造函数看出其super构造函数传入的就是null,而ExtClassLoader构造函数的super对应的形参就是parent。

openjdk/jdk/src/share/classes/sun/misc/Launcher.java /ExtClassLoader

static class ExtClassLoader extends URLClassLoader {
    public static ExtClassLoader getExtClassLoader() throws IOException {
        …
        return new ExtClassLoader(dirs);
    }
    public ExtClassLoader(File[] dirs) throws IOException {
        super(getExtURLs(dirs), null, factory);
        …
    }
    …
}

openjdk/jdk/share/classes/java/net/URLClassLoader.java

URLClassLoader(URL[] urls, ClassLoader parent, AccessControlContext acc) {…}

顺序:jvm的目的是要去加载main所在类->启动boot加载器->启动ext-加载器>启动app加载器->通过app加载器去加载main class

所以这不是继承上的父子关系,而是加载链逻辑上的父子关系。逻辑上的父子关系目的就是为了双亲委派。

 

 

扩展类加载器

扩展类加载器的加载路径是通过向System.getProperty()传入参数”java.ext.dirs”

String[] urls = System.getProperty(“java.ext.dirs”).split(“:”);
for (String url : urls)
    System.out.println(url);
ClassLoader classLoader = ClassLoader.getSystemClassLoader().getParent();

URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
URL[] urls = urlClassLoader.getURLs();
for (URL url : urls)
    System.out.println(url);

向System.getProperty()传入参数”java.ext.dirs”的做法也可以在openjdk源码中看到:

openjdk/jdk/src/share/classes/sun/misc/Launcher.java / ExtClassLoader

public ExtClassLoader(File[] dirs) throws IOException {
    super(getExtURLs(dirs), null, factory);
    …
}
private static File[] getExtDirs() {
    String s = System.getProperty("java.ext.dirs");
    …
}

不同的类加载器加载同一个类,相等吗?

不相等。方法区是按照类加载器进行分开存储的。每个类加载器在方法区里都有一块独立的区域,虽然加载的是同一份文件,但是不会在同一个空间里。

同一个类加载器加载同一个文件多次,实际上会加载几次?

一次。因为加载前会(根据全限定名)去判断空间里是否已经有这个类。

 

 

 

双亲委派

双亲委派:需要查找某个类时,先判断在当前类加载器是否已经加载(能在其空间中找到),如果已经加载则直接返回,没有则向上委托给其父类加载器。

*系统已加载的class信息存储在SystemDictionary类中。

e.g. 查找某个类

-> 判断当前最下层的自定义的类加载器是否已加载该类,是则直接返回,否则往上委托给父类AppClassLoader;

-> 判断在AppClassLoader中是否已经加载,是则直接返回,否则再往上委托给父类ExtClassLoader;

-> … 委托给BootstrapClassLoader…

-> 如果BootstrapClassLoader也没有加载直接报错

局限性:无法做到不委派或向下委派

e.g. 数据库需要实现的driver接口是由启动类加载器加载。而第三方数据(如mysql)的相关实现类需要由应用类加载器加载,启动类加载器不能加载,需要向下委派。

 

什么叫打破双亲委派?

两种思路。a) 不委派 –只用当前类加载器去加载 –实现方式:自定义类加载器

         或 b) 向下委派 (SPI机制中的一部分)

SPI: 一种服务发现机制,通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类

e.g. SPI Demo(该案例不算打破双亲委派,因为所有类都是启动类加载器加载的)

PayService是自定义的一个接口,有两个实现AlipayService和WxpayService。

Public interface PayService {
    void pay();
}

main中使用线程上下文加载器加载PayService类。

public static void main(String[] args) {
    ServiceLoader<PayService> services=ServiceLoader.load(PayService.class);
    for (PayService service : services)
        service.pay();
}

通过接口调用的pay方法调用的是哪一个实现类是看pom.xml/<dependencies>/<artifactId>中指定加载的是哪一个模块。

Pom.xml (gateway-main)

<dependencies>
    <dependency><artifactId>pay-wx</artifactId></dependency></dependencies>

每个实现类底下的/src/main/resources/META-INF.services/目录下的文件中指定了实现类的全限定名。SPI从而找到需要加载的实现类。

/pay-wx/src/main/resources/META-INF.services/com.luban.common.service.PayService

    com.luban.pay.AlipayService

实现向下委派?

需要使用ServiceLoader。

e.g. Driver中的SPI机制。JDBI的底层实现用到serviceloader,也是一种SPI。driver通过向下委派来打破双亲委派。

ServiceLoader<Driver> loadedDrivers=ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator=loadedDrivers.iterator();

线程上下文类加载器

ServiceLoader的底层是由线程上下文类加载器ContextClassLoader实现的,在SPI中向下委派中有应用。

ContextClassLoader可通过Thread.currentThread().setContextClassLoader()进行设置。

在checkAndLoadMain时已经进行设置,默认为AppClassLoader。

openjdk/jdk/src/share/classes/sun/misc/Launcher.java

public Launcher() {
    …
    // Now create the class loader to use to launch the application
    loader = AppClassLoader.getAppClassLoader(extcl);
    …
    Thread.currentThread().setContextClassLoader(loader);
}

自定义类加载器

如何实现自定义类加载器?

extends ClassLoader类,重写findClass方法。

实验1:自定义加载器重写findClass时返回null,能否加载成功

public class ClassLoader1 extends ClassLoader {
  public void main(String[] args) throws Exception {
      Classloader1 classloader1=new Classloader1();
      Class<?> clazz=classloader1.loadClass(“com.experiment.classloader.A”);
      System.out.println(“clazz hashcode: “+clazz.hashCode());
  }
  @Override
  protected Class<?> findClass(String className) throws ClassNotFoundException {
      return null;
  }
}

结果:可以加载

原因:双亲委派。可通过 System.out.println(clazz1.getClassLoader())打印出sun.misc.Launcher$AppClassLoader证明clazz1是由AppClassLoader加载的。

实验2

public class ClassLoader1 extends ClassLoader {
  public void main(String[] args) throws Exception {
      Classloader1 classloader1=new Classloader1(), classloader2=new Classloader1();
      Class<?> clazz1=classloader1.loadClass(“com.experiment.classloader.A”);
      Class<?> clazz2=classloader2..loadClass(“com.experiment.classloader.A”);
      System.out.println(clazz1==clazz2)
  }
  @Override
  protected Class<?> findClass(String className) throws ClassNotFoundException {…}
}

结果:true

原因:用同一个classLoader类加载同一个类,得到的Class是同一个类。可通过System.out.println(clazz1.hashCode())证明clazz1和clazz2的hashCode是一样的。

 

自定义类加载器如何打破双亲委派

classLoadder源码中:

Class(String)底层了调用loadClass(String, boolean)

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return this.loadClass(name, false);
}

loadClass(String, Boolean)中的双亲委派逻辑 

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { //name为全限定名
    …
    Class<?> c = this.findLoadedClass(name); //在自己空间中搜索该类
    if (c == null) { 
        try {
            if (this.parent! = null)
                c = this.parent.loadClass(name, false); //如果空间中没有,双亲委派
            else
                c = this.findBootstrapClassOrNull(name);
        } catch (ClassNotFoundException) {}    
    }
    if (c == null)
        c = this.findClass(name); //如果双亲委派仍未加载到,调用自己的findClass加载一次
    //此后再不能加载到将直接报错
    if (resolve)//判断有无解析,进行解析
        this.resolveClass(c);
    return c;
}

可通过重写loadClass(String, Boolean)打破双亲委派: 对指定包下的类的加载做不委派处理

@Override
protected Class<?> loadClass(String name, Boolean resolve) throws ClassNotFoundException {
    …
    if (c == null) {
        if (name.startsWith(“com.experiment”))
            c = findClass(name);
        else
            c = this.getParent().loadClass(name);
    }
}

沙箱安全


checkAndLoadMain底层有调用到initSystemClassLoader。initClassLoader中所做的AccessController.doPrivileged判断就是一种沙箱安全机制。

Openjdk/jdk/src/share/classes/java/lang /ClassLoader.java / initSystemClassLoader

private static synchronized void initSystemClassLoader() {
    …
    scl = AccessController.doPrivileged(
                        new SystemClassLoaderAction(scl));
    …
}

实验: 沙箱安全

在自创java.lang包下定义String类,在main中调用String的方法会报错。

public class String {
  public static void main(String[] args) {
      String.show();
  }
  public static void show() {
      System.out.println(“String show function”);
  }
}

结果:报错“在类中找不到main方法”

原因:沙箱安全防止打破双亲委派修改系统类,保护核心类库。 

原文地址:https://www.cnblogs.com/RDaneelOlivaw/p/13494532.html