一. 类的加载
- 什么是类的加载: 我们编写java代码存储为.java结尾的文件,经过编译器编译,将java代码转换为虚拟机指令生成.class结尾的文件,当需要某个类时,虚拟机加载指定的.calss文件,并创建对应的class对象, 将class文件加载到虚拟机内存的过程称为类的加载,通过ClassLoder 负责将class文件字节码内容加载到内存中,并将这些内容转换成方法区的运行时数据结构,ClassLoder只负责加载,是否可以运行则由 Execution Engine 决定
- 总结类加载要完成的事情
- 通过类的全限定名来获取定义此类的二进制字节流
- 将这个类字节流代表的静态存储结构转为方法区的运行时数据结构
- 在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口
- 类的加载过程分析: 加载—连接—初始化—使用—卸载
- 加载:
- 系统运行时.类加载器加载class文件,将文件中的二进制数据读取到内存中,Cpu在内存中读取数据和指令进行运算, (由于CPU的处理速度大于调入数据的速度,容易造成数据的脱节,所以需要内存起缓冲作用)
- 将.class文件读取到内存方法区后,会在堆中创建一个java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在类的加载过程中创建的,每个类对应一个Class对象,这个Class构造器私有,通过jvm才能创建,是反射的路口,通过该类,通过反射获取需要的目标类的.class文件中的数据
- 总结: 加载一个类时,jvm会在堆中创建这个类对应的Class对象,通过这个Class对象,利用反射获取到需要加载的类的clss文件
- 连接(分为三步,验证,准备,解析)
- 验证: 确保加载的类符合jvm规范
- 准备: 在内存的方法区(静态域)中,为类的静态成员分配内存,初始化静态成员(注意只是分配内存初始化,并不赋值,创建类时先加载类的静态资源,静态变量,静态代码块,静态方法,静态类等)
- 解析: 虚拟机常量池的符号引用替换为直接引用过程
- 初始化: 执行目标类的静态内容----构造器,对目标类对象创建的过程(多个静态内容按照代码的先后顺序执行),如果初始化的目标类有父类,会先初始化父类,简而言之主要工作就是:会执行系统提供的一个clinit方法为类的静态成员赋值,
-
在执行完加载后对应这个类的Class在存在什么位置: 上面我们了解到类的加载过程,当整个对象加载后会有对象引用,对象实例,与对应该类的Class对象,引用存在栈中,对象实例存在堆中,Class也是存在堆中
-
数组的加载有什么不同: 数组本身不是由类加载器创建的,而是通过JVM在运行时根据需要直接创建的,但是数组的元素类型仍然需要有类加载器加载完成
-
final类型变量与static类型变量分别在什么时候进行赋值的: final类型变量是在链接阶段进行显示赋值的,除了final类型变量以外其它所有变量例如static是在类加载时初始化步骤执行系统提供的clinit方法赋值的,然后再初始化局部变量属性等,进而又引出一个问题,所有final类型变量都是在链接阶段赋值吗?不是,例如先创建一个static类别变量,然后将该变量值赋值给一个final类型变量,该final类型变量就是在初始化阶段进行实际的赋值
-
初始化阶段会执行一个系统提供的clinit方法对类中静态成员进行初始化,那么clinit会有可能造成死锁吗,会,例如创建了A,B两个类,两个类中分别都在静态代码块等待对方先加载完成拿到资源完成自己的初始化,互相等待资源造成死锁
class A {
static {
try {
TimeUnit.SECONDS.sleep(5);
Class.forName("com.juc.test.B");
} catch (InterruptedException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class B {
static {
try {
TimeUnit.SECONDS.sleep(5);
Class.forName("com.juc.test.A");
} catch (InterruptedException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
- 获取 Class 的三种方式,类加载器需要Class去获取
public class T1 {
public static void main(String[] args) throws ClassNotFoundException {
//1.通过类名.class
Class cla = T1.class;
//2.通过 Class 调用 forName() 静态方法,输入需要获取 Class 类的全类名
Class cla2 = Class.forName("全类名");
//3.通过需要获取 Class 的对象调用 getClass() 方法
T1 t = new T1();
Class cla3 = t.getClass();
//通过 Class 获取类加载器
cla.getClassLoader();
}
}
-
什么时候会触发将一个类加载完毕,注意此处的加载不是指上面加载类时单独加载这一个步骤: 主动使用的情况下
-
Class.forName(“包名.类名”)与Class.getClassLoad().loadClass(“包名.类名”)有什么不同: 一个类的加载步骤分为装载—连接—初始化—使用—卸载五个步骤,使用Class.forName(“包名.类名”)获取一个类的Class,对应加载这个类会执行到初始化步骤,而通过Class.getClassLoad().loadClass(“包名.类名”)方式获取一个类的Class时,对应类的加载步骤只会执行load装载步骤
-
加载器的分类: Bootstrap----> Extension—>AppClassLoader
- Bootstrap 引导类加载器: C++ 语言编写的,主要用来加载jvm自身需要的类
- Extension 扩展类加载器: java 语言编写的,责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库
- AppClassLoader 系统类加载器(应用类加载器): 负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
- 一个类只能加载一次吗: 一个类只能被同一个类加载器加载一次
- 初始化一个类时该类的所有父类都会被初始化吗? 接口除外,初始化一个类或接口时,不会先初始化该类或接口的父接口,只有当程序首次使用特定接口的静态字段时才会导致初始化该接口
- 什么情况下类会被卸载: 当一个类被加载,链接,初始化后看为生命周期开始,当该类对应的Class对象不再被引用,变为不可达对象Class对象结束生命周期,该类在方法区中对应的数据也会被卸载,从而整个类的生命周期结束,类的卸载要注意下面几点
- 方法区的垃圾回收做了些什么工作
二. 类的加载器
- 什么是类的加载器: 首先要了解加载一个类的步骤: 加载—>链接—>初始化—>(使用—>卸载),类的加载器是用来加载一个类的,需要注意的是类加载器在加载一个类的步骤中只对应第一步加载阶段,是JVM执行类加载机制的前提
- 获取一个类的Class有哪几种方式: 实际可分为显示加载与隐式加载
- 类.class,
- 对象.getClass(),
- Class.forName(“com.Person”),
- Class.getClassLoad().loadClass(“包名.类名”),
- 类加载器有哪些特征
- 有哪些类加载器: BootStrap ClassLocader启动类装载器(又叫引导类加载器)—> ExtensionClassLocader扩展类装载器—>ApplicationClassLoader 应用程序类装载器---->还有就是用户自定义类装载器
- Object是使用Bootstrap 加载器进行加载的,自定义类使用 AppClassLoader 加载器加载,获取自定义类加载器的父加载器返回ExtClassLoader 加载器,不同加载器之间有继承关系, Bootstrap 是所有加载器的父加载器
- 获取类加载器示例
public class T1 {
public static void main(String[] args) {
//1.获取 Object 对象类加载器
//Object所有类的父类,使用 JAVA 默认提供的 Bootstrap 加载器加载
//Bootstrap也就是启动类加载器
Object obj = new Object();
//打印为 null说明使用 Bootstrap 加载器加载的
System.out.println(obj.getClass().getClassLoader());
//查看自定义类 T1 的类加载器,输出 sun.misc.Launcher$AppClassLoader@18b4aac2
//自定义类使用 AppClassLoader 加载器加载
System.out.println(T1.class.getClassLoader());
//输出 sun.misc.Launcher$ExtClassLoader@63c12fb0
//ExtClassLoader 加载器
System.out.println(T1.class.getClassLoader().getParent());
//null, Bootstrap 加载器
System.out.println(T1.class.getClassLoader().getParent().getParent());
}
}
类加载器的双亲委派机制
- 双亲委派机制: 使用类加载器加载一个类的过程,类加载器收到类加载请求,并不会自己加载,而是把这个请求委托给父加载器去执行,如果父加载器还存在父加载器,则继续向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父加载器可以完成类加载任务,成功返回,倘若父类加载器无法完成加载任务,向下放行子加载器才会尝试自己去加载,这就是双亲委派模式
- 双亲委派的优点: Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次, 考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报java.lang.SecurityException: Prohibited package name: java.lang
- **ClassLoader源码:**加载一个类时实际执行的是ClassLoader下的loadClass()方法,解读下方的源码,先获取父类加载器进行加载,如果父类加载器加载失败使用Bootstrap进行加载,如果Bootstrap还是加载失败则调用findClass()自己加载,这就是双亲委派,并且在自定义类加载器时,重写这个findClass()方法,通过该方法加载即可,不推荐通过重写loadClass()方法的方式实现自定义类加载器,防止破坏双亲委派模型
//1.加载一个类时执行的方法
public Class<?> loadClass(String name) throws ClassNotFoundException {
//调用loadClass,传递的false为是否解析加载的这个类,不解析
return loadClass(name, false);
}
//2.调用的loadClass方法
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
//1.先调用一个findLoadedClass()方法获取这个类的Class
Class<?> c = findLoadedClass(name);
//2.判断获取到的Class,判断当前加载的类是否已经加载过
if (c == null) {
long t0 = System.nanoTime();
try {
//2.1判断当前类加载器是否存在还存在父类加载器
if (parent != null) {
//如果存在父类加载器,通过父类,执行父类加载器的loadClass()方法加载该类
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,通执行findBootstrapClassOrNull()方法通过Bootstrap类加载器进行加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//3.判断通过上方最终父类加载器,或Bootstrap启动类加载是否成功,如果为null说明不成功
if (c == null) {
//3.1通过自己的类加载器加载
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();
}
}
//4.是否解析
if (resolve) {
//4.1.执行resolveClass()方法解析该类
resolveClass(c);
}
return c;
}
}
- 自定义类加载器
- 继承 ClassLoader
- 重写 findClass() 方法
- findClass()方法内读取类文件为字节数组
- 通过defineClass()将读取到的类文件数组加载为Class
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class UserDefineClassLoader extends ClassLoader {
//当前自定义类加载器加载的文件类文件路径
private String rootPath;
public UserDefineClassLoader(String rootPath) {
this.rootPath = rootPath;
}
//自定义ClassLoader主要方法,通过自定义classLoader
//可以在加载指定类时进行自定义操作例如加解密,隔离等等
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//1.获取需要加载的类文件路径
String filePath = this.rootPath + "\\" + name.replace(".", "\\") + ".class";
//2.获取指定路径下的class文件二进制流
byte[] data = this.getBytesFormPath(filePath);
//3.自定义Classloader内部建议调用defineClass(),将读到的指定的类文件转换为Class
return defineClass(name, data, 0, data.length);
}
private byte[] getBytesFormPath(String filePath) {
FileInputStream inputStream = null;
ByteArrayOutputStream byteArrayOutputStream = null;
try {
inputStream = new FileInputStream(filePath);
byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
return byteArrayOutputStream.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (null != inputStream) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (null != byteArrayOutputStream) {
byteArrayOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
public static void main(String[] args) throws ClassNotFoundException {
//注意需要加载的类所在位置必须正确,类.class文件必须存在
UserDefineClassLoader loader = new UserDefineClassLoader("E:\\work001\\test\\test-redis-parent\\sprngboot-redis-tk\\target\\classes");
Class classA = loader.findClass("com.juc.test.A");
System.out.println(classA);
//演示2, 上面通过new出来的类加载器获取到了classA,
//现在再new出第二个类加载器去加载第二个同一个类的class
UserDefineClassLoader loader2 = new UserDefineClassLoader("E:\\work001\\test\\test-redis-parent\\sprngboot-redis-tk\\target\\classes");
Class classB = loader.findClass("com.juc.test.A");
//判断classA是否==clssB,返回false,原因new了两个类加载器,使用两个类加载器加载的,进而实现了隔离
System.out.println(classA == classB);
}
}
- 自定义类加载器的好处
- 自定义类加载器的应用场景有哪些
- 破坏双亲委派机制: 双亲委派是在java1.2以后推出的,1.2前没有,还有就是JDBC中,就需要通过Bootstrop引导类加载器拿到AppliactionClassLoader系统类加载器加载的jdbc.jar SPI接口实现类,双亲委派是不允许的,而是通过一个反向委托线程上下文加载器ContextClassLoader反向去加载获取SPI接口实现类,ContextCassLoader就是ApplicationClassLoader,还有热部署也是打破双亲委派的,还有Tomcat的类加载机制为了实现隔离性也是打破双亲委派的每个webappClassLoader加载自己目录下的class文件不会传递给父加载器加载,但是Thocat8中可以通过"< loader delegate=“true” >" 来开启双亲委派
- Tomcat打破双亲委派后提出的几个问题