类加载器的创建时机
首先,当我们运行一个类时,会先获取这个类的类加载器,通过类加载器把类加载到JVM中。
Java虚拟机使用c++语言实现的,启动Java虚拟机,会创建一个引导类加载器Bootstrap ClassLoader,引导列加载器的创建也是c++语言实现的,所以获取它的时候总是打印null。
引导类加载器会通过Launcher.getLauncher()创建sun.misc.Launcher实例,这一步操作是c++代码调用Java代码,而Launcher是JVM的启动器,它的创建采用单例模式,保证了JVM只一份Launcher实例。
在Launcher实例的创建过程中会创建ExtClassLoader和AppLClassLoader的实例,这两个类加载器都继承自URLClassLoader类,除了引导类加载器没有父加载器外,其他的类加载器的根类都是ClassLoader类。
在ExtClassLoader和AppClassLoader的构造过程中,会调用父类URLClassLoader的构造器,会有一个URL[],里面存放的是类加载自己所要加载的文件的路径;还有一个参数是parent,ClassLoader类型,当构造AppClassLpader的时候会把ExtClassLoader的实例作为parent传入,然后在URLClassLoader的构造方法中会调用根类的一个构造方法,把parent设为AppClassLoader的父加载器。
父加载器并非父类,它们之间没有继承关系,只是通过parent这个成员变量设置了上下级关系。
JVM会默认使用Lancher的getClassLoader()方法返回AppClassLoader的实例来加载我们的应用程序。
类加载器分类
1、引导类加载器(启动类加载器 Bootstrap ClassLoader):用来加载JRE目录下的lib目录下的核心类库
2、扩展类加载器(Extension ClassLoader): 用来加载JRE目录下lib目录下的ext扩展目录下的JAR类包
3、应用程序类加载器(AppClassLoader):用来加载classpath的类包,该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载。
4、自定义加载器:定制类的加载方式。自定义的类加载器默认设置AppClassLoader为父加载器 ,初始化的时候会先初始化父类加载器。
public class MyTest {
public static void main(String[] args) {
ClassLoader bootstrapLoader = String.class.getClassLoader();
ClassLoader appClassLoader = MyTest.class.getClassLoader();
ClassLoader extClassLoader = ZipInfo.class.getClassLoader();
System.out.println("bootstrapLoader" + bootstrapLoader);
System.out.println("appClassLoader" + appClassLoader);
System.out.println("extClassLoader" +extClassLoader);
System.out.println("bootstrapLoader加载以下文件:");
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
System.out.println(urL);
}
System.out.println("appClassLoader加载以下文件:");
String property = System.getProperty("java.class.path");
System.out.println(property);
System.out.println("extClassLoader加载以下文件:");
String property1 = System.getProperty("java.ext.dirs");
System.out.println(property1);
}
}
运行结果:
ps:引导类加载器打印出来为null,因为它是用c++语言实现的对象,而非java
loadClass()
类加载过程,以及双亲委派机制的实现,都是通过ClassLoader的loadClass()方法实现,通过接受一个二进制文件名来加载这个Class。
类加载流程
加载、验证、准备、解析、初始化
一、加载:
在硬盘上查找并通过IO读取字节码文件,使用到类时才会加载类,在加载阶段会在堆中生成代表这个类的java.lang.Class对象,作为这个类的所有数据访问入口。在加载阶段,可通过系统提供的类加载器来完成加载,也可以自定义类加载器。
二、验证:
校验字节码文件是否符合Java虚拟机规范。
javap -v As.class 查反编译As的class文件。
三、准备:
给静态变量分配内存,并赋默认值(int->0,String->null),final修饰的变量是常量,会直接赋值。
四、解析:
将符号引用替换为直接引用,这是静态链接的过程。
符号引用:public,main,String等这些都可以称为符号,可以是任何字面量。
直接引用:指数据加载到内存区具备一个内存地址,根据地址可定位到目标的指针或句柄。
动态链接在类加载的时候不会链接,即代码不对被解析为内存地址,运行到它的时候才链接。
五、初始化:
对静态变量赋正确初始值。两种方式:声明类变量指定初始值、执行静态代码块为类变量赋值。
类初始化的步骤:
1、假如这个类还没有被加载和链接,程序先加载并链接该类;
2、假如该类的直接父类还没被初始化,先初始化其直接父类;
3、假如类中有初始化语句,则依次执行这些初始化语句。
类初始化时机:
只有当对类的主动使用才会导致类的初始化,类的主动使用包括六种:
1、创建类实例,new;
2、访问某个类的静态变量,或者对静态变量赋值;
3、调用类的静态方法;
4、反射(例如Class.forName(…))
5、初始化某个类的紫烈,则父类也会被初始化;
6、Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类。
双亲委派机制
在loadClass()方法中,调用findLoadedClass(String)检查当前类加载器是否已经加载过该类(只是判断它是否已经存在于被加载过的类中,自己并不会去加载它),已经加载过会直接返回。
未被加载过,如果parent(父类加载器)不为空,委派parent调用loadClass方法重新加载,
如果parent为空,委派引导类加载器bootstrap classloader来加载,
如果得到的class对象还是null,代表应用类加载器,扩展类加载器,引导类加载器都没有加载过该类,
此时会调用findClass(name):bootstrapClassLoader尝试自己去加载,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
ps:ClassLoader中的findClass(name)方法是一个空方法,URLClassLoader对它进行了重写,所以这里调用来源于URLClassLoader。
图解双亲委派:
为什么用双亲委派机制,好处?
1、沙箱安全机制:防止jdk核心类库被随意篡改。
2、避免类的重复加载:当父类已经加载了该类,就没必要子类加载器再加载一次,保证了加载类的唯一性。
全盘负责委托+缓存机制
全盘负责委托机制:
类加载器加载类用的是全盘负责委托机制,即当一个类加载器加载一个类的时候,这个类所依赖和引用的类通常也由这个类加载器负责加载。
缓存机制:
为什么修改了class必须重启虚拟机才生效?为什么class只加载一次?
类加载使用了缓存机制,如果缓存中保存了这个class文件就直接返回,没有的话就去读取和载入class,并存入缓存。
自定义类加载器
编写自定义类加载器,继承java.lang.ClassLoader类,通过重写loadClass(String,boolean)打破双亲委派,通过重写findClass实现类如何被载入到JVM。ClassLoader类的findClass是个空方法,它的子类URLClassLoader对它进行了重写,来实现类被加载的操作,现在要自定义类加载器,因此需要重写它。
不打破双亲委派
不打破双亲委派,只重写findClass方法,自定义加载规则。
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
/**
* 自定义类加载器
* 重写findClass(String)实现加载过程(进行类加载)
*/
public class MyClassLoader extends ClassLoader{
private String classPath;
public MyClassLoader(String classPath){
this.classPath = classPath;
}
//获取class的字节数组
private byte[] loadByte(String name) throws IOException {
name = name.replaceAll("\\.","/");//把.替换为/
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();//返回可以从此输入流中读取(或跳过)而不会被此输入流的方法的下一次调用阻塞的剩余字节数的估计
byte[] data = new byte[len];
fis.read(data);//将此输入流中最多data.length字节的数据读取到字节数组中
fis.close();
return data;
}
//重写此方法。
//实现类的加载(进行类加载的操作)
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
public static void main(String[] args) throws Exception{
MyClassLoader myClassLoader = new MyClassLoader("D:\\test");
//调用的是父类的loadClass方法,即未打破双亲委派机制
//loadClass方法内部会去调用findClass方法,这里对findClass进行了重写,因为会执行这里的findClass方法
Class aClass = myClassLoader.loadClass("com.xwl.User1", false);
Object obj = aClass.newInstance();
Method method = aClass.getDeclaredMethod("say",null);
method.invoke(obj,null);
System.out.println("查看类加载器是谁:");
System.out.println(aClass.getClassLoader());//MyClassLoader@49476842
}
}
ps:打印com.xwl.User1的类加载器,结果是自定义的类加载器,但是如果User1类在类路径下存在的话,再打印类加载就是AppClassLoader了。因为我们并没有打破双亲委派机制,所以父加载器已经加载过的类会直接返回。
打破双亲委派
打破双亲委派机制,重写loadClass(String,boolean)和findClass方法。
import sun.misc.PerfCounter;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
/**
* 自定义类加载器
* 重写loadClass(String,boolean)实现/打破双亲委派机制(进行委托)
* 重写findClass(String)实现加载过程(进行加载)
*/
public class MyClassLoader1 extends ClassLoader{
private String classPath;
public MyClassLoader1(String classPath){
this.classPath = classPath;
}
//获取class的字节数组
private byte[] loadByte(String name) throws IOException {
name = name.replaceAll("\\.","/");//把.替换为/
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();//返回可以从此输入流中读取(或跳过)而不会被此输入流的方法的下一次调用阻塞的剩余字节数的估计
byte[] data = new byte[len];
fis.read(data);//将此输入流中最多data.length字节的数据读取到字节数组中
fis.close();
return data;
}
//重写loadClass方法,打破双亲委派(进行委托的操作)
//可以把ClassLoader的loadClass方法复制过来,对双亲委派那部分的逻辑进行修改
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 查看当前类加载器(自定义类加载器)已经加载过的类中是否有此类
Class<?> c = findLoadedClass(name);
//没有加载过
if (c == null) {
long t0 = System.nanoTime();
long t1 = System.nanoTime();
//由当前类加载器(自定义类加载器)加载此类
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
//return super.loadClass(name, resolve);
}
//重写了此方法。
//实现类的加载(进行类加载的操作)
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
public static void main(String[] args) throws Exception{
MyClassLoader1 myClassLoader = new MyClassLoader1("D:\\test");
Class aClass = myClassLoader.loadClass("com.xwl.User1", false);
Object obj = aClass.newInstance();
Method method = aClass.getDeclaredMethod("say",null);
method.invoke(obj,null);
System.out.println("查看类加载器是谁:");
System.out.println(aClass.getClassLoader());
}
}
运行报错:java.io.FileNotFoundException: D:\test\java\lang\Object.class (系统找不到指定的路径。)
这是因为自己定义的类依赖于Object类,现在我们加载类没有双亲委派,所以自定义类加载器在自己的加载路径下找不到Object.class就会报错。把Object.class放进去:
再次运行依旧报错:Exception in thread “main” java.lang.SecurityException: Prohibited package name: java.lang
这是因为jdk核心类库的API,只能由系统的类加载器来加载,不允许随便定义加载器加载。
改善loadClass方法:如果是自己定义的类,不走双亲委派,如果是jdk自己定义的类,走双亲委派。
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
//如果是自己定义的类,不走双亲委派
if(name.startsWith("com.xwl")){
c = findClass(name);
}else{
//如果是jdk自己定义的类,走双亲委派
c = this.getParent().loadClass(name);
}
long t1 = System.nanoTime();
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
//return super.loadClass(name, resolve);
}
运行结果:
tomcat打破了双亲委派
tomcat类加载器机制:
Tomcat 为实现隔离性,每个 webappClassLoader 加载自己目录下的 class 文件,而不会传递给父类加载器,打破了双亲委派机制。
tomcat类加载流程:
1、先从缓存中加载,如果存在则直接返回该类。
2、委派jvm中的父类ExtClassLoader、BootStrapClassLoader,查看缓存中是否加载过该类。
3、从当前类加载器加载(WebAppClassLoader自己加载)顺序为/WEB-INF/classes, /WEB-INF/lib/.jar
4、还没有,则从父类加载器加载,依次 App、Common、shared 加载(父类默认使用双亲委派)
5、如果仍然没有加载成功,则抛出异常。
tomcat自定义类加载器:
CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader都是Tomcat自己定义的类加载器。
WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。
commonLoader:Tomcat最基本的类加载器,加载的class可以被Tomcat容器本身以及各个Webapp(web应用)访问;
catalinaLoader:Tomcat容器私有的类加载器,加载的class对于Webapp不可见;
sharedLoader:各个Webapp共享的类加载器,加载的class对于所有Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader:各个Webapp私有的类加载器,加载的class只对当前Webapp可见;
CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。
WebAppClassLoader可以使用SharedClassLoader加载到的类,但**各个***WebAppClassLoader实例之间相互隔离**。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了实现JSP的HotSwap功能。
很显然,Tomcat为了实现隔离性,打破了双亲委派,每个webappClassLoader加载自己的目录下的class文件。
Tomcat如果使用默认的双亲委派类加载机制行不行?
不行。
1.如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,双亲委派机制是不管你是什么版本的,只在乎你的全限定类名,并且只加载一份。Tomcat是个web容器,一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
2.部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。
3.web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来,web容器也有自己依赖的类库使用默认的类加载机制,保证唯一性。
4.web容器要支持jsp的修改,jsp文件要编译成class文件才能在虚拟机中运行,每个jsp对应的servlet都有一个自己的类加载器。程序运行后再修改jsp,类加载器也还是取方法区中已经存在的,不会重新加载。
jsp的热加载:web容器检测到到jsp文件发生了修改–>将它的类加载器JasperLoader置空,重新引入新的类加载器重新加载。
模拟tomcat同一个web容器下有两个web应用程序,test和test1,加载不同版本的同一个类(全限定名都一样)。
将User1类编译后把class文件放test目录下,修改User1中say()方法代码,重新编译后再把class文件放test1下。
以此来模拟版本的不同。
(利用了前面的代码)
public static void main(String[] args) throws Exception{
MyClassLoader1 myClassLoader = new MyClassLoader1("D:\\test");
Class aClass = myClassLoader.loadClass("com.xwl.User1", false);
Object obj = aClass.newInstance();
Method method = aClass.getDeclaredMethod("say",null);
method.invoke(obj,null);
System.out.println("查看类加载器是谁:");
System.out.println(aClass.getClassLoader());
System.out.println("==========================================");
MyClassLoader1 myClassLoader1 = new MyClassLoader1("D:\\test1");
Class aClass1 = myClassLoader1.loadClass("com.xwl.User1", false);
Object obj1 = aClass1.newInstance();
Method method1 = aClass1.getDeclaredMethod("say",null);
method1.invoke(obj1,null);
System.out.println("查看类加载器是谁:");
System.out.println(aClass1.getClassLoader());
}
运行结果: