JVM_6_类加载
类加载阶段
加载
-
将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
- _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
- _super 即父类
- _fields 即成员变量
- _methods 即方法
- _constants 即常量池
- _class_loader 即类加载器
- _vtable 虚方法表
- _itable 接口方法表
-
如果这个类还有父类没有加载,先加载父类
-
加载和链接可能是交替运行的
注意
- instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror对应的对象(xxx.class)是存储在堆中,元空间中有 _java_mirror的引用
- 可以通过前面介绍的 HSDB 工具查看
链接
验证
验证类是否符合 JVM规范,安全性检查
用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行
准备
为 static 变量分配空间,设置默认值
- static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
- static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果 static 变量是 fifinal 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果 static 变量是 fifinal 的,但属于引用类型,那么赋值也会在初始化阶段完成
源码
// 演示final对静态变量的影响
public class Demo3_2 {
static int a;
static int b = 10;
static final int c = 20;
static final String d = "hello";
static final Object e = new Object();
}
字节码
解析
类加载时,类的字节码载入方法区。解析时,将常量池中的符号引用解析为直接引用
// 解析的含义
public class Demo3_3 {
public static void main(String[] args) throws ClassNotFoundException, IOException {
ClassLoader classLoader = Demo3_3.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化,只会加载(类的字节码载入方法区)
Class<?> c = classLoader.loadClass("P3.C");
// new C() 加载、解析、初始化
// C c = new C();
System.in.read();
}
}
class C {
D d = new D();
}
class D{
}
启用HDBS工具
cd /Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home
java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
使用classLoad方法加载C
进入类C的常量池
-
使用类加载器ClassLoader的classLoad方法,只会加载C这个类。
-
加载即将类的字节码载入方法区中,但是也仅仅是字节码,不会对字节码做处理
-
所以常量池中,类D仅仅是一个符号引用,并不知道它要引用哪个地址
使用new C()
- 使用new C(),new会触发加载、解析
- 解析会将常量池中的符号引用解析为直接引用
初始化
()V 方法
初始化即调用 < cinit >()V ,虚拟机会保证这个类的『构造方法』的线程安全
发生的时机
概括得说,类初始化是【懒惰的】
-
main 方法所在的类,总会被首先初始化
-
首次访问这个类的静态变量或静态方法时
-
子类初始化,如果父类还没初始化,会引发
-
子类访问父类的静态变量,只会触发父类的初始化
-
Class.forName
-
new 会导致初始化
不会导致类初始化的情况
- 访问类的 static fifinal 静态常量(基本类型和字符串)不会触发初始化
- 在准备阶段就已经赋值了
- 类对象.class 不会触发初始化
- 在类加载阶段就已经生成了java_mirror即类对象.class
- 创建该类的数组不会触发初始化
- 类加载器的 loadClass 方法
- Class.forName 的参数 2 为 false 时
实验
class A {
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}
验证(实验时请先全部注释,每次只执行其中一个)
public class Load3 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
// 1. 静态常量(基本类型和字符串)不会触发初始化
System.out.println(B.b);
// 2. 类对象.class 不会触发初始化
System.out.println(B.class);
// 3. 创建该类的数组不会触发初始化
System.out.println(new B[0]);
// 4. 不会初始化类 B,但会加载 B、A
ClassLoader cl = Thread.currentThread().getContextClassLoader(); cl.loadClass("cn.itcast.jvm.t3.B");
// 5. 不会初始化类 B,但会加载 B、A
ClassLoader c2 = Thread.currentThread().getContextClassLoader(); Class.forName("cn.itcast.jvm.t3.B", false, c2);
// 1. 首次访问这个类的静态变量或静态方法时
System.out.println(A.a);
// 2. 子类初始化,如果父类还没初始化,会引发
System.out.println(B.c);
// 3. 子类访问父类静态变量,只触发父类初始化
System.out.println(B.a);
// 4. 会初始化类 B,并先初始化类 A
Class.forName("cn.itcast.jvm.t3.B");
}
}
练习1
从字节码分析,使用 a,b,c 这三个常量是否会导致 E 初始化
public class Load4 {
public static void main(String[] args) {
System.out.println(E.a);
System.out.println(E.b);
System.out.println(E.c);
}
}
class E {
public static final int a = 10;
public static final String b = "hello";
public static final Integer c = 20; // Integer.valueOf(20)
static {
// 如果E初始化了会打印此语句
System.out.println("init E");
}
}
输出结果
字节码
练习2
典型应用 - 完成懒惰初始化单例模式
public class Demo3_5 {
public static void main(String[] args) {
Singleton.test(); // 打印test
// 只有显示地调用了getInstance才会去声明那个单例变量
Singleton.getInstance(); // 打印lazy holder init
}
}
class Singleton {
public static void test() {
System.out.println("test");
}
// 别的类调用不了构造方法,保证单例
private Singleton() {
}
// 内部类中保存单例
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
static {
System.out.println("lazy holder init");
}
}
// 第一次调用 getInstance 方法,才会导致内部类加载和初始化其静态成员
// 不调用就不会触发L啊这样Holder的加载链接初始化,保证懒加载
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
以上的实现特点是:
-
懒惰实例化,第一次使用到的是否才会加载
-
初始化时的线程安全是有保障的
类加载器
以 JDK 8 为例:
名称 | 加载哪的类 | 说明 |
---|---|---|
Bootstrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap,显示为 null |
Application ClassLoader | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
启动类加载器
用 Bootstrap 类加载器加载类:
package cn.itcast.jvm.t3.load;
public class F {
static {
System.out.println("bootstrap F init");
}
}
执行
package cn.itcast.jvm.t3.load;
public class Load5_1 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.F");
System.out.println(aClass.getClassLoader());
}
}
输出
E:\git\jvm\out\production\jvm>java -Xbootclasspath/a:. cn.itcast.jvm.t3.load.Load5
bootstrap F init # 说明进行了初始化
null # 获取不到类加载器,说明类加载器是启动类加载器(C++代码编写的java获取不到)
- -Xbootclasspath 表示设置 bootclasspath
- 其中 /a:. 表示将当前目录追加至 bootclasspath 之后
- 可以用这个办法替换核心类
- java -Xbootclasspath:< new bootclasspath >
- java -Xbootclasspath/a:<追加路径>
- java -Xbootclasspath/p:<追加路径>
扩展类加载器
package cn.itcast.jvm.t3.load;
public class G {
static {
System.out.println("classpath G init");
}
}
执行
public class Load5_2 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("cn.itcast.jvm.t3.load.G");
System.out.println(aClass.getClassLoader());
}
}
输出
classpath G init # 说明进行了初始化
sun.misc.Launcher$AppClassLoader@18b4aac2 # G为ClassPath下的类,默认用应用类加载器
写一个同名的类
package cn.itcast.jvm.t3.load;
public class G {
static {
System.out.println("ext G init");
}
}
打个 jar 包,放在JAVA_HOME/jre/lib/ext目录下,以便让扩展类加载器加载此类
E:\git\jvm\out\production\jvm>jar -cvf my.jar cn/itcast/jvm/t3/load/G.class
已添加清单
正在添加: cn/itcast/jvm/t3/load/G.class(输入 = 481) (输出 = 322)(压缩了 33%)
重新执行 Load5_2
输出
ext G init
# 扩展类加载器优先于应用加载器
sun.misc.Launcher$ExtClassLoader@29453f44
双亲委派模式
所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则
这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查该类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2. 有上级的话,委派上级 (递归调用)
loadClass c = parent.loadClass(name, false);
} else {
// 3. 如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader(没有就没有了,不会再向上递归)
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展,重写了的)去加载
c = findClass(name);
// 5. 记录耗时
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
例如
public class Load5_3 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Load5_3.class.getClassLoader() .loadClass("cn.itcast.jvm.t3.load.H");
System.out.println(aClass.getClassLoader());
}
}
执行流程为:
-
sun.misc.Launcher$AppClassLoader //1 处, 开始查看已加载的类,结果没有
-
sun.misc.Launcher@AppClassLoader // 2 处,委派上级sun.misc.Launcher@ExtClassLoader.loadClass()
-
sun.misc.Launcher$ExtClassLoader // 1 处,查看已加载的类,结果没有
-
sun.misc.Launcher$ExtClassLoader // 3 处,没有上级了,则委派BootstrapClassLoader查找
-
BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有
-
sun.misc.Launcher@ ExtClassLoader // 4 处,调用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,回到sun.misc.Launcher@AppClassLoader 的 // 2 处
-
继续执行到 sun.misc.Launcher$AppClassLoader // 4 处,调用它自己的findClass 方法,在classpath 下查找,找到了
线程上下文类加载器
我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写
Class.forName("com.mysql.jdbc.Driver")
也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?
让我们追踪一下源码:
public class DriverManager {
// 注册驱动的集合
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
// 初始化驱动
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
先不看别的,看看 DriverManager 的类加载器:
System.out.println(DriverManager.class.getClassLoader());
打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?
继续看 loadInitialDrivers() 方法:
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String> () {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 1)使用 ServiceLoader 机制加载驱动,即 SPI
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 2)使用 jdbc.drivers 定义的驱动名加载驱动
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
// 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载,破坏了双亲委派机制,直接让应用程序类加载器加载。
再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)
约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称
这样就可以使用
ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
iter.next();
}
来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:
- JDBC
- Servlet 初始化器
- Spring 容器
- Dubbo(对 SPI 进行了扩展)
接着看 ServiceLoader.load 方法:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类LazyIterator 中:
private S nextService() {
if (!hasNextService()) throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// loader即线程上下文类加载器
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service, "Provider " + cn + " not a subtype");
}try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated", x);
}
throw new Error();
// This cannot happen
}
自定义类加载器
问问自己,什么时候需要自定义类加载器
- 想加载非 classpath 随意路径中的类文件
- 都是通过接口来使用实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤:
-
继承 ClassLoader 父类
-
要遵从双亲委派机制,重写 findClass 方法
- 注意不是重写 loadClass 方法,否则不会走双亲委派机制
-
读取类文件的字节码
-
调用父类的 defifineClass 方法来加载类
-
使用者调用该类加载器的 loadClass 方法
示例:
准备好两个类文件放入 E:\myclasspath,它实现了 java.util.Map 接口,可以先反编译看一下:
自定义类加载器
测试代码
输出结果