文章目录
- 类加载
- 1.类的生命周期
- 加载阶段
- 连接阶段
- 初始化阶段
- 2.类加载器
- 类加载器的分类
- 启动类加载器(Bootstarp)
- 扩展类加载器&应用程序加载器
- 3.类的双亲委派机制
- 什么是双亲委派机制?
- 打破双亲委派机制
- 自定义类加载器
- 线程上下文类加载器
类加载
注:本文章基于JDK1.8
1.类的生命周期
在Java中一个类的生命周期为:加载、连接、初始化、使用、卸载5个步骤
加载阶段
在加载阶段会创建两个对象一个Java.lang.class和instanceKlass对象
-
第一步
- 加载阶段第一步是类加载器根据类的全限定名(包名+类名)通过不同的渠道以二进制的方式获取字节码的信息
- 程序员可以使用Java代码拓展不同的渠道,如磁盘上的字节码文件,动态代理生成(程序运行时使用动态代理生成)等
-
第二步
- 类加载器加载完类之后,Java虚拟机会将字节码文件中的信息保存到方法区中
- 生成一个InstanceKlass对象,保存类的所有信息,里面还包含特定功能比如多态的信息
- 同时,Java虚拟机还会在堆中生成一份与方法区中数据类似的
java.lang.Class
对象,作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK8及之后)
-
对于开发者来说,只需要访问堆中的Class对象而不需要访问方法区中的所有信息。这样Java虚拟机就能很好的控制开发者访问数据的范围,instanceKlass对象是使用C/C++编写的Java程序是无法直接访问的,而堆区的
Java.lang.class
,是把instanceKlass对象进行了封装,可以让Java程序直接访问,且只包含一些开发者会使用的到的信息,去除了一些不需要的信息。
连接阶段
连接阶段又分为验证、准备、解析三个步骤
-
验证阶段
-
这个环节主要的目的是验证Java字节码文件是否遵循了《Java虚拟机规范》中的约束,这个阶段一般不需要程序员参与。
-
比如说验证字节码内容中的魔术也就是文件头是否Java字节码的文件头,以及主次版本号是否满足当前Java虚拟机的要求,主版本号不能高于运行环境主版本号,如果主版本号相等,副版本号也不能超过
-
还有就是原信息验证,比如一个类必须有父类,也就是super不能为空
-
验证程序执行指令的语义,方法内的指令执行中是否跳转到不正确的位置,比如方法内的指令执行到一半强行跳转到其他方法中
-
符号引用验证,例如是否访问了其它类中private的方法等
-
-
准备阶段
public class Demo {public static int num = 10;public static final VALUE = 100; }
- 准备阶段为静态变量(static)分配内存并设置赋初始值,静态变量是存在于堆区的class对象中,并为其赋默认值0,b比如说上诉中的num就会将其先赋值为0,注意这里并不是赋值为10,而是0。
- 而被final修改的静态变量则会直接在准备阶段将其赋值为其设置的值,因为这个值是不会改变的,例如上述代码中的VALUE会被赋值为100。因为这个值被final修饰,编译就会认为这个值以后是不会被改变的
- 为静态变量赋初值,是防止静态变量指向的内存地址中有残留的数据,为了避免出现随机值。
而静态变量的初始值,处理boolean的默认值为false,char的默认值为
'\u0000'
,引用类型为null
,其它基本类都为0 -
解析阶段
- 将常量池中的符号引用替换成指向内存的直接引用,也就是将编号索引直接替换成内存地址的引用
- 符号引用就是字节码文件中使用编号来访问常量池中的内容
- 直接引用不再使用编号,而是使用内存中地址进行访问具体的数据
初始化阶段
- 初始化阶段会执行静态代码块中的代码,并为静态变量赋值
- 初始化阶段执行字节码中clinit部分的字节码执行
一下几种方式会导致类初始化:
- 访问一个类的静态变量或者静态方法,需要注意的是变量是final修饰的并且等号右边是常量不会触发初始化
- 调用Class.forName(String className)
- new一个该类的对象时
- 执行Main方法的当前类(也就是执行main方法的类)
如下代码value的值为1,因为静态代码的执行顺序是和编写顺序一致的
public class Demo1 {static {value = 2;}public static int value = 1;public static void main(String[] args) {}
}
上面代码的字节码
如下代码:
public class Test {public static void main(String[] args) {System.out.println("A");new Test();new Test();}public Test() {System.out.println("B");}{System.out.println("C");}static {System.out.println("D");}
}
最后输出:DACBCB
,在类初始化的时候会执行静态代码块static,而普通代码块代码最后编译后指令会放在构造方法中,也就是比构造方法先执行,最后再执行构造方法。
clinit指令在特定情况下不会出现,比如:如下几种情况是不会进行初始化指令执行的
- 无静态代码块并且无静态变量赋值语句
- 有静态变量的声明,但是没有赋值语句
- 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化
需要注意的是访问父类的静态变量,只初始化父类
下面代码输出1,因为访问父类的静态变量只会初始化父类
public class Demo {public static void main(String[] args) {System.out.println(B.a);}
}
class A {static int a = 0;static {a = 1;}
}
class B extends A {static {a = 2;}
}
数组的创建不会导致数组中元素的类进行初始化,代码如下:创建这个Test数组并不会导致类初始化。也就是不会执行Test类中的静态代码块
public class Demo1 {public static void main(String[] args) {Test[] test = new Test[10];}
}class Test{static {System.out.println("1_1");}
}
final修饰的变量如果赋值的内容需要执行指令才能得出结果,会执行clinit方法进行初始化,下面代码则会对Test类进行初始化,并执行静态代码块
public class Demo1 {public static void main(String[] args) {System.out.println(Test.a);}
}class Test{public static final int a = Integer.valueOf(1);static {System.out.println("Test静态代码块");}
}
2.类加载器
类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术,类加载器只参与加载过程中的字节码获取并加载到内存这一部分。
类加载器的分类
类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的。
- 虚拟机底层实现:源代码位于Java虚拟机的源码中,实现语言宇虚拟机底层语言一致,比如Hotspot使用C++实现
- JDK中默认提供或者自定义:JDK中默认提供了多种处理不同渠道的类加载器,程序员也可以自己根据需求定制
JDK8及以前的版本中默认的类加载器有如下几种:
-
虚拟机底层使用C++实现
- 启动类加载器(Bootstrap):加载Java中最核心的类
-
Java代码实现
- 扩展类加载器(Extension):允许拓展Java中比较通用的类
- 应用程序类加载器(Application):加载应用使用的类
启动类加载器(Bootstarp)
启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供、使用C++编写的类加载器。
默认加载Java安装目录/jre/lib
下的类文件,通过一下代码尝试获取到启动类加载器。下面的代码打印为null,因为启动类加载器底层是C++实现的,而Java是上层的代码,为了安全考虑Java代码中是或不到底层的类加载器的。
public static void main(String[] args) {ClassLoader classLoader = String.class.getClassLoader();System.out.println(classLoader);
}
所以当获取到的类加载器为空的时候,其实应该就是启动类加载器。
如果我们想扩展jar包使用启动类加载器进行加载,是不建议直接将jar包放入/jre/lib
下的,这可能会导致放进去由于文件名不匹配的问题也不会正常地被加载,建议使用一下方法:
- 使用
-Xbootclasspath/a:jar包目录/jar包名
进行扩展
扩展类加载器&应用程序加载器
扩展类加载器和应用程序类加载器都是JDK提供的,使用Java编写的类加载器。它们的源码都位于sun.misc.Launcher
中,是一个静态内部类。继承自URLClassLoader。具备通过目录或者指定jar包将字节码文件加载到内存中。
- 扩展类加载器默认加载Java安装目录
/jre/lib/ext
下的类文件 - 应用程序类加载器加载
classpath
下的类文件
- URLClassLoader: 利用URL获取目录下或者指定的jar包进行加载,获取其字节码数据
- SecureClassLoader:使用证书机制提升类加载的安全性
- ClassLoader:抽象类,定义了类加载器的具体行为模式,通过JNI调用底层的Java虚拟机方法
3.类的双亲委派机制
什么是双亲委派机制?
由于Java虚拟机中有多个类加载器,双亲委派机制的核心就是解决一个类到底由谁加载的问题。
双亲委派机制有什么作用?
- 保证类加载的安全性:通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如
java.lang.String
,确保核心类库的完整性安全性 - 避免重复加载:双亲委派机制避免同一个类被多次加载
在类加载的过程中,每个类加载器都会先检查是否已经加载了该类,如果已经加载则直接返回,否则会将加载请求委派给父类加载器 。如果一个类加载器 的 parent为null,则会提交给启动类加载器加载。
每个Java实现的类加载器中保存了一个成员变量叫parent的双亲加载器,,可以理解为它的上级,并不是继承关系。
- 应用程序类加载器的parent父类加载器是扩展类的加载器,而扩展类加载器的parent是空的
- 启动类加载器是使用C++编写,没有上级类加载器
双亲委派机制指的是:自底向上查找是否加载过,再由顶向下进行加载
- 当程序需要进行加载时,首先是从应用程序加载器检查这个类是否被加载,如果没有就会让拓展类加载器进行检查,依此类推,如果这个类被加载过就会直接返回
- 当启动类加载器检查玩这个类也没有被加载时,就会判断这个类是否在它的加载列表中,如果是就直击加载,如果不存在就让下一级检测是否能被加载
- 如果在加载过程中这3个类加载器都无法去加载这个类,最后就会出现一个类无法找到的错误
小结:
- 当一个类加载器去加载某个类的时候,会自底向上查找是否加载过,如果加载过就直接返回,如果一直到最顶层的类加载器都没有加载,再由顶向下进行加载
- 应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加
载器。 - 双亲委派机制的好处有两点: 第一是避免恶意代码替换JDK中的核心类库,比
java.lang.String
,确保核心类库的完整性和安全性。第二是避免一个类重复地被加载
打破双亲委派机制
自定义类加载器
- 一个Tomcat程序中是可以运行多个Web应用的,如果这两个应用中出现了相同限定名的类,比如Servlet类,
Tomcat要保证这两个类都能加载并且它们应该是不同的类。 - 如果不打破双亲委派机制, 当应用类加载器加载Web应用1中的MyServlet之后, Web应用2中相同限定名的MyServlet类就无法被加载了。
- Tomcat使用了自定义类加载器来实现应用之间类的隔离,每一个应用会有一个独立的类加载器加载对应的类。
- 先来分析ClassLoader的原理,ClassLoader中包含了4个核心方法。
- 双亲委派机制的核心代码就位于loadClass方法中
//类加载的入口,提供了双亲委派机制。内部会调用 findClass
public Class<?> loadClass(String name)
// 由类加载器子类实现,获取二进制数据调用defineClass ,比如URLClassLoader会根据文件路径去获取类文件中的二进制数据
protected Class<?> findClass(String name)
// 做一些类名的校验,然后调用虚拟机底层的方法将字节码信息加载到虚拟机内存中
protected final Class<?> defineClass(String name,byte[] b,int off, int len)
//执行类生命周期中的连接阶段
protected final void resolveClass(Class<?> c)
来看一下loadClass方法的源码,首先是执行入口方法的内部调用的其重载的loadClass方法,传递了一个false参数,该参数表示 是否执行连接这个过程,默认为false也就是不会执行连接这个过程
public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);
}
接下来看一下调用的重载方法
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{// 类加载之前先加锁synchronized (getClassLoadingLock(name)) {// 检测当前类是否已经被加载过,如果被加载过,就直接返回Class对象Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {// 判断父类加载器是否为空if (parent != null) {// 如果父类加载器不为空,就会由父类加载器去调用对应的loadClass方法c = parent.loadClass(name, false);} else {// 如果parent为空说明此时是扩展类加载器,那么就会调用启动类加载器来进行加载,也就是Bootstrapc = 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 statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}
通过上述的源代码来看,其实双亲委派的核心代码就是这么一段:
// 判断父类加载器是否为空
if (parent != null) {// 如果父类加载器不为空,就会由父类加载器去调用对应的loadClass方法c = parent.loadClass(name, false);
} else {// 如果parent为空说明此时是扩展类加载器,那么就会调用启动类加载器来进行加载,也就是Bootstrapc = findBootstrapClassOrNull(name);
}// 如果此时还为空,说明该类此时并没有被加载过,就会被当前类加载器进行加载
if (c == null) {c = findClass(name)
}
所以想要打破双亲委派机制,就可以实现一个自定义的类加载器,也就是 自定义一个类继承ClassLoader,并重写loadClass方法,将其的双亲委派代码给去除掉,这样就打破了双亲委派机制。
如果只是实现一个自定义的类加载器,而不需要打破双亲委派机制,就可以重写findClass
方法,因为一个类在加载的时候首先是由其加载器的父类加载器进行加载,当父类没有加载时,最后则会调用findClass
方法来进行加载,我们就可以重写findClass
方法来进行自定义加载,而且不会 打破双亲委派机制
注意:如果一个自定义类加载器不知道父类加载器,默认的父类加载器就是应用程序类加载器。
两个自定义类加载器加载相同限定名的类,是不会冲突的。在同一个Java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类。
线程上下文类加载器
在JDBC中,不希望在代码中出现某一种数据库的语法,他要提高它的通用性,让其对接任何数据库都非常方便。在JDBC中使用了DriverManager
来管理项目中引入的不同数据库的驱动,使用不同的数据库,只需要添加对应的数据库驱动即可。DriverManager
会把jar包中对应的驱动加载进来,如MySQL驱动。
DriverManager
类位于rt.jar包中,由启动类加载器加载。 而用户jar包中的MySQL驱动需要由应用程序类加载器进行加载,就违反了双亲委派机制。
DriverManager
类会先由启动类加载器进行加载后,启动类加载器会委派应用程序类加载器去加载Jar包中的MySQL驱动,而正常情况是由下向上委派,而且启动类加载器比应用程序类加载器高的多,这样就打破了双亲委派机制。
DriverManager怎么知道jar包中要加载的驱动在哪儿?
- SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,如果你想去加载一个接口的实现类对象,就可以通过SPI快速的去找到
- SPI的工作原理:
- 在ClassPath路径下META-INF/services文件夹中,以接口的全限定名来命名文件名,对应的文件里面写接口的实现,
- 使用ServiceLoader加载实现类
- 整个SPI机制分为两部分,第一部分需要在驱动的Jar中,去暴露出你要让加载器去加载哪个类放到一个固定的文件中,接下来在
DriverManage
代码中就会主动去使用ServiceLoader去加载文件中的类名,并且使用类加载器去加载对应的类并创建对象。 - 通过迭代器去遍历找到Jar包符合条件的类加载并创建对象
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
SPI中是如何获取到应用程序类加载器的?
SPI中使用了线程上下文中保存的类加载器进行类的加载,这个类加载器一般是应用程序类加载器 ,源代码如下
public static <S> ServiceLoader<S> load(Class<S> service) {//获取线程上下文类加载器ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}
- 启动类加载器加载DriverManager。
- 在初始化DriverManager时,通过SPI机制加载jar包中的myql驱动。
- SPI中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。
- 这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制
那么JDBC真的打破了双亲委派机制吗?
- 首先
DriverManager
这个类位于rt.jar中,是由启动类加载器来进行加载的,是满足双亲委派机制的 - 而在Jar包中的MySQL驱动,这些类 位于ClassPath会由应用程序类加载器来进行加载,而应用程序类加载器也是满足双亲委派机制的,首先他会一层一层的向上查找,然后再向下委派,由于这个驱动没有被加载过,由于路径不满足要求,启动类加载器和拓展类加载器都不能加载最后还是有应用程序类加载器,也是满足要求
- 并且启动也没有重写loadClass方法,重写loadClass方法才会 打破双亲 委派机制
JDBC只是在DriverManager加载完之后,通过初始化阶段触发了驱动类的加载,类的加载依然遵循双亲委派机制,所以并没有打破双亲委派机制