Java类加载机制,类加载器的学习

  一直想理解Java程序是如何运行的。java的一个类从.java文件转变成.class文件,之后.class文件在Java虚拟机JVM中的加载,连接,初始化的过程。之前学习的一直都是零零星星的知识点,不太完整。今天阅读了几篇写的很不错的文章,结合之前看书对JVM的基本知识的了解,终于对Java类加载机制,类加载器有了深刻的理解,总结一下,便于后面查看。

以一个经典的例子来学习JVM类加载机制再好不过了,例子是一个单例的创建,单例类如下。

public class Singleton {
    private static Singleton singleton = new Singleton();
    public static int counter1;
    public static int counter2= 0;
    private Singleton() {
        counter1++;
        counter2++;
    }
    public static Singleton getSingleton() {
        return singleton;
    }
}

测试代码:

public class TestSingleton {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getSingleton();
        System.out.println(singleton.counter1);
        System.out.println(singleton.counter2);
    }
}

如果比较了解JVM类加载机制的小伙伴就很容易知道这个输出结果,不了解的也没关系,这篇文章带你了解JVM类加载机制,这边先不上结果,先说一下类加载的过程。

一. JVM类加载机制

  类加载机制分五个步骤,加载、验证、准备、解析和初始化。

1. 加载

  JVM 在加载阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中,并在堆上生成一个代表该类的 java.lang.Class 对象,提供访问方法区数据的一个入口。

2. 验证

  验证阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  1.文件格式验证:如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等。
此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java类型信息的要求。

  2.元数据验证:是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否与父类产生矛盾等。

  3.字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上。

  4.符号引用验证:在解析阶段中发生,保证可以将符号引用转化为直接引用。

3. 准备

  准备阶段主要为类变量分配内存并设置初始值。这些内存都在方法区分配。在这个阶段我们只需要注意两点就好了,也就是类变量和初始值两个关键词:

  1.类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中,

  2.这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值。比如 public static int value = 1; 在这里准备阶段过后的value值为0,而不是1。赋值为1的动作在初始化阶段。

4. 解析

  解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。什么是符号引用和直接引用呢?

  符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好。编译时期A类引用了B类,B就是符号引用。

  直接引用:通过对符号引用进行解析,找到引用的实际内存地址。

5. 初始化

  该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。

二. 类加载的时机

对于初始化阶段,虚拟机规范规定了有以下情况必须立即对类进行“初始化”:

  1.使用 new 实例化对象、访问一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法。

  2.对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

  3.当初始化类的父类还没有进行过初始化,则需要先触发其父类的初始化。(而一个接口在初始化时,并不要求其父接口全部都完成了初始化)

  4.虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。

三. 类的初始化步骤

  类的初始化分为两种,第一种是没有父类,第二种有父类。

  第一种没有父类:

1.类的静态属性和静态代码块(按照编码顺序)
2.类的非静态属性和非静态代码块(按照编码顺序)
3.类的构造方法

  第二种有父类:

1.父类的静态属性和静态代码块
2.子类的静态属性和静态代码块
3.父类的非静态属性和非静态代码块
4.父类的构造方法
5.子类的非静态属性和非静态代码块6.子类构造方法

 

四. 类加载器

 1.启动类加载器(Bootstrap ClassLoader)

  负责加载 JAVA_HOMElib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。

2.扩展类加载器(Extension ClassLoader)

  负责加载 JAVA_HOMElibext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
3.应用程序类加载器(Application ClassLoader)

  负责加载用户路径(classpath)上的类库。JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader实现自定义的类加载器。

五. 双亲委派

  当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
  采用双亲委派的好处是避免重复加载 + 避免核心类篡改,避免重复加载很好理解,通过双亲委派机制询问上层加载器是否加载此类,如果加载过了就不用再重复加载了

避免核心类篡改,比如java.lang.String类,如果你自己定义了java.lang.String类是不会被JVM加载的,保护Java的内部核心的API不被篡改。

  下图是自定义类加载器的加载一个类的过程。

六. 问题的答案

刚开始的问题的答案为:

1
0

分析:

1.Singleton singleton = Singleton.getSingleton();这行代码我们可以知道Singleton类的静态方法被访问了,所以要对Singleton类进行加载、验证、准备、解析、初始化。
2.在准备阶段,静态属性分别赋予默认的值。
private static Singleton singleton = null
public static int counter1 = 0
public static int counter2 = 0
3.在初始化阶段,将初始化类变量的值,singleton = new Singleton();
这会执行 Singleton的构造方法,此时counter1 = 1; counter2 = 14.counter1没有初始化的值,所以此时counter1=1;counter2设为0,所以此时counter2 = 0;解答完毕。

参考资料:

https://www.jianshu.com/p/b6547abd0706

https://www.jianshu.com/p/8c8d6cba1f8e

https://www.cnblogs.com/czwbig/p/11127222.html

https://blog.csdn.net/u010312474/article/details/91046318

https://baijiahao.baidu.com/s?id=1636309817155065432&wfr=spider&for=pc

原文地址:https://www.cnblogs.com/peter_zhang/p/jvm_classload.html