【JVM虚拟机】类的加载,连接和初始化

jdk版本:Java HotSpot™ 64-Bit Server VM (build 25.131-b11, mixed mode)

JVM虚拟机的学习可能更偏向于理论,但是当你理解了JVM虚拟机后,你将会写出更好的代码,性能更好的代码。

今天先介绍JVM对类的处理,总共分为加载,连接,初始化,下面我们慢慢介绍

首先,我们先看java虚拟机的退出情况

java的虚拟机和程序的生命周期

在如下的情况下,java虚拟机会结束生命周期

  • 执行了System.exit()方法
  • 程序正常结束
  • 程序在执行中遇到异常或者错误而异常终止
  • 由于操作系统出现错误,导致java虚拟机退出

下面我们来详细了解下java虚拟机中类的加载过程

加载

描述:
类的加载是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法内,然后在内存中保存一个java.lang.Class对象,用来封装类在方法区内的数据结构

可以多种类型文件:

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从数据库中提取.class文件
  • 将java源代码编译成.class二进制文件

连接

  • 验证 :确保被加载类的正确性
  • 准备 :为类的静态变量分配内存,并把它初始化为默认值
    • java中一些变量有一些默认值:
      • int = 0
	//默认是把i赋值为0
	public final static int i = 0;
  • 解析 :把类中的符号引用转换成直接引用
    • 符号表示:间接引用
    • 直接引用:直接指针指向某个类的方法

初始化

把类的静态变量赋值为正确的初始值

	//默认是把i赋值为0
	public final static int i = 2;

注意:所有的JAVA虚拟机实现必须在每个类或者接口被java程序首次主动使用时才会初始化他们

使用

正常使用类 ,比如 new String();

java程序对类的使用分为2种

  • 主动使用 (7种)
    • 创建类的实例
    public class Test1 {
    
        public static void main(String[] args) {
            //这里创建实例
            MyParent myParent = new MyParent();
            System.out.println(myParent);
        }
    
        public static class MyParent {}
    }
    
    • 访问某个类或者接口的静态变量 ,或者对该静态变量进行赋值 这里其实并不会去加载MyParent,这里留了个坑哦???,详细请看下一章
    public class Test1 {
    
        public static void main(String[] args) {
        	//调用静态变量
            System.out.println(MyParent.str);
        }
    
        public static class MyParent {
    
            public static final String str = "123";
    
            public MyParent() {
                System.out.println("myParent init");
            }
        }
    }
    
    • 调用类的静态方法
    • 反射
    • 初始化一个类的子类
    public class Test1 {
    
        public static void main(String[] args) {
            MyChild myChild = new MyChild();
        }
    
        public static class MyParent {
        
            public MyParent() {
                System.out.println("myParent init");
            }
        }
    
        public static class MyChild extends MyParent{
    
            public MyChild() {
                System.out.println("MyChild init");
            }
        }
    }
    
    • java虚拟机启动时,被标记为启动类的类 如:Java Test
    • Jdk1.7 开始提供的动态语言支持
  • 被动使用
    除了主动使用,其他都是被动使用

卸载

把加载到java虚拟机中的class类删除掉

下面3种情况会导致类的卸载

  • 类加载器没有被引用

  • 类对象没有被引用

  • 没有该类的实例对象存在

扩充:

JVM的参数介绍

-XX:+TraceClassLoading : 配置参数,可以显示类的加载过程
【JVM虚拟机】类的加载,连接和初始化_第1张图片
-XX:+TraceClassUnloading : 配置参数,可以显示类的卸载的过程

参数形式的解析:
-XX :规定的
+:说明是开启的参数
-:说明是关闭的参数

助记符

  1. iconst_* 基础类型(int) -1~5
    通过iconst_*指令把常量压到栈顶
指令
iconst_m1 -1
iconst_0 0
iconst_1 1
iconst_2 2
iconst_3 3
iconst_4 4
iconst_5 5

【JVM虚拟机】类的加载,连接和初始化_第2张图片

  1. bipush 基础类型(int) -128~127
    通过bipush指令把常量压到栈顶
    【JVM虚拟机】类的加载,连接和初始化_第3张图片

  2. sipush 基础类型(int) -32768~32767
    通过sipush指令把常量压到栈顶
    【JVM虚拟机】类的加载,连接和初始化_第4张图片

  3. ldc 基础类型(int) -2147483648~2147483647
    通过ldc指令把常量压到栈顶

截图中看到,超过这个值就已经报错了
【JVM虚拟机】类的加载,连接和初始化_第5张图片

  1. getstatic static关键字
    【JVM虚拟机】类的加载,连接和初始化_第6张图片
    【JVM虚拟机】类的加载,连接和初始化_第7张图片

加强理解

理解链接中的准备阶段和初始化阶段

首先我们先准备个例子。

public class Test2 {

    public static void main(String[] args) {
        MyParent instance = MyParent.getInstance();
        System.out.println(instance.num1);
        System.out.println(instance.num2);
    }
}

class MyParent {

    public static  int num1;

    public static  int num2 = 0;

    private static MyParent myParent = new MyParent();

    private MyParent() {
        num1++;
        num2++;
    }

    public static MyParent getInstance() {
        return myParent;
    }
}

这个就是一个跟简单的单例模式。运行后输出为

1
1

然后我们把这个例子里面的位置改变下

public class Test2 {

    public static void main(String[] args) {
        MyParent instance = MyParent.getInstance();
        System.out.println(instance.num1);
        System.out.println(instance.num2);
    }
}

class MyParent {

    public static int num1;

    private static MyParent myParent = new MyParent();

    private MyParent() {
        num1++;
        num2++;
    }
	//把num2迁移到这里
    public static int num2 = 0;

    public static MyParent getInstance() {
        return myParent;
    }

}

我们再执行,发现变成了

1
0

咦,这是为什么呢????

我们根据连接中的3个步骤来看下

  • 验证阶段,验证MyParent这个类是否是否能正确被加载
  • 准备阶段呢? 为类的静态变量分配内存,并且设置为默认值,问题就在这。。
    • jvm把num1 设置为了0
    • jvm把myParent 设置为了Null
    • jvm把num2 设置为了0
  • 解析阶段,就是把类之间的引用关系给转换成直接引用
    到这里 连接步骤已经完成了。

连接执行完了,就开始初始化了(jvm是按照类中的静态变量的顺序进行一个个初始化的)

  • 把num1设置了0,因为是默认值嘛
  • 把myParent 创建实例的时候,就会调用MyParent() 构造方法
    • MyParent() 构造方法中
      • 把num1 和num2进行+1
    • 这个时候 num1 = 1 , num2 = 1
  • 继续把num2 设置为0
    到这里,初始化的步骤以及完成了。
    所以说最后打印的结果就是num1=1,num2=0

为了验证是否是和想的一样,我们MyParent() 构造函数打印出num1和num2

 private MyParent() {
        num1++;
        num2++;
        System.out.println("myParent init "+num1);
        System.out.println("myParent init "+num2);
}

继续执行。

myParent init 1
myParent init 1
1
0

会发现在构造函数里面打印1和1。

相信看到这里,小伙伴对连接和初始化阶段已经很明白了吧。

你可能感兴趣的:(JVM虚拟机,JVM虚拟机)