在约翰·冯·诺伊曼的计算机模型中,任何程序都需要加载到内存才能与CPU进行交流。
加载过程load
- 根据一个类的全限类名来获取此类的二进制流(此处并没有说是特指的本地class文件)
- 将这个class文件所代表的静态存储结构转化为方法区中的运行时结构
- 在内存中生成一个java.lang.Class 对象,这个对象将作为程序访问方法区中的类型数据的外部接口
其中此类的二进制流,不仅仅是本地的.class文件,也可以是从jar,war包中的,或者使用java自带的基于接口的动态代理,或者基于cglib动态生成的二进制流,或者是老式的jsp,以及加密解密的class文件等等
连接link
连接分为三个:验证,准备,解析
验证verification
验证是为了运行当前代码不会危害虚拟机自身的安全
- 文件格式验证 :1.是否是以0xCAFEBABE开头2.版本号是否在java
- 。。。。下面就不多赘述
准备prepare
为类变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始化。
但是常量(final修饰)在编译javac将常量生成ConstantValue属性,在准备阶段根据ConstantValue设置赋值。
解析
java虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化
- 初始化阶段会执行
<clinit>
方法 - 调用
clinit
方法前会调用其父类的clinit
的方法,最初调用应该是java.lang.object
的clinit
方法 - 多线程环境下会竞争
clinit
方法(要避免clinit是个死锁) clinit
主要是对被static修饰的变量和静态代码块进行赋值。如果都不存在,就没有clinit
初始化6种情况
- 遇到new、getstatic、putstatic、或invokestatic这个4个指令,如果类型没初始化过,就会初始化一下
- 通过反射调用时,如果没有初始化就会初始化一下
// 这是java mysql jdbc最基本的方式
// 会通过类加载机制,执行static方法块,在DriverManger里面注册一个Mysql驱动
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test_index?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC","root","happyday");
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select * from test limit 10");
while (resultSet.next()){
System.out.println(resultSet.getInt("id"));
}
//我们来看下,这个com.mysql.cj.jdbc.Driver里面的clinit
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
- 当初始化类的时候,如果父类没有初始化,会触发父类的的初始化
public class ClinitTest1 {
static class Father{
public static int a = 1;
static {
a = 2;
}
}
static class Son extends Father{
public static int b = a;
}
public static void main(String[] args) {
System.out.println(Son.b);
}
}
// 输出 2
- 当虚拟机启动时,用户需要的指定一个要执行的主类(包含main()),虚拟机会加载这个类
// 我举了一个不知恰不恰当的例子,通过数组定义引用类并不会触发此类的初始化
public class TestClass {
static {
System.out.println("static method");
}
public static void main(String[] args) {
TestClass[] testClasses = new TestClass[10];
}
}
// 输出 static method
public class TestClass2 {
public static void main(String[] args) {
TestClass[] testClasses = new TestClass[10];
}
}
// 无输出
- 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
- 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化
举例
public class ClassInitTest {
private static int num ;
static {
num = 2;
number = 20;
System.out.println(num);
// System.out.println(number);非法前像引用
}
private static int number = 10;
public static void main(String[] args) {
System.out.println(num);
System.out.println(number);
}
}
// 输出
/**
2
2
10
*/
/**
clinit方法
0 iconst_2
1 putstatic #2 <org/example/jvm/classload/ClassInitTest.num>
4 bipush 20
6 putstatic #3 <org/example/jvm/classload/ClassInitTest.number>
9 getstatic #4 <java/lang/System.out>
12 getstatic #2 <org/example/jvm/classload/ClassInitTest.num>
15 invokevirtual #5 <java/io/PrintStream.println>
18 bipush 10
20 putstatic #3 <org/example/jvm/classload/ClassInitTest.number>
23 return
*/
/**
反编译的class文件
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.example.jvm.classload;
public class ClassInitTest {
private static int num = 2;
private static int number = 20;
public ClassInitTest() {
}
public static void main(String[] args) {
System.out.println(num);
System.out.println(number);
}
static {
System.out.println(num);
number = 10;
}
}
*/
还有很多我没有举例到,如多线程竞争static方法,数组。。。
类加载器
java 团队有意把类的加载阶段的“通过一个类的全限定名来获取描述该类的二进制字节流”放到java虚拟机外部实现,以便让程序自己决定去获取所需类。
这个时候,如果是两个对象比较是否相等的前提必须是同一个类加载器加载比较才有意义。
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader classLoader = 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) {
e.printStackTrace();
}
return null;
}
};
Object obj = classLoader.loadClass("org.example.jvm.classload.ClassLoaderTest").newInstance();
System.out.println(obj);
System.out.println(obj instanceof org.example.jvm.classload.ClassLoaderTest);
Object obj2 = new ClassLoaderTest();
System.out.println(obj2 instanceof org.example.jvm.classload.ClassLoaderTest);
}
}
// 输出
org.example.jvm.classload.ClassLoaderTest@6e0be858
false
true
类加载器模型
public class ClassLoaderTest {
public static void main(String[] args) {
// 获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);// sun.misc.Launcher$AppClassLoader@18b4aac2
// 获取扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);// sun.misc.Launcher$ExtClassLoader@135fbaa4
// 试图获取bootstrap classloader 但没有获取到为空
System.out.println(extClassLoader.getParent());
// String 是通过引导类来获取的==>java 核心库都是通过bootstrap class loader 来引导的
System.out.println(String.class.getClassLoader());
// sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(ClassLoaderTest.class.getClassLoader());
}
}
// 输出
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@4b67cf4d
null
null
sun.misc.Launcher$AppClassLoader@18b4aac2
各个类加载器的职责
- Bootstrap Class Loader : 加载核心库(JAVA_HOME/jre/lib/rt.jar和resources.jar和sun.boot.class),是通过c++编写,java获取时是null,譬如String.class就Bootstarp Class Loader加载的
- Extension Class Loader: 加载扩展库(java.ext.dirs或jre/lib/ext包)
- Application Class Loader:加载用户自定义的类
双亲委派机制
双亲委派模型应该是叫溯源委派加载模型,起初加载类时,是依次向上询问是否已加载过, 然后再向下逐层询问是否可加载。一般在主流中间件都有自定义类加载器,实现类的隔离,防止冲突。
什么情况需要自定义类加载器
- 隔离加载类:在某些容器框架类进行中间件和应用的隔离,把类加载到不同环境。譬如mybatis中org/apache/ibatis/io中就有自定义类加载器
- 防止源码泄露:class 文件如果正常jar包反编译,解压一下就可以看见源码,所以需要编译加密,并且自定义类加载器来实现解密还原字节码。
- 等等
参考
- 《码出高效》
- 《深入理解JVM虚拟机》
- 尚硅谷JVM视频