从 static 关键字深入理解 java 对象初始化顺序

news/2024/5/19 3:41:04 标签: java, jvm, 编程语言, 反射, android

目录

前言

静态变量

静态块

类加载中,静态域的加载时机

总结

参考


微信搜索【猿芯】,关注愿与你一起分享程序员内心独白的程序员。

本文已收录至我的 GitHub 地址 https://github.com/dunzung/JavaBlogs,喜欢用简单的文字记录工作与生活中的点点滴滴,愿与你一起分享程序员灵魂深处真正的内心独白。

前言

最近在阅读 ThreadLocal 源码的时候,发现一段很有意思的代码,代码片段如下:

private final int threadLocalHashCode = nextHashCode(); 
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

以上代码片段主要是 ThradLocal 生成哈希值(threadLocalHashCode)的逻辑,通过静态的原子整型变量 nextHashCode 以及静态方法 nextHashCode (),为每个线程持有的 ThreadLocal 本地变量生成唯一 的 hashCode

注:ThreadLocalhashCode 选择 HASH_INCREMENT 变量值:0x61c88647 很有意思,里面涉及到斐波那契数列黄金分割法,感兴趣的同学可以自行了解下。

当然本文的重点不是 ThreadLocal 原理分析上,而是分析 static 关键字修饰的静态域(静态变量、静态块)顺序加载问题。

这段代码总共四行,除了第一行都是用 static 关键字修饰的,这里我们设想一个问题,当类初始化的时候,这四行代码是从上往下执行的吗?

答案是:”否“。

静态变量

为了方便 debug 调试,我们把上面的代码稍微做了下调整,代码片段如下:

public class Static01 {
    private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode = new AtomicInteger();

    private static final int HASH_INCREMENT = getIncr();

    public Static01(){
        System.out.println("threadLocalHashCode::" + threadLocalHashCode);
    }

    private static int getIncr() {
        return 0x61c88647;
    }

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    public static void main(String[] args) {
        new Static01();
    }

}

上面的代码片段用 debug 模式启动,通过为每行代码打断点,发现当真正实例化 Static01 类时,代码运行顺序并非是按照逐行执行,而是如下图红色标记顺序进行的。

其执行流程是:

  • 第一步、用 new 关键字初始化 Static01 类的构造方法

  • 第二步、初始化静态变量 nextHashCode

  • 第三步、初始化静态变量 HASH_INCREMENT

  • 第四步、初始化成员变量 threadLocalHashCode

  • 最后 、在 Static01 构造方法打印 threadLocalHashCode 变量的 hash

对象实例化

就是执行类中构造函数的内容,如果该类存在父类,会通过显示或者隐示的方式(super方法)先执行父类的构造函数,在堆内存中为父类的实例变量开辟空间,并赋予默认的初始值,然后在根据构造函数的代码内容将真正的值赋予实例变量本身,然后,引用变量获取对象的首地址,通过操作对象来调用实例变量和方法

从上面代码执行流程可以看出

  • 在对象实例化之前必须先初始化 static 修饰的静态变量,并且静态变量也是有加载顺序的;

  • 类的成员变量的初始化在构造方法里面进行,加载顺序优先于构造方法体的执行语句。

  • 如果某类继承了父类,那么必须先初始化父类的构造方法以及成员变量以及构造方法的执行语句,然后才是子类的成员变量以及构造方法的执行语句。

public Static01() {
    super();
    System.out.println("threadLocalHashCode::" + threadLocalHashCode);
}

另外,静态语句块中只能访问到定义在静态块之前的变量,在静态块里可以给该变量赋值,但是不能访问,否则编译器会提示 “Illegal forward reference” 错误,如下图

静态块

静态块主要用于类的初始化,不是指对象的实例化。它只会执行一次,静态块只能访问类的静态成员属性和方法,不能在静态块使用 this

我们先把上面的代码稍加改造下,增加 “静态块1”和“静态块2” 静态块代码

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();

static{
    System.out.println("静态块1");
}

private static final int HASH_INCREMENT = getIncr();

static{
    System.out.println("静态块2");
}

运行结果如下:

 

发现不管是静态块还是静态变量,它们之间都是按顺序执行的。那为什么是静态块、静态变量的初始化是有顺序的呢?

通过查看 Static01 类的 class 编译文件,发现编译器会把 static 块的代码放在同一 static 花括号{}内。

代码顺序是按照之前编码的顺序整合,这么看来是编译器在作怪吧。

static {
    System.out.println("静态块1");
    HASH_INCREMENT = getIncr();
    System.out.println("静态块2");
}

类加载中,静态域的加载时机

从《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》这本书讲的类加载机制原理可知:

当遇到newgetstaticputstaticinvokestatic4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。

这就解释了为什么在对象未实例化前,可以通过 “类名.静态属性变量、类名.静态方法” 的方式访问静态变量和静态方法了。

类加载的时机

对于初始化阶段,虚拟机规范规定了有且只有 5 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

  1. 遇到new、getstatic 和 putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应场景是:使用 new 实例化对象、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法。

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

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

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

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

总结

1、静态域(静态变量、静态块)是按逐行顺序加载的,并且静态域只会加载一次。

2、当实例化对象之前(构造方法调用),会先去初始化静态域,再去调用构造函数实例化对象。

3、一般对象初始化顺序如下:父类的静态域顺序加载–>子类静态域顺序加载–>父类非静态域初始化->父类构造函数初始化–>子类非静态域初始化->子类构造函数初始化。

参考

  • https://blog.csdn.net/qq_36522306/article/details/80584595

  • https://www.cnblogs.com/cxiang/p/10082160.html


http://www.niftyadmin.cn/n/1240817.html

相关文章

台达asda-b2伺服驱动器说明书_【智慧中达】台达获得“CMCD 2018年度运动控制最具影响力企业”奖项...

环保 节能 爱地球凭藉2018年良好的市场表现,台达收获“CMCD 2018年度运动控制最具影响力企业”奖项。1月11日,从于广东顺德举行的"2019智能制造&中国运动控制行业发展高峰论坛暨颁奖盛典"上传来喜讯,凭藉2018年…

Spring Boot 的接口限流算法优缺点深度分析

点击上方蓝色“猿芯”关注,输入1024,你懂的 前言 在一个高并发系统中对流量的把控是非常重要的,当巨大的流量直接请求到我们的服务器上没多久就可能造成接口不可用,不处理的话甚至会造成整个应用不可用。 那么何为限流呢&#xff…

I9 9900K线程_地表最强无人撼动!i9-9900K仍是最强游戏CPU

[PConline杂谈]大家都在等7月份上线的新产品对吧,技术的革新对消费者来说无疑是一种福利。不过经过我们的测试发现,一直以来被称为地表最强游戏U的Core i9-9900K的地位依然不变,而它的小弟Core i7-9700K甚至在某些游戏上反超老大哥。Corei9-9…

万字超强图文讲解AQS以及ReentrantLock应用(建议收藏)

点击上方蓝色“猿芯”关注,输入1024,你懂的 Java SDK 为什么要设计 Lock 曾几何时幻想过,如果 Java 并发控制只有 synchronized 多好,只有下面三种使用方式,简单方便 public class ThreeSync {private static final Ob…

python中型项目开发_教Python小白读懂一个复杂的中型Django项目

原标题:教Python小白读懂一个复杂的中型Django项目 【前言】中型的项目是比较多的APP,肯会涉及多数据表的操作。如果有人带那就最好了,自己要先了解基本的django框架(MTV ,ORM等)师傅可以给讲解一下框架怎么组织url.py,model.py&a…

Jetpack架构组件库-Lifecycle应用解析之知其然

点击上方蓝色“猿芯”关注我们,输入1024,你懂的 前言 本次主要讲解的内容: 1、Lifecycle介绍,解决了什么问题 2、Lifecycle理解与运用 3、Lifecycle如何感知生命周期的状态? 一、Jetpack 介绍 1、什么是Lifecycle Life…

c定义一个整型数组_面向小白——c指针的详细讲解

相信我,指针绝对比你想象中的简单100倍,仅仅只是因为人云亦云说指针抽象难以理解。实则不然,放好心态去学习即可。指针介绍: 首先明确指针是一种数据类型(分为int型指针,char型指针等等)&#x…

faster rcnn fpn_Faster-RCNN细节deep-in(原文翻译)之一

http://www.telesens.co/2018/03/11/object-detection-and-classification-using-r-cnns/​www.telesens.co本文参考上文,做了部分翻译和自己的理解,欢迎交流讨论!使用RCNN进行目标检测和分类经典的RCNN系列(主要是RCNN&#xff0…