JVM上分笔记 - 类的加载、连接与初始化(一)

关于JVM,不得不说在学习的过程中感觉有些枯燥。不同于学习Android、Flutter,有了一点基础之后便可以在手机上看到自己写的小程序;学习Python,有了一些基础之后便可以写简单的接口供前端调用;学习机器学习,在掌握一个算法之后,便可以拿些数据集进行把玩,去kaggle上找找刺激;学习经典算法与数据结构,打打leetcode周赛也是其乐无穷。上面举得这几个例子并不是说它们就好学,其实每一门知识深入下去都是需要付出大量的时间与精力,这里举这几个例子的意思是,它们都体现了编程中一个很快乐的点,实时反馈,一段代码带给我们的预期,run一下就可以看到,而JVM更多的时候是无法具有这个特点的,并且在日常编码中,不少程序猿是不会直接去涉及到JVM,这也产生了一个错觉,JVM不重要,其实这种认知是不正确的,在笔者看来,JVM更像是一种内功,有了它你会更好的理解与运用招式。

JVM涉及到的知识点十分的广泛,此系列博客也只是和大家一起分享其中某些常用的知识点,以及和大家一起探索揭秘JVM的更好的方法。

第一篇博客,和大家一起分享类的加载、连接与初始化。

首先,先来看看类的加载、连接与初始化分别都做了什么事情。

一、加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存中创建一个Java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在了方法区中)用来封装类在方法区内的数据结构。

加载.class文件的方式

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件

二、连接

这个过程中主要做了三件事:

  • 验证:   确保被加载的类的正确性
  • 准备:为类的静态变量分配内存,并将其初始化为默认值。
  • 解析:把类中的符号引用转换为直接引用

三、初始化

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

首次主动使用的七种方式,除了这七种方式,其他的方式都可以看作被动使用,不会初始化

  • 创建类的实例
  • 访问某个类或者接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射
  • 初始化一个类的子类
  • Java虚拟机启动时被表明为启动类的类 (main方法)
  • JDK1.7开始提供动态语言的支持

看过这些概念之后,有个映像就可以,下面通过一些例子,来加深对于概念的理解,在编写了例子之后,在去回头理解概念。

Test1

public class Test1 {
    public static void main(String[] args) {
        System.out.println(Child1.str);
    }
}

class Parent1 {
    public static String str = "hello jvm";

    static {
        System.out.println("Parent1 static block");
    }
}

class Child1 extends Parent1{
    public static String str = "child1";  // 1

    static {
        System.out.println("Child1 static block");
    }
}

注意注释1处的代码,首先我们屏蔽注释1处的代码,运行程序,可以看到Child1中的静态代码块并没有执行,说明Child1没有初始化,然后打开注释1处的代码,则Child1与Parent1处的静态代码块都会执行,说明这两个类都进行了初始化。

对于静态字段来说,只有调用定义了该字段的类才会被初始化,当一个类在初始化时,要求其父类全部初始化完毕。

我们可以使用jvm命令来追踪类的加载信息并打印,通过-XX:+TraceClassLoading

 加了这个命令之后,会看到很多的类信息

开始的都是jdk里的Object等这些大名鼎鼎的类,我们关注下面自己定义的类

后面还有三个小例子,如果你能看到这里,证明你对这篇文章的内容还是感兴趣的,下面的三个小例子,会先给出程序,小伙伴们看到每个程序后不要往下翻,先自己想想这个程序会输出什么,然后在来一起分析为什么。

Test2

public class Test2 {
    public static void main(String[] args) {
        System.out.println(Parent2.str);
    }
}

class Parent2 {
    public static final String str = "Hello Jvm";

    static {
        System.out.println("Parent2 static black");
    }
}

 

 

------------我是占位图------------

 

 

 

程序的运行结果是“Hello Jvm”,Parent2中的静态代码块并没有输出。

因为常量在编译阶段会存入到调用这个常量的方法所在的类中的常量池中,本质上调用类并没有直接引用到定义常量的类,所以不会触发定义常量的类的初始化。

本例中,是将常量存放到了MyTest2的常量池中,之后MyTest2与MyParent2就没有任何关系了,甚至我们可以将MyParent2的class文件删除。

在out文件下找到MyParent2的class文件,将其删除,然后重新运行代码,结果依旧。

 

Test3

public class Test3 {
    public static void main(String[] args) {
        System.out.println(Parent3.str);
    }
}

class Parent3 {
    public static final String str = UUID.randomUUID().toString();

    static {
        System.out.println("Parent3 static block");
    }
}

 

 

 

------------我是占位图------------

 

 

 

 

 

程序运行的结果是

Parent3 static block
323588ec-149b-4ef0-90e0-23434c29389f

第二行随机生成的UUID不用去管它,主要是第一行的输出,Parent3的静态代码块执行了,说明Parent3初始化了。

和Test2不同的是,Test2里的常量的值,在编译时就已经确定了,而在Test3中,当一个常量的值并非编译期间可以确定的,那么其值就不会被放到调用类的常量池中,这时程序运行时,会导致主动使用这个类,便会初始化此类。

 

Test4

public class Test4 {
    public static void main(String[] args) {
        Parent4 parent4 = new Parent4(); //1

        Parent4[] parent4 = new Parent4[1]; //2
    }
}

class Parent4 {
    static {
        System.out.println("Parent4 static block");
    }
}

 在main函数中有注释1和注释2,屏蔽注释2看看程序运行的结果,再屏蔽注释1打开注释2看看程序执行的结果。

 

 

 

 

------------我是占位图------------

 

 

 

 

 

 

在屏蔽注释2时,很显然会初始化Parent4,而在屏蔽注释1打开注释2时,发现Parent4并没有被初始话,是因为对于数据来说,其类型是由jvm在运行时动态生成的,动态生成的类型,其父类就是object。

本篇文章介绍了类的加载、连接与初始化的基本概念以及4个小例子,小例子旨在突出初始化时类与类之间的关系,下篇文章将和大家一起看看初始化过程中类与接口的区别以及更加深入的理解类连接中的准备过程和初始化过程。

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值