JVM的类加载是通过ClassLoader及其子类来完成的。
类加载器如下:
JAVA_HOME\lib
目录或通过-Xbootclasspath
参数指定路径中的且被虚拟机认可(rt.jar
)的类库;JAVA_HOME\lib\ext
目录或通过java.ext.dirs
系统变量指定路径中的类库;classpath
上的类库;类加载器执行顺序如下图:
自底向上检查类是否已经加载:
加载过程中会先检查类是否已被加载,从自定义加载器到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,需要先将主类加载。
一个类的一生如下:
主要做了三件事:
类加载途径如下图:
我们可以自定义类加载器,来加载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技术。