一、前言
首先JVM会首先加载BootstrapClassLoader,它是由C++编写的类加载器,并且加载<JAVA_HOME>/lib 下的代码。然后会创建sun.misc.Launcher类, 这个类是java的入口,会创建初始化ExtClassLoader,AppClassLoader和
并对应的需要加载的类加入Stack<URL>这个成员变量中(并不加载类,只是先放到本地内存中)。
这个三个类加载对应加载的文件
BootstrapClassLoader: $JAVA_HOME/lib/*.jar System.getProperty("sun.boot.class.path")
ExtClassLoader: $JAVA_HOME/lib/ext/*.jar,System.getProperty("java.ext.dirs")下的文件
AppClassLoader: classpath下的文件,System.getProperty("java.class.path")下的文件
二、Launcher类介绍
sun.misc.Launcher类是java的入口,在启动java应用的时候会首先创建Launcher类,创建Launcher类的时候回准备应用程序运行中需要的类加载器。 其中包括AppClassLoader和
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// 将AppClassLoader 放入当前线程当中(SPI的实现用到了)
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
;
} catch (InstantiationException var6) {
;
} catch (ClassNotFoundException var7) {
;
} catch (ClassCastException var8) {
;
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
三、双亲委派
定义
第一次听到双亲委派感觉很高大上,容易被吓住,其实双亲委派很简单,所谓双亲委派用通俗的话来讲就是类加载器加载类的时候会先交给父类去加载,如果父加载器找不到才会交给子加载器去加载。
BootstrapClassLoader <==> ExtClassLoader <==> AppClassLoader
具体怎么玩的就不讲的,因为比较简单, 打个比方AppClassLoader 加载Class A , 会丢给 ExtClassLoader, 然后ExtClassLoader 丢给 BootstrapClassLoader,BootstrapClassLoader找到则返回,找不到就找ExtClassLoader,然后如果ExtClassLoader找不到 就继续丢该AppClassLoader
双亲委派实现代码
双亲委派实现代码很简单,就是一个递归,可以思考一下是怎么实现的。
见ClassLoader.loadClass(String name, boolean resolve)的代码, ClassLoader是一个抽象类
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();
try {
// 双亲委派核心就在这里
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 这个是BootstrapClassLoader, 因为是这个是BootstrapClassLoaderC++写的
,代码里也不好体现ExtClassLoader的父类这个是BootstrapClassLoader,所以做单独 处理。
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 父类找不到,才会找子类。
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
问题
1.类加载器在什么情况下会加载类
1)使用new关键字实例化对象
2)访问类的静态变量,包括读取一个类的静态字段 和 设置一个类的静态字段 (除常量【 被final修辞的静态变量】 原因:常量一种特殊的变量,因为编译器把他们当作值(value)而不是域(field)来对待。如果你的代码中用到了常变量(constant variable),编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种优化机制,叫编译器编译优化,对这个机制不理解可以
3)访问类的静态方法
4)反射 如( Class.forName(“my.xyz.Test”) )
5)当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化
6)虚拟机启动时,定义了main()方法的那个类先初始化
有兴趣的可以根据上诉几种情况打个断点试一下。
2、JVM在搜索类的时候,又是如何判定两个class是相同的呢?
JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同的class。
好处
1. 可以保证同一个类是有同一个类加载器记载,可以防止核心API被任意篡改。
局限性
1.父加载器加载的类拿不到子加载器对应的类。
2.加载的路径有限,如果需要自定义路径加载类的话,那么就需要自定义类加载器了,典型的有Tomcat,Spring。
四、破坏双亲委派
什么是破坏双亲委派委派呢,名词很高大上,其实也很简单,就是在某些情况不需要遵循双亲委派规则或者双亲委派做不到。
示例有:
SPI的实现机制:原理是从当前线程拿到ClassLoader (Thread.currentThread().getContextClassLoader();)
Tomcat的classloader:(catalinaLoader,sharedLoader, commonLoader为同一个URLClassLoader,而且该URLClassLoader的Parent ClassLoader正是sun.misc.Launcher$AppClassLoader。 ),通过自定义ClassLoader实现可以在除了jdk定制的路径下加载类。
1.JDBC的DriverManager 类, 由于DriverManager 是rt.jar里面的包,也就是由BootstrapClassLoader加载的, 所以DriverManager通过当前类所在ClassLoader是拿不到AppClassLoader加载的类的。
不破坏双亲委派模型的情况(JDBC4.0以前)
// 1.加载数据访问驱动
Class.forName("com.mysql.jdbc.Driver");
//2.连接到数据"库"上去
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
破坏双亲委派模型的情况(在JDBC4.0以后 )
在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个,然后使用的时候就直接这样就可以了:
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
//省略代码
//这里就是查找各个sql厂商在自己的jar包中通过spi注册的驱动
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
//省略代码
}
}
SPI是如何破坏双亲委派的
那么JDBC4.0以后是怎么干掉Class.forName的呢,其实就是用到了SPI,而SPI也正是破坏了双亲委派模型的方式,那么他是怎么做到的呢?见ServiceLoader.load(Driver.class)的代码
public static <S> ServiceLoader<S> load(Class<S> service) {
// 这段代码正是上面Launcher初始化 setContextClassLoader的代码
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader){
return new ServiceLoader<>(service, loader);
}
从代码中可以发现在当前线程中拿到了AppClassLoader, 于是便可以加载到AppClassLoader加载的类了,方法很简单。