实例详解 JVM 类加载机制

Javaer,技术爱好者,喜欢接触新技术。CSDN 博客专家。关注分布式服务架构,机器学习,爬虫等技术领域。

作为有理想有追求的程序员,对于 JVM 的了解和掌握是有必要的。然而在我们工作中一般并不会直接和 JVM 打交道,因此这块内容也显得较为陌生和神秘,像是空中楼阁。本文通过实例讲解的方式而不是泛泛的总结,将使你对 Java 类加载机制有相对深刻的理解和掌握。 本场 Chat 包含以下内容: 类加载相关陷阱面试题 类的生命周期 类加载器机制 类加载器命名空间 线程上下文类加载器与 SPI 本场 Chat 适合有基础编程能力,渴望了解和掌握 Java 类加载相关内容的读者。 当前内容版权归码字科技所有并授权显示,盗版必究。

文章正文

感谢各位订阅本次 Chat 。首先要说在开篇的话,本场 Chat 为了更好的说明和理解类加载器机制,使用了许多示例,希望大家从阅读本文开始就准备动手起来,创建一个简单的 Java 工程,跟随文章逐一将文中的示例代码自行运行并思考结果。 相信这也会给大家带来更多的收获。

类加载陷阱题

之前(大概还在学校没出来工作)遇到了一个挺有意思的题目,这可能是一道让许多人都会犯错的题,不瞒各位,我在第一次遇到到这个题目时也做错了,很容易掉进题目的陷阱,有的人可能已经见过,不管怎样一起来看看题目:

class SingleTon {
    private static SingleTon singleTon = new SingleTon();
    public static int count1;
    public static int count2 = 0;

    private SingleTon() {
        count1++;
        count2++;
    }

    public static SingleTon getInstance() {
        return singleTon;
    }
}

public class Test {
    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getInstance();
        System.out.println("count1=" + singleTon.count1);
        System.out.println("count2=" + singleTon.count2);
    }
}

看完题想想结果会是什么?可能有人会很轻松的回答出答案是 1 和 1,那恭喜你,你成功中了出题人所设下的陷阱,如果不是,那又是为什么呢?大家别急,我们再将其中的几行代码改变顺序如下:

    public static int count1;
    public static int count2 = 0;
    private static SingleTon singleTon = new SingleTon();

结果又不一样了,大家这里稍稍思考一下,为什么?原因就在于加载类时各个成员的加载顺序不同,这里其实涉及到类的加载初始化流程,我们暂且先不具体解释原因,我们来先介绍类的生命周期相关内容,对类的生命周期有了一定了解,理解这道题就很容易了。

类的生命周期

我们都知道,当我们编写一个 java 源文件后,经过编译会生成一个后缀名为 class 的字节码文件,只有这种字节码文件才能被 Java 虚拟机执行,java 类的生命周期就是指一个 class 文件从被 JVM 加载到卸载的全过程。

一个 java 类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五大阶段。当 Java 程序中需要使用某个类时,Java 虚拟机首先要确保这个类已经被加载、连接和初始化,其中连接过程又包括验证、准备和解析这三个子步骤。这些步骤一般按照以下顺序执行,如图:

enter image description here

加载:查找并加载类的二进制数据,并利用字节码文件创建一个 Class 对象。

连接(分为三个步骤):

- 验证:确保被加载类的正确性,可被 JVM 执行。

- 准备:为类的静态变量分配内存,并将其初始化为**系统默认初始值**。

    系统默认初始值如下:

    1. Java 八种基本数据类型默认的初始值是 0 ;

    2. 引用类型默认的初始值是 null ;

    3. static final 修饰的常量为程序中设定的值,例如:static final int x=10;则默认就是 10。

- 解析:把类中的符号引用转换为直接引用。

初始化:主要完成对静态变量的初始化(为类的静态变量赋予用户定义的初始值),静态块执行等工作为。

类的初始化过程是按照顺序自上而下给静态变量赋值并执行静态语句,如果有父类,则首先按照顺序运行父类中静态语句。

现在我们可以回过头来,分析一下文章开头的题目:

  1. 首先SingleTon singleTon = SingleTon.getInstance(); 调用了 SingleTon 类的静态方法,触发类的初始化;

  2. 类加载的时候在准备过程中为类的静态变量分配内存并初始化系统默认值,也就是 singleton=null,count1=0,count2=0 ;

  3. 类初始化阶段,为类的静态变量赋予用户定义的默认值和执行静态代码快。singleton 赋值为 new SingleTon(),调用类的构造方法,构造方法调用后 count=1;count2=1 ;

  4. 继续为 count1 与 count2 赋值,此时 count1 没有赋值操作,所有 count1 为 1,但是 count2 执行赋值操作就变为 0。

对于初始化我们需要注意几点:

  • 当初始化子类的时候,该子类的所有父类都会先被初始化。

    public class Test1 { public static void main(String[] args) { new Teacher(); } } class Person { static { System.out.println("Person static block"); } } class Teacher extends Person { static { System.out.println("Teacher static block"); } }

打印结果如下

Person static block
Teacher static block

Person 的静态代码块首先执行了,通过生成 Teacher 类的实力触发 Teacher 类的初始化,Teacher 类初始化时,首先初始化了它的父类 Person。

  • 引用类的静态变量,不会导致子类初始化(这属于下面将要讲到的被动使用的一种)。

    public class Test2 { public static void main(String[] args) { System.out.println(Teacher.str); } } class Person { public static String str = "hello world!"; static { System.out.println("Person static block"); } } class Teacher extends Person { static { System.out.println("Teacher static block"); } }

打印结果:

Person static block
hello world!

程序中我们访问了 str 变量,但并没有打印“Teacher static block”,说明 Teacher 类并没有被初始化。这里如果将 str 定义为 final 类型,结果只会打印一个“hello world!”,这是为什么?

  • ClassLoader 类的 loadClass() 与 Class.forName() 方法区别

这两个方法都可以用来加载目标类,但是调用 ClassLoader 类的 loadClass() 方法加载一个类,该方法只是加载该类,并不是对类的主动使用(什么是主动使用,后续马上就会介绍),不导致类的初始化。使用Class.forName()静 态方法会导致类的初始化。

forName 还提供了多参数版本,可以指定使用哪个 ClassLoader 来加载

public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)

其中第一个参数和以前一样,第二个参数 initialize 表示加载时是否需要初始化,对于只有一个参数的 forName 方法来说,这个参数默认为 true,第三个参数可以指定类加载器,通过这种形式的 forName 方法,可以使用自定类加载器使得我们可以自由加载其它任意来源的类库。

通过一个例子来感受下:

public class LoadTest {
    static {
        System.out.println("hello static");
    }
}

public class Test3 {

    public static void main(String[] args) throws ClassNotFoundException {
        // 初始化,
        Class.forName("com.chm.jvm.LoadTest");
        // 不会
                        
隐藏内容 支付可见
购买文章 ¥9.9
订阅频道首月仅需 12 元/月,预计可省 1288 元
¥9.9
¥9.9购买
订阅频道免费读
× 订阅 Java 精选频道
首次订阅 ¥ 元/月 15元/月
订阅即可免费阅读所有精选内容