类的加载过程及注意点

  类的加载过程主要分为三个阶段:加载、链接(验证,准备,解析)、初始化。
  网上有很多关于这一块的介绍和概念,但是要么不准确,要么就不够具体。如果单从概念上看是很难理解的,本文更多的是解释每个步骤的相关概念以加深同学们的理解。
  整体过程如下:

image

  先说一个java的命令,方便下面反编译看字节码文件:javap -v XXX.class,不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用到的常量池等信息。(也可以通过IDEA装插件的形式看,搜jclasslib)
  通过反编译看到的class文件中的常量池,加载到内存后叫运行时常量池。

一.加载

先看概念:

image

关于第一点需要补充一下:
image

关于第二点方法区具体的实现要看jdk版本(jdk7-永久代,jdk8-元空间)。
另外还
\color{red}{需要知道生成大的class实例是在加载这个过程中出现的。}

二.链接

链接又分为三个步骤:验证、准备、解析。还是先看概念,然后逐点解释:


image

2.1 验证

  目的是防止恶意修改或攻击。
  举个例子就是:能够被java虚拟机识别的字节码文件都有一个专有标示,它的十六进制格式开头都有“CAFEBABE”,被称为Java class文件的魔数。
  额外说明:十六进制(简写为hex或下标16)在数学中是一种逢16进1的进位制。只能用数字0到9和字母A到F(或af)表示,其中:AF表示10~15,这些称作十六进制数字。例如123123->1e0f3。
  再额外说明:在计算机领域,魔数有两个含义,一指用来判断文件类型的魔数;二指程序代码中的魔数,也称魔法值。所谓魔法值,是指在代码中直接出现的数值,只有在这个数值记述的那部分代码中才能明确了解其含义。例如阿里巴巴代码规约就会提示。
  为什么是CAFEBABE呢?
  网上猜测是Java一直以咖啡为代言,CAFEBABE可以认为是 Cafe Babe,读音上和Cafe Baby很近。所以这个也许就是代表Cafe Baby的意思。

2.2 准备

主要是设置类变量的默认初始值,例如:

public class A {
    private static int a = 1;//准备阶段:a=0;----->初始化阶段a=1。
    public static void main(String[] args) {
        System.out.println(a);
    }
}

数据类型不同,初始值不同。
final修饰的static变量编译的时候就分配了,准备阶段会显示初始化。
不会为实例变量分配初始化。

2.3 解析

主要是符号引用转直接引用的过程,了解即可。

三.初始化

还是先看概念,然后逐点解释:


image

注:clinit=class init
clinit方法不是我们自己定义的,通过反编译可以看到:


image

如何理解“顺序执行”呢?先看代码:

public class A {
    private static int a = 1;//准备阶段:a=0;----->初始化阶段a=1。
    static {
        a = 2;
        b = 20;
    }
    private static int b = 10;//为什么可以先赋值再定义?链接之准备阶段:b=0--> 初始化:20-->10
    public static void main(String[] args) {
        System.out.println(a);//2
        System.out.println(b);//10
    }
}

下图为上面代码反编译后的字节码文件:

image

从字节码文件可以看出是顺序执行的。
如果代码里没有静态变量或者静态代码块,编译就不会有clinit方法。比如下图中的情况:
image

所以可以看出类构造器方法clinit就是针对类变量的赋值和静态代码块的,如果没有就不会生成clinit方法。
还有个init方法其实就是类的构造器。
另外,需要注意的是下面的写法:
image

前向引用可参考oracle官方文档:
前向引用官方解释
<clinit>概念中还提到:方法在多线程只会执行一次又是什么意思呢?看下面的例子:

public class A {
  public static void main(String[] args) {
      Runnable runnable = () -> {
          System.out.println(Thread.currentThread().getName() + "开始");
          DeadThread deadThread = new DeadThread();
          System.out.println(Thread.currentThread().getName() + "开始");
      };
      Thread t1 = new Thread(runnable, "线程1");
      Thread t2 = new Thread(runnable, "线程2");
      t1.start();
      t2.start();
  }
}
class DeadThread {
  static {
      if (true) {
          System.out.println(Thread.currentThread().getName() + "初始化当前类");
          while (true) {
          }
      }
  }
}

输出如下:

线程2开始
线程1开始
线程2初始化当前类









部分代码和截图参考自B站视频。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容