类加载器加载过程的锁
类加载器在加载类的时候会一直持有这个类的锁
/**
* 死锁原因
* 1、线程1:类A对class A加锁,加锁后,执行类的静态初始化块(在堆栈里体现为<clinit>函数),发现用到了class B,于是去加载B;
*
* 2、线程2:类B对class B加锁,加锁后,执行类的静态初始化块(在堆栈里体现为<clinit>函数),发现用到了class A,于是去加载A;
*
* 3、死锁发生。*/
public class TestClassLoading {
public static class A{
static {
System.out.println("class A init");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
B.test();
}
public static void test() {
System.out.println("aaa");
}
}
public static class B{
static {
System.out.println("class B init");
A.test();
}
public static void test() {
System.out.println("bbb");
}
}
public static void main(String[] args) {
new Thread(() -> A.test()).start();
new Thread(() -> B.test()).start();
}
}
类与接口的初始化情况
当类中的静态变量也被final修饰时,编译后就会被存入到调用这个常量的方法所在类的常量池中,使用该变量,既不会使类初始化,也不会使类加载,若无final修饰,使用该变量,类就会初始化,且该类的父类(如果未初始化)也会初始化,但是该类实现的接口不会初始化,但是会被加载,
static加入final且等号右边是个定值,该变量就是字面值常量,才会放入常量池,否则如果使用random这种在运行期才能确定的变量,一样会导致初始化
命名空间
自定义类加载器
package VM.类加载器;
import java.io.*;
/**
自定义的类加载器的父加载器都是ApplicationLoader,
当加载类的时候都先看该类加载器是否加载过该类,若没有则向父类询问是否加载过,一直到顶层类,
再从顶层类开始看谁能加载,谁就加载(相当于一个递归过程),
自定义的类加载器,访问的路径不在系统默认路径所以父类加载器无法加载到,
这样就会由该自定义的类加载器加载,而第二个自定义类加载实例加载时发现父类也没有加载,
所以也是自己加载,所以就加载了两个不同的类
*/
public class MyClassLoader extends ClassLoader {
private String classLoaderName;
private String path;
private final String fileExtension=".class";
public MyClassLoader(String classLoaderName){
super(); //将系统类当做该类的父加载器
this.classLoaderName=classLoaderName;
}
public MyClassLoader(ClassLoader parent,String classLoaderName){
super(parent); //显式指定该类的父加载器
this.classLoaderName=classLoaderName;
}
public MyClassLoader(ClassLoader parent){
super(parent); //显式指定该类的父加载器
}
public void setPath(String path){
this.path=path;
}
/**
* 会由类加载器的loadClass加载
* */
@Override
protected Class<?> findClass(String className){
String name=className;
System.out.println("calssName="+className);
className=className.replace(".", File.separator);
byte[] data= new byte[0];
try {
data = loadClassData(className);
} catch (IOException e) {
e.printStackTrace();
}
return defineClass(name,data,0,data.length); //define方法为父类方法
}
private byte[] loadClassData(String name) throws IOException {
byte[] data=null;
name=name.replace(".","/");
try(InputStream is=new FileInputStream(new File(this.path+name+this.fileExtension));ByteArrayOutputStream baos=new ByteArrayOutputStream()){
int ch;
while(-1!=(ch=is.read())){
baos.write(ch);
}
data=baos.toByteArray();
}catch(Exception e) {
}
return data;
}
@Override
public String toString() {
return "this ["+classLoaderName+"]";
}
public static void test(ClassLoader classLoader) {
Class<?> clazz= null;
try {
clazz = classLoader.loadClass("VM.test");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//loadClass是父类方法,在方法内部调用findClass
System.out.println("hashCode:"+clazz.hashCode());
Object object= null;
try {
//导致初始化
object = clazz.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
System.out.println(object);
}
public static void main(String[] args){
//父亲是系统类加载器,根据父类委托机制,MyTest1由系统类加载器加载了
//MyClassLoader loader1=new MyClassLoader("loader1");
//test(loader1);
//自定义类加载器进行加载的,因为路径不再使用系统路径所以双亲委派机制找不到,所以会使用该类加载器加载
//可以发现因为父类加载器没有找到相关类,所以loader2和loader3两个类加载器都可以加载不同的类
MyClassLoader loader2=new MyClassLoader("loader2");
loader2.setPath("F:\\项目新技术\\javaSE\\javaSE\\target\\test\\");
test(loader2);
MyClassLoader loader3=new MyClassLoader("loader3");
loader3.setPath("F:\\项目新技术\\javaSE\\javaSE\\target\\test\\");
test(loader3);
//将loader2作为父加载器,这样loader5发现父类加载器已经加载过了所以就不会再加载MyClassLoader loader5=new MyClassLoader(loader2,"loader5");
loader5.setPath("F:\\项目新技术\\javaSE\\javaSE\\target\\test\\");
test(loader5);
}
类的卸载:
由java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载
类的卸载:JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
- 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
- 加载该类的ClassLoader已经被GC。
- 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法.
双亲委派机制续:
如果A类的初始化会引起B类的初始化 A类由加载器A加载,则B类会从加载A类的加载器开始,向上查找看谁能初始化B类
命名空间的重要说明:
1 :子类加载器加载的类可以访问到父加载器所加载的类
2: 父加载器加载的类不能访问到子加载器加载的类
如果想让启动类加载器加载我们写的类只需要将我们写的类放入启动类加载器加载的目录里,这样由于双亲委派机制最终会由启动类加载器加载就行
同一个类如果由两个不同的类加载器加载,会是不同的类、
类加载器的双亲委派模型的好处:
可以确保Java核心库的类型安全;所有的java应用都至少会引用java.lang.Object类,也就是说如果在运行期这种核心类可以由我们自定义的类加载器完成,那么久很可能在JVM中存在多个版本的同一个类,且相互之间是不兼容的,相互是不可见的
可以确保java核心类库所提供的的类不会被自定义的类所替代
直接 类.class会在系统类加载器的空间中找
扩展类加载器:不会直接查找目录下的.class文件,需要打成jar包才能被识别
数组类型是在运行期间由jvm虚拟机来创建加载的,而不是由加载器加载
数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。但数组类与类加载器任然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建。
系统类加载器可以在运行时动态被修改
运行时修改系统类加载器
系统类加载器可以在运行时动态被修改
java.system.class.loader
指定系统类加载器的源码解析
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
private static synchronized void initSystemClassLoader() {
//sclSet是静态变量,用来表名系统是否自己设置了系统类加载器
if (!sclSet) {
//scl用于存放系统类加载器
if (scl != null)
throw new IllegalStateException("recursive invocation");
//获取launcher对象
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
//获取应用类加载器
scl = l.getClassLoader();
try {
//主要用于设置自定义系统类加载器
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
//方法结束表明设置好了系统类加载器
sclSet = true;
}
}
/**
*在launcher中用loader记录系统类加载器,父类加载器可以用过getParent获取
*/
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);
}
//将系统类加载器也设置为当前线程的上下文类加载器
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);
}
}
/**对接上面类加载器初始化自定义的系统类加载器*/
class SystemClassLoaderAction
implements PrivilegedExceptionAction<ClassLoader> {
private ClassLoader parent;
SystemClassLoaderAction(ClassLoader parent) {
this.parent = parent;
}
public ClassLoader run() throws Exception {
//就是检查是否我们自定义了系统类加载器,如果没有就默认的系统类加载器
String cls = System.getProperty("java.system.class.loader");
if (cls == null) {
return parent;
}
//如果有·这通过反射,利用指定的类加载器加载类,且true表示进行初始化
Constructor<?> ctor = Class.forName(cls, true, parent)
.getDeclaredConstructor(new Class<?>[] { ClassLoader.class });
//创建用户自定义类加载器实例
ClassLoader sys = (ClassLoader) ctor.newInstance(
new Object[] { parent });
//将当前线程的上下文类加载器设置为我们自定义的系统类加载器
Thread.currentThread().setContextClassLoader(sys);
return sys;
}
}
线程上下文类加载器
类Thread的getContextClassLoader()与setContextClassLoader分别用来获取和设置上下文类加载器
如果没有通过setContextClassLoader()进行设置的话,线程将继承其父线程的上下文类加载器
Java应用运行时的初始按线程的上下文类加载器是系统类加载器,在线程中运行的代码可以通过该类加载器来加载类与资源
线程上下文类加载器的重要性
父类的ClassLoader无法看到其子ClassLoader所下载的类,但是可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的?ClassLoader所加载的类
这就改变了父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的CLassLoader加载的类的情况,即改变了双亲委托模型
比如最开始的Connection接口是由启动类加载器加载,而具体实现由厂商提供,最后由应用类加载器加载,通过设置当前线程类加载器,使得启动类加载器可以访问到当前线程类加载器加载的类
父ClassLoader可以使用当前线程Thread.currentThread().getContextLoader()所指定的classLoader加载的类,这就改变了父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的ClassLoader加载的类的情况,即改变了双亲委托模型
当高层提供了统一的接口让底层去实现,同时又要在高层加载(或者实例化)地层的类时,就必须要通过上下文类加载器来帮助高层的ClassLoader找到并加载类
ServiceLoader
/*
线程上下文类加载器的一般使用模式:(获取-使用-还原)
伪代码:
ClassLoader classLoader=Thread.currentThread().getContextLoader();
try{
Thread.currentThread().setContextLoader(targetTccl);
myMethod();
}finally{
Thread.currentThread().setContextLoader(classLoader);
}
在myMethod中调用Thread.currentThread().getContextLoader()做某些事情
ContextClassLoader的目的就是为了破坏类加载委托机制
在SPI接口的代码中,使用线程上下文类加载器就可以成功的加载到SPI的实现类。
当高层提供了统一的接口让底层去实现,同时又要在高层加载(或实例化)底层的类时,就必须通过上下文类加载器来帮助高层的ClassLoader找到并加载该类。*/
public class 线程上下文类加载器一般使用模式 {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
/* Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql?useSSL=false&serverTimezone=UTC","root","12345678");
System.out.println(conn.getClass().getClassLoader());
System.out.println(Thread.currentThread().getContextClassLoader());*/
//一旦加入下面此行,将使用ExtClassLoader去加载Driver.class, ExtClassLoader不会去加载classpath,因此无法找到MySql的相关驱动。
// Thread.currentThread().setContextClassLoader(线程上下文类加载器一般使用模式.class.getClassLoader().getParent());
//ServiceLoader服务提供者,加载实现的服务
// Class.forName("com.mysql.cj.jdbc.Driver")
Thread.currentThread().setContextClassLoader(线程上下文类加载器一般使用模式.class.getClassLoader().getParent());
ServiceLoader<Driver> loader=ServiceLoader.load(Driver.class);
Iterator<Driver> iterator=loader.iterator();
while(iterator.hasNext()){
Driver driver=iterator.next();
System.out.println("driver:"+driver.getClass()+
",loader"+driver.getClass().getClassLoader());
}
System.out.println("当前上下文加载器"
+Thread.currentThread().getContextClassLoader());
System.out.println("ServiceLoader的加载器"
+ServiceLoader.class.getClassLoader());
}
}
为什么在load方法中要获取上下文类加载器
因为加载ServiceLoader是在启动类加载器加载,因为里面还依赖其他类,而其他类依靠双亲委派机制1,默认是在启动类加载器以及之上的类加载器加载,所以都加载不到,所以需要设置当前线程类加载器,让启动类加载器知道该当前线程类加载器加载的类
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
为什么不用系统类加载器而用上下文类加载器
很明显了确实通过线程上下文类加载器加载的,实际上核心包的SPI类对外部实现类的加载都是基于线程上下文类加载器执行的,通过这种方式实现了Java核心代码内部去调用外部实现类。我们知道线程上下文类加载器默认情况下就是AppClassLoader,那为什么不直接通过getSystemClassLoader()获取类加载器来加载classpath路径下的类的呢?其实是可行的,但这种直接使用getSystemClassLoader()方法获取AppClassLoader加载类有一个缺点,那就是代码部署到不同服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,因为这些服务使用的线程上下文类加载器并非AppClassLoader,而是Java Web应用服自家的类加载器,类加载器不同。,所以我们应用该少用getSystemClassLoader()。总之不同的服务使用的可能默认ClassLoader是不同的,但使用线程上下文类加载器总能获取到与当前程序执行相同的ClassLoader,从而避免不必要的问题
LoadService能访问实现类的原因:
具体实现类的jar
包里有META-INF.services文件夹,里面有文件,文件名就是借口名,该文件的内容就是具体实现类的限定名
JDBC getConnection中的一部分解释
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
//防止类加载器不同导致的命名空间不同,以致于引起以后的castException
/**
因为driver是利用DriverManger初始化过程中添加的,利用的类加载器是当前线程上下文类加载器
当前
线程上下文类加载器我们可以自己制作一个合适的
而classLoader参数则是调用getConnection的类的类加载器,这样在后面的Class.forName()
即使
双亲委派机制也不会和我们自定义的类加载器一致,会出问题*/
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}