JVM—类加载子系统
JVM的类加载是通过ClassLoader及其子类来完成的。
有哪些类加载器
类加载器如下:
- 启动类加载器(BootStrap ClassLoader):负责加载
JAVA_HOME\lib
目录或通过-Xbootclasspath
参数指定路径中的且被虚拟机认可(rt.jar
)的类库; - 扩展类加载器(Extension ClassLoader):负责加载
JAVA_HOME\lib\ext
目录或通过java.ext.dirs
系统变量指定路径中的类库; - 应用程序类加载器(Application ClassLoader):负责加载用户路径
classpath
上的类库; - 自定义类加载器(Custom ClassLoader):加载应用之外的类文件;
类加载器执行顺序
类加载器执行顺序如下图:
-
自底向上检查类是否已经加载:
加载过程中会先检查类是否已被加载,从自定义加载器到BootStrap逐层检查,只要某个类加载器已加载某个类,就视为此类已加载,可以保证此类使得所有ClassLoader只加载一次;
-
自顶向下尝试加载类:由上层来逐层尝试加载此类。
类加载时机与过程
类加载的四个时机:
-
遇到new、getStatic、putStatic、invokeStatic四条指令;
比如有如下类:
public class MyTest { public static int hello; public static void testMethod(){ } }
当使用如下三种代码时,此类会被加载:
//第一种 MyTest.age; //第二种 MyTest.testMethod(); //第一种 new MyTest();
-
使用java.lang.reflect包方法对类进行反射调用;
比如:
Class clazz = Class.forName("com.sjdwz.MyTest");
-
初始化一个类,发现其父类还没初始化,要先初始化其父类;
-
当虚拟机启动时,用户需要指定一个主类main,需要先将主类加载。
一个类的一生
一个类的一生如下:
类加载做了什么
主要做了三件事:
- 根据类全限定名称,定位到class文件,以二进制字节流形式加载到内存中;
- 把字节流静态数据加载到方法区(永久代,元空间);
- 基于字节流静态数据,创建字节码Class对象。
类加载途径
类加载途径如下图:
自定义类加载器
我们可以自定义类加载器,来加载D:\sjdwzTest
目录下的lib文件夹下的类。
步骤如下:
-
新建一个类MyTest.java
package com.sjdwz.myclassloader; public class MyTest { public void sayHello(){ System.out.println("hello world!"); } }
-
使用
javac MyTest.java
命令,将生成的MyTest.class
文件放到D:\sjdwzTest\lib\com\sjdwz\myclassloader
文件夹下注意:包路径不能错。
-
自定义类加载器,继承
ClassLoader
,重写findClass()方法 ,调用defineClass()方法:/** * @Description 自定义类加载器 * @Created by 随机的未知 */ public class SjdwzClassLoader extends ClassLoader { private String classpath; public SjdwzClassLoader(String classpath) { this.classpath = classpath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { //输入流,通过类的全限定名称加载文件到字节数组 byte[] classDate = getData(name); if (classDate != null) { //defineClass方法将字节数组数据 转为 字节码对象 return defineClass(name, classDate, 0, classDate.length); } } catch (IOException e) { e.printStackTrace(); } return super.findClass(name); } /** * 加载类的字节码数据 * @param className * @return * @throws IOException */ private byte[] getData(String className) throws IOException{ String path = classpath + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; try (InputStream in = new FileInputStream(path); ByteArrayOutputStream out = new ByteArrayOutputStream()) { byte[] buffer = new byte[2048]; int len = 0; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } return out.toByteArray(); } catch (FileNotFoundException e) { e.printStackTrace(); } return null; } }
-
测试类如下:
public class SjdwzClassLoaderTest { public static void main(String[] args) throws Exception { //自定义类加载器的记载路径 SjdwzClassLoader sjdwzClassLoader = new SjdwzClassLoader("D:\\sjdwzTest\\lib"); Class<?> testClazz = sjdwzClassLoader.loadClass("com.sjdwz.myclassloader.MyTest"); if(testClazz != null){ Object testObj = testClazz.newInstance(); Method sayHelloMethod = testClazz.getMethod("sayHello", null); sayHelloMethod.invoke(testObj,null); System.out.println(testClazz.getClassLoader().toString()); } } }
输出如下:
双亲委派与打破双亲委派
什么是双亲委派
当一个类加载器收到类加载任务,会先交给其父类加载器去完成。 因此,最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,子类才会尝试加载任务。
为什么需要双亲委派
主要考虑安全因素,双亲委派可以避免重复加载核心的类,当父类加载器已经加载了该类时,子类加载器不会再去加载。
为什么还需要破坏双亲委派
在实际应用中,双亲委派解决了Java基础类统一加载的问题,但是存在着缺陷。JDK中的基础类的方法作为典型的API被用户类用户调用,但是也存在API调用用户代码的情况,比如:SPI代码。这种情况就需要打破双亲委派模式。
比如:数据库驱动DriverManager。以Driver接口为例,Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,由系统类加载器加载。这个时候就需要启动类加载器来委托子类来加载Driver实现,这就破坏了双亲委派。
如何破坏双亲委派
-
重写ClassLoader的loadClass方法;
在JDK1.2之后,新加了一个findClass方法让用户重写;
-
SPI,父类委托子类加载器加载Class;
-
热部署和不停机更新用到的OSGI技术。