类的生命周期
1. 加载
加载过程总览
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 通过字节流的方式把
.class
文件放入MetaSpace
- 在堆中生成一个Class对象,指向这个
.class
文件的地址
动态代理类的加载
java.lang.reflect.Proxy
或者CGLib
都是在运行时生成字节码,也可以把这个直接在堆内存中的字节码加载进到MetaSpace
,同样堆中生成一个Class
对象指向这块内存。
数组类的加载
-
如果数组的元素类型是引用类型,而且这个类还没有被放入
MetaSpace
,那么先加载元素类,然后创建的数组类的工作空间就是加载这个类的类加载空间。这句话好像有点绕,等会介绍完类加载器画个图示吧。
-
如果数组元素是基本类型(int[] 的元素类型 int),则这数组的名称空间就是
BootStrap
类加载空间 -
数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的 可访问性将默认为
public
反射的基础
/*
* @Author 郭学胤
* @University 深圳大学
* @Description
* @Date 2021/2/21 13:14
*/
public class L02_Reflect {
public static void main(String[] args) throws NoSuchFieldException {
ReflectTest reflectTest = new ReflectTest();
// 通过指向类文件的引用,查找类中定义的field
reflectTest.getClass().getField("i");
// 查找其他属性、方法、父类、接口、注解
}
}
class ReflectTest{
public int i;
public void m(){
}
}
反射也都是基于这个Class对象实现的,通过Class引用找到相应的类文件,然后查找自己需要的信息。
验证
验证class文件是否以魔数CAFE BABE
开头、是否继承了final
类、是否实现了抽象类或者接口的全部方法等等等吧
准备
静态变量
准备阶段是正式为静态变量,分配内存并设置类变量初始值的阶段。
static int i = 1;
static Object = new Object;
static boolean flag = true;
这三个变量经过准备阶段之后会变成默认值:0 , null , false
把变量赋值为初始值(自己设定的值)的代码存放于类构造器<clinit>()
方法之中,所以把static变量赋值为初始值要到类的初始化阶段才会被执行。
final
被final修饰的变量其实就是常量了,准备阶段会把常量赋值。
static final int constantValue = 1;
会在此阶段赋值为1。
解析
简单来讲就是把符号引用转成直接引用。
符号引用
符号可以是任何形式的字面量。
在上一节中说过的常量池中的第二项指向第十五项,第十五项为一个utf8
的字符串java/lang/Object
,此时这个就是一个符号引用。
直接引用
是可以直接指向目标的指针、或者是一个能间接定位到目标的句柄。
解析是在干什么
假设现在已经把java/lang/Object
这个类加载到了MetaSpace
,堆中有一个Object.Class
对象指向MetaSpace
中的这块内存区域
我的第二项或者第十五项不在单纯的记录这个类的全限定名了,我要把他换成一个指向这块MetaSpace
内存的指针,或者换成一个指向Object.Class
的指针。
当然除了类的解析之外,还会有接口、Files、方法等的解析。
解析的过程又可能会触发其他类的验证准备过程。
-
引用类的接口(如果接口中默认方法)
-
递归加载父类
-
然后还要确定当前解析的类对引用类是否具有访问权限。如果发现不具备访问权限, 将抛出
java.lang.IllegalAccessError
异常。 -
解析一个
Field
class Test{ main(){ //OneClass.oneStaticField... } }
出现上述伪代码情况的时候会解析类中字段,解析
OneClass
的接口或者父类,然后查找这个Filed的引用,失败则NoSuchFieldError
,无权限则IllegalAccessError
-
解析一个方法
class Test{ main(){ //OneClass.oneStaticMethod() } }
解析
OneClass
的接口或者父类,然后查找这个oneStaticMethod()
的引用,失败则NoSuchMethodError
,无权限则IllegalAccessError
初始化
准备阶段时,变量已经赋过一次系统要求的默认值,而在初始化阶段,需要给这些变量赋值为代码声明的初始值。
<clinit>()
方法与类的构造函数<init>()
不同这个方法是所有的静态变量 + 静态代码块收集的总和。
<clinit>()
与<init>()
class Test{
static int i = 1;
static Object o = new Object();
static{
System.out.println("来了就是深圳人!");
}
public Test(){}
}
可以理解成下边,此代码经过JAVAC
编译之后
class Test{
// **************
// 准备阶段赋值默认值
// **************
static int i;
static Object o;
// *******************************************
// 虚拟机自动把所有的静态语句收集到一起,当成clinit方法
// *******************************************
synchronized <cinit>(){
i = 1;
o = new Object();
static{
System.out.println("来了就是深圳人!");
}
}
// *************
// 构造方法 <init>
// *************
public Test(){}
}
初始化阶段就是在执行<cinit>()
请注意这里的<cinit>()
是需要同步的,同一个类加载器下保证多个线程同时初始化一个类的时候也是cinit
只执行一次
实验代码
/*
* @Author 郭学胤
* @University 深圳大学
* @Description
* @Date 2021/2/21 14:39
*/
public class L03_ClassLoader {
public static void main(String[] args) {
Runnable runnable = ()->{
new TestClassLoader();
};
new Thread(runnable, "t1").start();
new Thread(runnable, "t2").start();
}
}
class TestClassLoader{
// 虚拟机收集 static 代码块为 cinit 方法的时候,会加锁!!!!!!!
static {
/*
* 参考了周志明老师的深入理解Java虚拟机才知道用这个判断不会报错
* 单独写一个 while(true)编译不通过
* */
if(true){
while (true){
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
此小实验会一直输出进入同步代码块的线程名字。
面试题
class Interview {
public static T t = new T();
public static int count = 2;
private T() {
count ++;
}
}
// 这个变量count 最后的值为多少,答案是 2
class Interview {
// 两个static语句换了一下位置
public static int count = 2;
public static T t = new T();
private T() {
count ++;
}
}
// 这个变量count 最后的值为多少,答案是 3
卸载
当一个类的Class对象没有强引用指向他之后,GC线程回收Class对象,此时也没有任何引用指向MetaSpace
中的类文件,此时类文件所占用的内存也会被GC回收。完成类的卸载过程。至此一个类的生命周期结束。等到下次再用到这个类的时候再去加载一次。重新走一次刚刚的流程。
类加载器
双亲委派模型
很多以为双亲委派模型的意思是下层的类加载器是继承自上层的加载器,但是不是的!!!
不是继承自上层的类加载器!!!
不是继承自上层的类加载器!!!
不是继承自上层的类加载器!!!
重要事情说三遍。而是类中的一个属性
public abstract class ClassLoader {
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent; // !!!!!!!!就是它
}
委派工作过程
-
一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给(父-类加载器)去完成,
-
每一个层次的类加载器都是如此,因此所有的 加载请求最终都应该传送到最顶层的启动类加载器中
-
只有当父加载器的搜索范围中没有找到所需的类时,子加载器才会尝试自己去完成加载。
委派的意义
java.lang.Object
它存放在rt.jar
之中,无论哪一 个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类 在程序的各种类加载器环境中都能够保证是同一个类。
如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object
的类,并放在程序的ClassPath
中,那系统中就会出现多个不同的Object类,应用程序将会变得一片混乱。
源码解析
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
/* 查看此类是否已经加载过,如果已经加载,直接返回 */
Class<?> c = findLoadedClass(name);
if (c == null) {
/* 如果没加载过 */
long t0 = System.nanoTime();
try {
/* 先让父-类加载器去loadClass,父也是先检查是否已经加载过 */
if (parent != null) {
c = parent.loadClass(name, false);
} else {
/* BootStrap类加载器的父为空,所以找到这里直接就开始加载 */
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
/* 如果都已经在父加载器中找了一圈,也没找到而且也没加载成功,就只能自己加载了 */
long t1 = System.nanoTime();
c = findClass(name);
// ...
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
打破双亲委派模型
JNDI
现在已经是Java的标准服务, 它的代码由BootStrap ClassLoader
来完成加载(在JDK 1.3
时加入到rt.jar
的)。JNDI
存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程 序的ClassPath
下的JNDI
服务提供者接口(Service Provider Interface, SPI
)的代码,现在问题来了,BootStrap ClassLoader
是绝不可能认识、加载这些代码的,那该怎么办?
对资源进行查找和集中管理是神魔恋?
数据库驱动
拿JDBC
来说,DriverManager
是rt.jar
中用来对数据库连接资源进行统一管理的类。进行管理现在的厂商是Oracle
和MySQL
DriverManager
是由BootStrap ClassLoader
来进行加载的。
现在的标准都是各厂商把配置信息写在META-INF/services/java.sql.Driver
中,现在的内容都已经换成了com.mysql.cj.jdbc.Driver
package com.mysql.cj.jdbc; // 进入文件指明的包中找到对应的Driver文件
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
// 调用 DriverManager 静态方法把自己注册进DriverManager让其管理
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
触发DriverManager
类加载
调用DriverManager
的静态方法registerDriver()
导致DriverManager
被加载
public class DriverManager {
// 用一个列表来管理所有注册的数据库连接驱动
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
// ...
static {
// 类加载时执行方法(见下)
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
// ...
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 获取到 APP 类加载器
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
/*
* public static <S> ServiceLoader<S> load(Class<S> service) {
* *********************************************
* 获取到线程上下文加载器,默认就是APP加载器
* *********************************************
* ClassLoader cl = Thread.currentThread().getContextClassLoader();
* return ServiceLoader.load(service, cl);
* }
*/
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
/*
* 方法调用见下个代码块详解
* 获取到所有的"META-INF/services/java.sql.Driver"中定义的类
* com.alibaba.druid.proxy.DruidDriver -> {DruidDriver@5625}
* com.alibaba.druid.mock.MockDriver -> {MockDriver@5641}
* com.mysql.cj.jdbc.Driver -> {Driver@5652}
*/
while(driversIterator.hasNext()) {
// 使用 Class.forName(cn, false, loader);
// 其中 loader 就是 APPClassLoader
driversIterator.next();
}
} catch(Throwable t) {
}
// 仍然返回为空????
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 代码就在这里返回了????
if (drivers == null || drivers.equals("")) {
return;
}
// ......
}
}
但是现在的问题是DriverManager
的类加载器是BootStrap ClassLoader
,它无法加载rt.jar
之外的类,所以为了解决这种问题,线程都有了自己的线程上下文类加载器
public class Thread implements Runnable {
//...
private ThreadGroup group;
/* contextClassLoader 默认为 APPClassLoader */
private ClassLoader contextClassLoader;
ThreadLocal.ThreadLocalMap threadLocals = null;
}
通过这个上下文加载器加载各大厂商自己的驱动类。
这就是所谓的打破双亲委派模型。
之前的逻辑是子类加载器加载一个类的时候向上询问,然而当父加载器用到子类加载器加载路径中的类时束手无策,只能等着报ClassNotFoundException
,
因为父只能继续向上问,Boot没得问,只能自己加载,然而Boot的加载路径只有rt.jar
,所以他不可能找到,现在Boot可以拿到APPClassLoader
,让他给Boot加载到MetaSpace
中,Boot拿过来用。
定义自己的类加载器
findClass()
继续遵循双亲委派
protected Class<?> findClass(String name) throws ClassNotFoundException {
File f = new File("Urpath");
try {
FileInputStream fi = new FileInputStream(f);
// ByteArrayInputStream byteIn = new ByteArrayInputStream(fi);
int CAFEBABE = 0;
ByteArrayOutputStream op = new ByteArrayOutputStream();
while ((CAFEBABE = fi.read()) != -1){
op.write( CAFEBABE );
}
byte[] bytes = op.toByteArray();
op.close();
fi.close();
return defineClass(name, bytes, 0, bytes.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
throw new NoClassDefFoundError();
}
loadClass()
重写此方法打破双亲委派
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
File f = new File("Urpath");
if(!f.exists()) return super.loadClass(name);
try {
InputStream is = new FileInputStream(f);
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
}
return super.loadClass(name);
}