JVM深入理解(2)——类加载子系统(1)

接上篇文章JVM深入理解(1)——概述

写在最前,本篇文章大部分来源于b站尚硅谷JVM全套教程的提炼,并附带自己的理解。主要是为了帮助自己理解,和用于复习。如果同时还能对其他人有所裨益,那就更好不过了。如果有谬误的地方,还请不吝指出。

类加载器子系统作用

  • 类加载器子系统负责从文件系统或网络中加载Class文件,class文件在文件开头有特定的文件标识。
  • ClassLoader只负责class文件加载,不关心是否能运行。(由执行引擎判断)
  • 加载的类信息存放在方法区的内存空间,方法区还会存放运行时常量池信息。

ClassLoader角色

在这里插入图片描述

  1. class文件存放在本地硬盘上,在执行时,要加载到JVM中。
  2. class文件在JVM中被称为DNA元数据模板,放在方法区。
  3. ClassLoader是一个运输工具,它使得class文件在JVM中变成元数据模板(也就是Class类对象)。
注:
一个实例可以通过getClass()得到其类对象,类对象对同一个类是唯一的。
一个类对象可以通过getClassLoader()获得其类加载器,类加载器不止一种(后文解释)。

类加载过程

阶段一:加载 Loading

  1. 通过类的全限定名称获取此类的二进制字节流
  2. 字节流代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个类对象,作为方法区这个类的各种数据的访问入口。

注意:class文件并不一定是实际存在于硬盘上的文件,它可以动态生成,也可以从 网络中获取。

阶段二:链接 Linking

验证
  • 确保class文件字节流符合虚拟机要求,并确保安全性。
准备
  • 类变量(static变量)分配内存,并设置类变量的默认初始值。
  • 不包含final修饰的类变量,因为final编译时就已经分配了,准备阶段会显式初始化。
  • 不会为实例变量分配初始化,类变量分配在方法区中,实例变量随对象分配至java堆中。
解析
  • 将常量池内的符号引用转为直接引用的过程。
  • 事实上,解析往往会伴随着JVM执行初始化之后再执行
  • 符号引用:一组符号描述引用的目标。直接引用:直接指向目标的指针、相对偏移量或间接定位到目标的句柄。

阶段三:初始化*

  • 初始化阶段即执行类构造器方法<clinit>()的过程
  • 此方法不需定义,javac编译器自动收集类变量的赋值动作和静态代码块中的语句合并而来。(1)
  • 指令按语句在源文件出现的顺序执行 (2)
  • 此方法不同于类的构造器(虚拟机中的<init>())(3)
  • 若该类有父类,JVM保证子类的<clinit>()执行前,父类已经执行完毕。
  • 虚拟机必须保证一个类的<clinit>()在多线程下被同步加锁,这是由于一个类只会被加载到内存一次,被缓存起来。也就是说<clinit>()同样也只会被调用一次。(4)

后文一一解释这几个规则。

我们可以通过在IDEA–>setting–>Plugins 搜索查找jclass来查看我们的字节码文件。
在这里插入图片描述
(1):
我们编译下面这段代码,并通过view–>Show ByteCode with Jclasslib来查看。

public class initTest {
    private static int num=1; //在准备阶段num已经被赋值为0了
    static{
    		num=2;
    }

    public static void main(String[] args) {
        System.out.println(initTest.num);
    }
}

在这里插入图片描述
我们先注释掉static块
字节码:

0 iconst_1
1 putstatic #3 <Test/JVM/clsLoader/initTest.num : I>
4 return

然后加上static块(记得重新编译):

0 iconst_1
1 putstatic #3 <Test/JVM/clsLoader/initTest.num : I>
4 iconst_2
5 putstatic #3 <Test/JVM/clsLoader/initTest.num : I>
8 return

所以验证了我们前面所说<clinit>()方法由类变量赋值静态代码块合并而来。

(2):
下面根据以上所学的知识进行一个判断题:

public class initTest {
    static{
    		num=20;
    }
		private static int num=10;
}

这段代码能够顺利执行么?
将代码编译执行,果然没报错,为什么?
可能的解答是:对于类变量的初始化先开始,然后再执行静态代码块。
如果按这样理解,下面这段代码同样不会出错

public class initTest {
    static{
    		num=20;
    		System.out.println(num);
    }
		private static int num=10;
}

事实并非如此,以上代码会爆出非法前向引用(illegal forward reference)的错误。
实际上,第一段代码之所以顺利执行,是因为num变量在类加载过程的准备中已经被初始化为了0。根据上文所述:

指令按语句在源文件出现的顺序执行

实际上是先执行了num=20, 然后再执行num=10
通过字节码来check一下:

 0 bipush 20
 2 putstatic #2 <Test/JVM/clsLoader/initTest.num : I>
 5 bipush 10
 7 putstatic #2 <Test/JVM/clsLoader/initTest.num : I>
10 return

是不是非常神奇?

(3):
<clinit>()方法仅会在类中有类变量时,才会出现。
而<\init>()方法对于每个类都会存在,代表着类的构造函数。

(4):
我们来看这样一段代码:

public class initTest2 {
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            System.out.println(Thread.currentThread().getName()+": 开始");
            DeadThread deadThread = new DeadThread();
            System.out.println(Thread.currentThread().getName()+": 结束");
        });
        Thread t2=new Thread(()->{
            System.out.println(Thread.currentThread().getName()+": 开始");
            DeadThread deadThread = new DeadThread();
            System.out.println(Thread.currentThread().getName()+": 结束");
        });
        t1.start();
        t2.start();
    }

}

class DeadThread{
    static {
        if(true){
            System.out.println(Thread.currentThread().getName()+" 进入dead thread");
            while(true){
            }
        }
    }
}

如果DeadThread的<clinit>()方法不被同步保护,我们看到两个线程均输出*“进入dead thread”*语句。
如果不通过同步保护,而仅通过检查再执行(check-then-act)式来判断是否方法仅被调用了一次,那么当线程A卡在while循环时,线程B可能认为<clinit>()方法并未执行完成也调用此方法。(或者线程A、B同时进入此方法)从而不能保证其仅被调用一次。通过加锁(阻塞同步)可以保证,一次只能有一个线程进入此方法。

Thread-0: 开始
Thread-1: 开始
Thread-0 进入dead thread
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值