类加载器子系统的作用
ClassLoader负责从文件系统或者从网络中加载Class文件,至于它是否可以运行,有Execution Engine来决定。
加载类到哪里?
加载的类的信息存放在内存空间的“方法区”,除了类的信息外,方法区还会存放运行时的常量的映射等信息。
将常量池内的符号引用,转化为直接引用。
例:Object类的引用
初始化阶段就是执行类构造器方法<clinit>()的过程,他和类的构造器<init>()是不一样的。
此方法是javac编译器自动收集类中的所有静态变量的赋值动作和静态代码块中的语句合并而来
执行顺序按照编写的代码的顺序执行
<init>为类的构造器方法,任何一个类声明以后,内部至少存在一个类的构造器
若被加载的类具有父类,JVM会保证子类的<clinit>()执行之前,父类的<clinit>()已经执行完毕
举例:
定义父类
public class initTest { public static int A = 1; static { A=2; } }
定义子类
public class Son extends initTest { public static int B =A; public static void main(String[] args) { System.out.println(B); } }
编译运行
查看字节码文件,可以看到加载B的时候先调了父类的clinit,加载了A
虚拟机必须保证一个类的<clinit>()方法在多线程的情况下被同步加锁
举例:
编写被调用的class
public class initTest { static { //这里使用循环,目的是为了卡住<clinit>()方法,让别的线程等待 if (true){ System.out.println(Thread.currentThread().getName()+"进来了"); while (true){ } } } }
编写两个线程,两个线程都加载initTest,因为对于JVM来说,同一个类只会被加载一次,加载以后类信息等存放在方法区中
public class Son { public static void main(String[] args) { Runnable r = () ->{ System.out.println(Thread.currentThread().getName()+"开始"); initTest initTest = new initTest(); System.out.println(Thread.currentThread().getName()+"结束"); }; Thread t1 = new Thread(r,"线程1"); Thread t2 = new Thread(r,"线程2"); t1.start(); t2.start(); } }
编译运行,第一个加载initTest的线程会进去,而另一个加载initTest的线程则会在后面等待
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader),和自定义类加载器(User-Defined ClassLoader)
JVM将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
可能你会疑惑,拓展类加载器,和系统类加载器是什么类型,其实他们都是派生于ClassLoader类的,JVM均视为自定义类加载器
引导类加载器,拓展类加载器,系统类加载器,用户自定义加载器,这四者的关系是包含关系。不是子父继承的关系
举例:
import sun.misc.Launcher; import java.net.URL; import java.security.Provider; public class ClassLoaderTestFyp { public static void main(String[] args) { // 获取引导类加载器加载的路径 System.out.println("====================引导类加载器加载的路径=========================="); URL[] urLs = Launcher.getBootstrapClassPath().getURLs(); for (URL urL : urLs) { System.out.println(urL.toExternalForm()); } //从上面的路径中随意选择一个类,看一下这个类的类加载器是什么 System.out.println("====================获取引导类加载器==============================="); ClassLoader classLoader = Provider.class.getClassLoader(); System.out.println(classLoader); //应该为null,因为引导类加载器是c和c++编写,我们无法获取到 // 拓展类加载器 System.out.println("====================拓展类加载器加载的路径=========================="); String extDirs = System.getProperty("java.ext.dirs"); System.out.println(extDirs); // 系统类加载器classpath System.out.println("====================系统类加载器加载的路径=========================="); String classpath = System.getProperty("java.class.path"); for (String s : classpath.split(";")) { System.out.println(s); } } }
为什么要自定义类的加载器:
自定义类加载器实现步骤:
ClassLoader是一个抽象类,其后所有的类加载器都继承自ClassLoader(除引导类加载器)
clazz.getClassLoader()
Thread.currentThread().getContextClassLoader()
ClassLoader.getSystemClassLoader()
DriverManager.getCallerClassLoader()
Java虚拟机对class文件采用的按需加载的方式,也就是说当需要使用此类的时候,才会把这个类的class文件加载到内存,生成class对象。且,在加载某个类的class文件的时候,Java虚拟机采用的是双亲委派模式,即把请求交给父类加载器处理,它是一种任务委派模式。
举例:
当我们加载jdbc.jar包用于实现数据库连接的时候,首先我们要知道的是jdbc.jar是基于SPI接口进行实现的,所以在加载的时候会进行双亲委派,从引导类加载器加载SPI核心的类,然后再加载SPI接口类,接着在进行反向委派,通过线程上下文类加载器进行实现类jdbc.jar的加载。
避免类的重复加载
保护程序安全,防止核心的API被随意的修改
举例:假设我们自己实现了一个java.lang.String,如果给这个类写个main方法,去运行,是不行的,因为当类加载器收到类加载请求的时候会向上委托,会加载核心的String类,而核心的String类无此方法,所以会报错为方法找不到,这也称为沙箱安全机制。
如何判断两个Class对象是否相等
在JVM中表示两个Class对象是否为同一个类,有两个必要的条件
也就是说,即使两个类对象来自同一个class文件,但是加载他们的类加载器不同,那这两个对象也是不想等的。
java程序对类的使用方式分为:主动使用和被动使用
主动使用,又分为7种情况:
除了以上7种情况,其他使用java类的方式,都看作是对类的被动使用,不会导致类的初始化。