01 类加载深入解析和阶段分解
-
java代码中,类的加载,连接与初始化过程都是在程序运行期间完成的。
- 类:指的是我们在程序中定义的一个class,interface,enum等。注意这里的类型并不是对象(new)。
- 举例子:这里的类型是指Object类本身而不是Object new出来的实例对象。因为在实际的开发过程中一直对对象进行操作,所以类型这个概念可能没有太多的概念。因为在创建对象之前,jvm已经把改类已经加载完毕,我们程序才能直接进行创建对象。
- 类:指的是我们在程序中定义的一个class,interface,enum等。注意这里的类型并不是对象(new)。
-
提供更大的灵活性,增加了更多的可行性。
-
整体流程如下所示:
01 加载
-
类的加载指的是将类的Class文件的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个java.lang.class对象用来封装类在方法区的数据结构。
-
加载.class文件的方式 – jvm规范中并没有明确规定.class文件的加载方式
- 从本地系统中直接加载(也是大部分的加载方式)
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有数据库中提取,class文件
- 将java源文件动态编译成.class文件 ----> 典型的场景:动态代理,jsp->servlet
02 连接
- 验证:确保被加载的类的正确性
- 准备:为类的静态变量分配内存,并将其初始化默认值
class Test{
//准备阶段将静态变量a初始化默认值,int的默认值为0
public static int a = 1;
}
- 解析:把类的符号引用转换为直接引用。
- 符号引用 – 间接的引用,用某个符号来表示引用关系。
- 直接引用将对象指向类的内存地址(可以直接找到具体某个类)
03 初始化
- 为类的静态变量赋予正确的初始值
class Test{
//初始化阶段将静态变量a赋予正确的初始值,a =1 如何a 没有显示的赋值为1 那a不会再初始化阶段在进行赋值。
public static int a = 1;
}
04 使用
-
java程序对类的使用方式分为两种:
-
主动使用
- 创建类的实例 —> 通过类new一个实例对象
- 访问某个类/接口的静态变量,或者对改类的静态变量进行赋值
- 调用类的静态方法
- 反射调用
- 初始化一个类的子类也是对该类进行的主动使用
- java虚拟机启动时被表明为启动类(main/test)
- jdk1.7开始提供了动态语言支持:java.lang.invoke.MethodHandle实例的解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化。
-
被动使用
- 除去主动使用,剩余的使用方式就是被动使用,被动使用是不会导致类的初始化。只是进行初始化,但是初始化之前的动作可能会进行,也可能不会进行。
-
-
所有的java虚拟机实现必须在每个类/接口被java程序首次主动使用才会初始化他们。
- 首次主动使用:第一次主动使用的方式才会导致类/接口的初始化,被动使用或者不使用就不会被初始化。而且初始化的动作只会执行一次。就是在首次使用的时候。
05 卸载
- 就是将类从内存中移除,平常日常开发的过程中,我们很少会遇到。
java虚拟机与程序的生命周期
- 在如下的几种情况下,java虚拟机讲结束生命周期
- 执行了System.exit()方法。
- 程序正常执行结束。
- 程序在执行的过程中遇到了异常或错误而导致的异常结束。
- 由于操作系统出现错误而导致java虚拟机进程终止。
Coding
- 主动使用的案例
public static void main(String[] args) {
/**
* 场景1:
* 通过子类调用父类的str静态变量,我们只需要关注str隶属于谁就是对谁的直接调用
* 对父类的来说是直接使用 父类就会被加载
* 输出结果为
* ---- Parent1 ----
* hello
*/
System.out.println(Child1.str);
/**
* 场景2:
* 直接调用子类的静态变量 对子类进行直接调用
* 子类就会被初始化 同时父类也会被初始化 而且父类要先行与子类进行初始化
* 输出结果为
* ---- Parent1 ----
* ---- Child1 --------
* hello
*/
System.out.println(Child1.str2);
}
}
class Parent1{
public static String str = "hello";
static {
//静态代码块在类的初始化过程会加载
System.out.println(" ---- Parent1 ----");
}
}
class Child1 extends Parent1{
public static String str2 = "hello";
static {
System.out.println("---- Child1 --------");
}
}
- 常量的本质以及相关助记符的讲解
/**
* 描述: 常量的本质含义 讲解01
* @author karl
* @create 2019-09-30 17:57
*/
public class MyTest01 {
public static void main(String[] args) {
/**
* 这个毫无疑问 直接调用了Parent1的静态变量str Parent1进行了初始化
* 控制台打印如下所示:
* ----- Parent1 ---------
* hello
*/
System.out.println(Parent1.str);
/**
* 调用了str2 str2本质是个常量
* 常量在编译阶段会存到调用这个常量所在类(即MyTest01这个类)的常量池中
* 本质上,调用类并没有直接引用到定义常量的类,而是直接调用本类常量池中的str2 所以Parent1不会被初始化静态代码块也不会被执行
* 执行效果如下所示:
* hello
*/
System.out.println(Parent1.str2);
}
}
class Parent1 {
public static String str = "hello";
public static final String str2 = "hello";
static {
System.out.println("----- Parent1 ---------");
}
}
通过字节码进行分析
Compiled from "MyTest01.java"
public class com.study.jvm.day02.MyTest01 {
public com.study.jvm.day02.MyTest01();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
//上述是MyTest01的无参构造方法的编译后的字节码,我们不做分析
//主要看下面的main方法的字节码
public static void main(java.lang.String[]);
Code:
// getstatic 助记符 获取静态熟悉/静态方法的
// 0 getstatic 获取System.out out方法是静态方法
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// 03 是获取Parent1.str 调用了Parent1类的静态变量str
3: getstatic #3 // Field com/study/jvm/day02/Parent1.str:Ljava/lang/String;
6: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
9: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// ldc 助记符 -> int、float或String型常量从常量池推送至栈顶 这里的str2 直接展示的就是hello
//并没有通过Parent1进行调用
12: ldc #6 // String hello
14: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
17: return
}
我们通过字节码的分析,也验证了我们案例的结论。
/**
* 描述: 常量的讲解2
* @author karl
* @create 2019-09-30 21:22
*/
public class MyTest01 {
public static void main(String[] args) {
/**
* 当一个常量的值在编译期间无法确定,那么就不会调用类的常量池中
* 这时在运行期间就会主动使用Parent,使用Parent就会被初始化,运行静态代码块的内容
* 输出结果如下所示:
*
* --- Parent -----
* 5070fd4e-ff69-4f2d-82cb-dfb18b04a886
*
*/
System.out.println(Parent.randomStr);
}
}
class Parent{
/**
* 编译期间是不知道具体的值,只有在运行期间才会知道
*/
public static final String randomStr = UUID.randomUUID().toString();
static {
System.out.println("--- Parent -----");
}
}
同时这个过程也可以通过字节码进行分析,在这里就不做演示了。
- 数组的理解
/**
* 描述: 数组的讲解
*
* @author karl
* @create 2019-09-30 21:35
*/
public class Mytest02 {
public static void main(String[] args) {
/**
* 对于数组的实例来说 其类似是由JVM在运行期动态生成的,动态代理一样 通过parent02s.getClass().getSuperclass()得知其父类是Object
* new 出来的是Parent02[]数组的实例而不是Parent02的实例,所以不会导致Parent02 的初始化
* 引用类的数组创建并不会导致类的初始化 所以Parent02的静态代码块不会被执行
* 输出: 无输出
*/
Parent02[] parent02s = new Parent02[1];
int[] ints = new int[1];
}
}
class Parent02{
static {
System.out.println("--- Parent02 ---");
}
}
- 如果文章有帮助到你欢迎关注微信公众号《后端学长》