JVM虚拟机(一):类加载机制
类加载的时机
类加载的生命周期为: 加载、验证、准备、解析、初始化、使用、卸载七个阶段,其中验证、准备、解析三个阶段统称为连接。其中加载与连接时交叉执行的。
类必须初始化的六种情况
- 遇到new、getstatic、putstatic、或者invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需先触发其初始化阶段。
- 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需先触发其初始化阶段。
- 当初始化类的时候,其父类还没有初始化,则需先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
- 当使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需先触发其初始化。
- 当一个接口中定义了JDK8新加入的默认方法(default关键字修饰的接口方法)时,如果有这个接口的实现类进行初始化的场景,那该接口要在其之前被初始化。
注意: ① 《Java虚拟机规范》中使用了非常强烈的限定语——"有且仅有"这六种情况。② 接口与类真正区别的是第三种初始化场景: 接口在初始化时,并不要求其父接口全部完成初始化,只有真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。
被动引用示例
示例一
package com.chinda.init;
/**
* 被动使用类字段演示一
* 通过字类引用父类的静态字段, 不会导致子类初始化
*
* @author Wang Chinda
* @date 2020/3/16
* @see
* @since 1.0
*/
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
package com.chinda.init;
/**
* @author Wang Chinda
* @date 2020/3/16
* @see
* @since 1.0
*/
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
/**
* 控制台打印输出
* SuperClass init!
* 123
* <p>
* 通过字类引用父类的静态字段, 不会导致子类初始化
*/
@Test
public void test1() {
System.out.println(SubClass.value);
}
示例二
/**
* 通过数组定义来引用类, 不会触发此类的初始化
*/
@Test
public void test2() {
SubClass[] sca = new SubClass[10];
}
示例三
package com.chinda.init;
/**
* @author Wang Chinda
* @date 2020/3/16
* @see
* @since 1.0
*/
public class ConstClass {
static {
System.out.println("ConstClass init");
}
public static final String HELLOWORLD = "helloworld";
}
/**
* 常量在编译阶段会存入调用类的常量池中, 本质上没有直接引用到定义常量的类, 因此不会触发定义常量的类的初始化。
*/
@Test
public void test3() {
System.out.println(ConstClass.HELLOWORLD);
}
类加载的过程
加载
加载阶段时整个类加载过程中的一个阶段,在加载阶段,Java虚拟机需要完成三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据机构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
注意:① 数组不是通过类加载器创建的,是Java虚拟机直接在内存中动态构造出来的。但是数组的元素类型是需要类加载器来完成加载。② 加载阶段与连接阶段的部分动作是交叉进行的。
验证
这一阶段的目的就是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证虚拟机的自身安全。验证阶段大致会完成四个阶段的校验动作:
- 文件格式校验。
- 元数据校验。
- 字节码验证。
- 符号引用验证。
准备
准备阶段是正式为类中定义的变量(即静态变量)分配内存并设置类变量初始值的阶段。这些变量所使用的内存都在方法区进行分配。
public static int value = 123;
变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,而把value赋值为123的putstatic执行时程序被编译后,存放于类构造器
public static final int value = 123;
编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue的设置将value赋值为123。
注意:方法区本身是一个逻辑上的区域,JDK7及以前,HotSpot使用永久代来实现方法区时,实现时完全符合这种逻辑概念的。JDK8及以后,类变量则随着Class类对象一起存放在Java堆中,这时候"类变量在方法区"就完全是一种对逻辑概念的表述。
解析
- 类或接口的解析。
- 字段解析。
- 方法解析。
- 接口方法解析。
初始化
初始化阶段就是执行类构造器
()方法是有编译器自动收集类变量赋值动作和静态语句块(static {}块)种的语句结合产生的,编译器收集的顺序是由语句在源文件中出现的先后顺序决定的。 - 子类
()方法执行前,父类的 ()方法必须已经执行完毕。 - 接口中也存在
()方法,但是不强制父接口的 ()方法必须在子接口 ()前。 - Java虚拟机必须保证一个类的
()方法在多线程种被正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的 ()方法,其他线程都需要阻塞等待,直到活动线程执行完毕 ()方法。
示例一
/**
* 非法的向前引用
* 静态语句块种只能访问到定义在静态语句块之前的变量, 定义在它之后的变量, 在前面的静态语句块可以赋值, 但是不可以访问。
* @author Wang Chinda
* @date 2020/3/26
* @see
* @since 1.0
*/
public class InitTest01 {
static {
i = 0; // 可以正常编译通过, 原因是在准备阶段已经将此变量赋值为0
System.out.println(i);// 这句编译器会提示"非法的向前引用"
}
static int i = 1;
}
示例二
/**
* 控制台打印值是:2
* 原因: 因为子类<clinit>()方法在执行前, 父类的<clinit>()方法必须已经执行完毕。
* @author Wang Chinda
* @date 2020/3/26
* @see
* @since 1.0
*/
public class InitTest02 {
static class Parent {
public static int a = 1;
static {
a = 2;
}
}
static class Sub extends Parent {
public static int b = a;
}
public static void main(String[] args) {
System.out.println(Sub.b);
}
}
示例三
/**
* 控制台打印
*
* Thread[Thread-0,5,main] start
* Thread[Thread-1,5,main] start
* Thread[Thread-0,5,main] init DeadLoopClass
* 分析: Thread-0 初始化执行DeadLoopClass类的<clinit>()方法时, 将类加锁, 导致Thread-1一直被阻塞状态。
*
* @author Wang Chinda
* @date 2020/3/26
* @see
* @since 1.0
*/
public class InitTest03 {
static class DeadLoopClass {
static {
// 如果不加上这个if语句, 编译器将提示"Initializer must be able to complete normally", 拒绝编译
if (true) {
System.out.println(Thread.currentThread() + " init DeadLoopClass");
while (true) {
}
}
}
}
public static void main(String[] args) {
Runnable script = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() + " start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread() + " run over");
}
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
}
类加载器
”通过一个类的全限定名获取描述该类的二进制字节流“这个动作被称为“类加载器”(Class Loader)。分别为:启动类加载器(BootstrapClassLoader)、扩展类加载器(ExtensionClassLoader)、应用程序加载器(ApplicationClassLoader)。
类与类加载器
同一个Class文件通过不同的加载器加载,在Java虚拟机种是两个相互独立的类。
示例
package com.chinda.init;
import java.io.IOException;
import java.io.InputStream;
/**
* @author Wang Chinda
* @date 2020/3/27
* @see
* @since 1.0
*/
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException();
}
}
};
Object obj = myLoader.loadClass("com.chinda.init.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof ClassLoaderTest);
System.out.println(obj instanceof com.chinda.init.ClassLoaderTest);
}
}
运行结果:
class com.chinda.init.ClassLoaderTest
false
false
双亲委外模型
双亲委派模型的工作流程是:如果一个类加载器收到类加载请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,一直委派到最顶层启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。
通俗讲:
孙子弄到了一个梨,给了他爹,让他爹吃。他爹一想,孙子他爷爷还没吃,就拿给了孙子他爷爷。孙子他爷爷拿到梨看了下和孙子他爹说,这个梨太硬,牙口不好,吃不了,给了孙子他爹。孙子他爹拿到梨,看梨嫌弃梨酸,就把梨又给了这个孙子。最后孙子拿这个梨吃。能吃,说明类加载成功,吃不了,抛出异常。要是孙子他爹不嫌弃酸的话,这个梨就孙子他爹吃了,也就是说,这个类孙子他爹加载。
启动类示例代码
/**
* @author Chinda.Wang
* @date 2020/3/9
* @see
* @since 1.0
*/
public class ClassLoaderTest1 {
public static void main(String[] args) {
// 应用加载器(AppClassLoader)
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(appClassLoader);
// 扩展加载器(ExtClassLoader)
ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println(extClassLoader);
// 启动类加载器(BootstrapClassLoader)
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);
}
}
运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@449b2d27
null
启动类加载器是C实现的,所以返回是null
加载路径
/**
* @author Chinda.Wang
* @date 2020/3/9
* @see
* @since 1.0
*/
public class ClassLoaderTest2 {
public static void main(String[] args) {
System.out.println("****************启动类加载器***************");
// 启动类加载器(BootstrapClassLoader)加载路径
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
Arrays.stream(urls).forEach(System.out::println);
System.out.println("****************扩展加载器***************");
String property = System.getProperty("java.ext.dirs");
Arrays.stream(property.split(";")).forEach(System.out::println);
}
}
运行结果:
****************启动类加载器***************
file:/E:/Java/jdk1.8.0_201/jre/lib/resources.jar
file:/E:/Java/jdk1.8.0_201/jre/lib/rt.jar
file:/E:/Java/jdk1.8.0_201/jre/lib/sunrsasign.jar
file:/E:/Java/jdk1.8.0_201/jre/lib/jsse.jar
file:/E:/Java/jdk1.8.0_201/jre/lib/jce.jar
file:/E:/Java/jdk1.8.0_201/jre/lib/charsets.jar
file:/E:/Java/jdk1.8.0_201/jre/lib/jfr.jar
file:/E:/Java/jdk1.8.0_201/jre/classes
****************扩展加载器***************
E:\Java\jdk1.8.0_201\jre\lib\ext
C:\Windows\Sun\Java\lib\ext