读Java虚拟机类加载引发的血案

雾凇接口
• 阅读 123

/ 前言 /

最近在看 Java 虚拟机类加载的知识点,结果让我发现了自己一个曾经一直纠结,又没彻底弄懂的类加载黑洞,从而引发下面一系列的测试血案。

相信面试过的你们也会见过类似下面测试的这几道题。不过,答案你真的理解了么?话不多说,直接 GKD。可惜我不是大佬,所以...哈哈哈 GKD 吧!下面就是测试过程种发现的一些疑惑点,赶紧记录一波...

/ 正文 /

测试开始,先思考下下面代码输出什么:

class Singleton {

    public Singleton() {
        System.out.println("Singleton new instance");
    }

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

    {
        System.out.println("Singleton  block !!!");
    }

}

public class NewTest {
    public static void main(String args[]){
        Singleton singleton = new Singleton();
    }
}

输出结果:

Singleton static block
Singleton  block !!!
Singleton new instance

当然,大佬们应该都能知道答案...毕竟,新手入门级的野怪,谁都打得过。这个对我这小菜鸡也算还比较容易理解;加载连接过程,没有需要处理的 static。new Singleton() 直接开始类的初始化了,所以输出直接按照类的初始化顺序来就好了

类的初始化的执行顺序

没有父类的情况:

类的静态属性
类的静态代码块
类的非静态属性
类的非静态代码块
构造方法

有父类的情况:

父类的静态属性
父类的静态代码块
子类的静态属性
子类的静态代码块
父类的非静态属性
父类的非静态代码块
父类构造方法
子类非静态属性
子类非静态代码块
子类构造方法

这里有个小误区,是我自己的误区~~比如下面这个例子:

class ParentSingleton{

public static int value = 100;

public ParentSingleton(){
    System.out.println("ParentSingleton new instance");
}

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

{
    System.out.println("ParentSingleton  block !!! ");
}

}

当要初始化上面这个类的时候,会输出什么?

如果这时候,我们只看上面的初始化顺序,会觉得这样输出,根据顺序来嘛~

ParentSingleton static block
ParentSingleton  block !!!
ParentSingleton new instance
???

OMG,错了,这里的顺序不是说,只要初始化,就要全部按照顺序一一执行...不是这样的。实际上只会输出:

ParentSingleton static block

如果有创建这个类的实例,比如 new ParentSingleton(),才会:

ParentSingleton block !!!
ParentSingleton new instance

是的,这里的误区,我曾经一度搞错了...尴尬。那再看这个测试:

class Singleton {
private static Singleton singleton = new Singleton();

private Singleton() {
    System.out.println("Singleton new instance");
}

public static void forTest() {

}

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

{
    System.out.println("Singleton  block !!! ");
}

}

public class TestSingleton {
public static void main(String args[]){
    Singleton.forTest();
}
}

看完资料的我,逐渐膨胀,毕竟100多斤的胖子,我想的输出应该是:

Singleton static block
Singleton  block !!!
Singleton new instance

然后运行一看,懵逼了,结果是:

Singleton  block !!! 
Singleton new instance
Singleton static block

咋回事啊,小老弟,结果乱套了...为什么不是先执行 static 代码块先了。认真想了一波,也不知道对不对,只能疯狂测试这样子...

经过一番测试,查看资料...最终...我觉得是这样子的。整个的流程详解应该是执行的第一步:Singleton.forTest();这时候,对Singleton类进行加载和连接,所以首先需要对它进行加载和连接操作。在连接-准备阶段,要讲给静态变量赋予默认初始值,这里还没到执行 forTest;初始值是 singleton = null。加载和连接完毕之后,再进行初始化工作:

private static Singleton singleton = new Singleton();

所以执行去到了 new Singleton(); 这里因为 new 会引起 Singleton 的初始化。需要执行 Singleton构造函数里面的内容。但是又因为非static初始化块,这里面的代码在创建java对象实例时执行,而且在构造器之前!!!!就是这东西...所以输出应该是:

Singleton  block !!! 
Singleton new instance

而根据类的初始化顺序,要执行 static 代码块,应该输出:

Singleton static block

完成初始化后。接下来就到真正调用 forTest 方法了,方法什么都不做,没输出。所以,总的答案就是:

Singleton  block !!! 
Singleton new instance
Singleton static block

这里最大的原因就是,连接加载的时候,要给属性初始化,而这里的初始化又刚好是 创建java 实例,需要执行构造,执行构造的前面又必须先执行 {} 大括号非 static 块。而不是和第一个测试例子那样,static 属性不需要初始化,所以....

IG 永不加班,但我需要哇,继续测试吧...继续测试验证:

class Singleton {
private static Singleton singleton = new Singleton();

private Singleton() {
    System.out.println("Singleton new instance");
}

public static Singleton getSingleton() {
    return new Singleton();
}

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

{
    System.out.println("Singleton  block !!! ");
}

}

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

输出结果如下所示。emm, 再次根据上面自己的理解,走一遍,应该是:

Singleton  block !!! 
Singleton new instance
Singleton static block
Singleton  block !!! 
Singleton new instance

这里后面第二次 new 为啥不引起第二次 类的初始化?? 因为一个类只能初始化一次啊!new 只是创建实例,不再初始化了。所以在调用 getSingleton 的时候,只创建实例就好了,而创建实例就是:

Singleton  block !!! 
Singleton new instance

在同一个类加载器下面只能初始化类一次,如果已经初始化了就不必要初始化了。为什么只初始化一次呢?类加载的最终结果就是在堆中存有唯一一个Class对象,我们通过Class对象找到的那个唯一的。噢?运行看一手,丢,对了..还有存在 final 的时候,和存在父类的时候,下面慢慢再测试验证....继续测试:

class Singleton extends ParentSingleton {

public Singleton() {
    System.out.println("Singleton new instance");
}

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

{
    System.out.println("Singleton  block !!! ");
}

}

class ParentSingleton{

public ParentSingleton(){
    System.out.println("ParentSingleton new instance");
}

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

{
    System.out.println("ParentSingleton  block !!! ");
}

}

public class TestSingleton {
public static void main(String args[]){
    Singleton singleton = new Singleton();
}
}

输出结果如下所示。这个,很明了,还是按照上面的类的初始化,有父类的情况按顺序调用,输出如下:

ParentSingleton static block
Singleton static block
ParentSingleton  block !!! 
ParentSingleton new instance
Singleton  block !!! 
Singleton new instance

继续测试如下所示。那个人,又来了...改成和上面没有父类一样的情况:

class Singleton extends ParentSingleton {
private static Singleton singleton = new Singleton();

private Singleton() {
    System.out.println("Singleton new instance");
}

public static Singleton getSingleton() {
    return singleton;
}

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

{
    System.out.println("Singleton  block !!! ");
}

}

class ParentSingleton{

public ParentSingleton(){
    System.out.println("ParentSingleton new instance");
}

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

{
    System.out.println("ParentSingleton  block !!! ");
}

}

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

输出结果如下所示。这里,就开始懵了...有点。先看结果:

ParentSingleton static block
ParentSingleton  block !!! 
ParentSingleton new instance
Singleton  block !!! 
Singleton new instance
Singleton static block

其实,很容易看清了,现在,再走一遍流程吧!执行到 Singleton.getSingleton() 时,先加载 Singleton ,这时因为 Singleton 有父类,需要需要加载父类先,加载父类 ParentSingleton,根据加载流程,在连接-准备阶段,要讲给静态变量赋予默认初始值,但父类没有 static 属性需要赋值初始化什么的,但是根据顺序,需要初始化static 代码块:

ParentSingleton static block

这时候回到子类的加载流程。根据连接-准备阶段,子类有需要处理的属性 private static Singleton singleton = new Singleton();赋值默认值先,singleton = null;然后初始化 singleton = new Singleton();根据上面的经验,这里是创建实例 ,并引起初始化,正常应该是:

Singleton  block !!! 
Singleton new instance
Singleton static block

但是,重点来了 !! 类实例创建过程:按照父子继承关系进行初始化,首先执行父类的初始化块部分。然后是父类的构造方法;再执行本类继承的子类的初始化块,最后是子类的构造方法,也就是:

ParentSingleton  block !!! 
ParentSingleton new instance

同时子类的初始化,因为初始化子类它有父类,所以需要先初始化父类(但是这里因为父类已经初始化了,就不再初始化了)。所以结果是:

ParentSingleton static block
ParentSingleton  block !!! 
ParentSingleton new instance
Singleton  block !!! 
Singleton new instance
Singleton static block

最终测试如下所示:

class Singleton extends ParentSingleton {
private static Singleton singleton = new Singleton();

private Singleton() {
    System.out.println("Singleton new instance");
}

public static Singleton getSingleton() {
    return singleton;
}

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

{
    System.out.println("Singleton  block !!! ");
}

}

class ParentSingleton{

private static ParentSingleton parentSingleton = new ParentSingleton();   

public ParentSingleton(){

    System.out.println("ParentSingleton new instance");
}

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

{
    System.out.println("ParentSingleton  block !!! ");
}

}

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

测试结果如下所示:

ParentSingleton  block !!! 
ParentSingleton new instance
ParentSingleton static block
ParentSingleton  block !!! 
ParentSingleton new instance
Singleton  block !!! 
Singleton new instance
Singleton static block

加载一个类时,先加载父类。按照先加载,创建实例,初始化,这个顺序就发现很通顺的写出答案了。哈哈哈哈哈,终于清楚了。所以一切的一切,都是创建实例这个东西。搞得我头晕。

部分特殊不引起类初始化记录,先记录下吧。

通过子类引用父类的静态字段,不会导致子类初始化,对于静态字段,只有直接定义这个字段的类才会被初始化
通过数组定义来引用类,不会触发此类的初始化
常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
public static final int x =6/3; 能够在编译时期确定的,叫做编译常量,不会引起类的初始化!!!
public static final int x =new Random().nextInt(100); 运行时才能确定下来的,叫做运行时常量,运行常量会引起类的初始化!!!

在虚拟机规范中使用了一个很强烈的限定语:“有且仅有”,这5种场景中的行为称为对类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。

5种必须初始化的场景如下

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有初始化,则需要先触发其初始化

这4条指令对应的的常见场景分别是:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

注:静态内容是跟类关联的而不是类的对象。

  1. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

注:反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法

对于任意一个对象,都能够调用它的任意一个方法和属性

这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制,这相对好理解为什么需要初始化类。

  1. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

注:子类执行构造函数前需先执行父类构造函数

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

注:main方法是程序的执行入口

  1. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化。则需要先触发其初始化。

注:JDK1.7的一种新增的反射机制,都是对类的一种动态操作

这回,以后看代码的时候,就不会再被这些执行加载顺序弄混了,对优化代码可能还是有帮助的吧。

再不说,也能再让我看到这些测试题,或者问我加载的过程,怎么也能处理回答个7788了吧。

可能其中个人理解有部分纰漏,还请大佬们指出~~蟹蟹鸭!

Android开发资料+面试架构资料 免费分享 点击链接 即可领取

《Android架构师必备学习资源免费领取(架构视频+面试专题文档+学习笔记)》

点赞
收藏
评论区
推荐文章
灯灯灯灯 灯灯灯灯
4年前
图文详解,史上最全【类加载子系统】解说!!
内存结构概述简图详细图英文版中文版注意:方法区只有HotSpot虚拟机有,J9,JRockit都没有如果自己想手写一个Java虚拟机的话,主要考虑哪些结构呢?1.类加载器2.执行引擎类加载器子系统类加载器子系统作用:1.类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。2.ClassLo
Wesley13 Wesley13
3年前
java类的加载与加载器
java代码在计算机中经历的三个阶段:1.Source源代码阶段(代码还是在硬盘上,并没有进入内存)  Student.java通过javac编译Student.class字节码文件2.类加载器ClassLoader将字节码文件加载进入内存,成为Class类对象(成员变量Field\\fields、构造方法Const
Wesley13 Wesley13
3年前
Java高级篇——深入浅出Java类加载机制
类加载器简单讲,类加载器ClassLoader的功能就是负责将class文件加载到jvm内存。类加载器分类从虚拟机层面讲分为两大类型的类加载器,一是BootstrapClassloader即启动类加载器(C实现),它是虚拟机的一部分,二是其他类型类加载器(JAVA实现),在虚拟机外部,并全部继
Stella981 Stella981
3年前
Jvm类的加载机制
1.概述虚拟机加载Class文件(二进制字节流)到内存,并对数据进行校验、转换解析和初始化,最终形成可被虚拟机直接使用的Java类型,这一系列过程就是类的加载机制。2.类的加载时机类从被虚拟机加载到内存开始,直到卸载出内存为止,整个生命周期包括:加载——验证——准备——解析——初始化——使用——卸载这7个阶段。其中验
Wesley13 Wesley13
3年前
JDBC之数据库的连接步骤(六步)
1.加载驱动在连接数据库之前,需要加载数据库的驱动到JVM(Java虚拟机),这需要通过java.lang.Class类的静态方法forName(StringclassName)实现.例如://加载Oracle的驱动try{Class.forName("oracle.jdbc.OracleDriver");}catch(ClassNo
Wesley13 Wesley13
3年前
Java类加载机制
启动(Bootstrap)类加载器启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C语言实现的,是虚拟机自身的一部分,它负责将<JAVA\_HOME/lib路径下的核心类库或Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机
Wesley13 Wesley13
3年前
Java虚拟机类加载机制
概述  虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。  与那些在编译时需要进行连接工作的语言不同,在Java语言里面,类型的加载、连接和初始化过程都在程序运行期间完成的,这种策略虽然会稍微增加一些系统性能开销,但是会为Java应用程序
Stella981 Stella981
3年前
Android动态加载之ClassLoader详解
Dalvik虚拟机如同其他Java虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。而在Java标准的虚拟机中,类加载可以从class文件中读取,也可以是其他形式的二进制流。因此,我们常常利用这一点,在程序运行时手动加载Class,从而达到代码动态加载执行的目的。只不过Android平台上虚拟机运行的是Dex字节码,一种对class文件优化的产物
Wesley13 Wesley13
3年前
Java类加载机制的理解
算上大学,尽管接触Java已经有4年时间并对基本的API算得上熟练应用,但是依旧觉得自己对于Java的特性依然是一知半解。要成为优秀的Java开发人员,需要深入了解Java平台的工作方式,其中类加载机制和JVM字节码这样的核心特性。今天我将记录一下我在新的学习路程中对Java类加载机制的理解。1.类加载机制类加载是一个将类合并到正在运
Stella981 Stella981
3年前
JVM(四)JVM的双亲委派模型
1、两种不同的类加载器  从JAVA虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(BootstrapClassLoader),这个类加载器使用C语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java,lang.ClassLoader。
Java类加载机制详解 | 京东云技术团队
一.类加载器及双亲委派机制|类加载器|加载类|备注||||||启动类加载器(BootstrapClassLoader)|JAVAHOME/jre/lib|无上级,无法直接访问由jvm加载||拓展类加载器(ExtensionClassLoader)|JAVA