《JVM系列》深入浅出类加载机制中<init>和<Clinit>的区别一篇即可搞懂初始化机制
Posted Roninaxious
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《JVM系列》深入浅出类加载机制中<init>和<Clinit>的区别一篇即可搞懂初始化机制相关的知识,希望对你有一定的参考价值。
前言:init和Clinit怎么产生的?
public class ClinitAndInitTest
private Integer num = 2;
private static Integer num2 = 3;
这两个方法是编译成字节码之后生成的(我们只在Java代码中定义了一个非静态和静态的变量),你觉得这两个方法有什么作用呢?看完你就明白了。
🍛首先从字面意义上看Clinit比init多了一个“C”,是不是可以认为是Class的缩写
🌀Clinit :类构造器方法【类级别的变量谁初始化呢?比如static修饰的】
🌀init :对象构造器方法【对象级别的实例变量谁初始化呢?】
这里及其下面我说的初始化是显示初始化,显示初始化就是赋值操作。比如private int a = 3;(另外一个就是默认初始化,它在变量被分配空间时就会默认初始化为0或null或false)
这就涉及到了类加载机制和实例化对象的过程,下图仅供了解。后续会讲解~
上图是JVM的整体结构
上图是类加载的流程图上图是对象内存结构图
1.init方法
1.1.init方法什么时候被调用?用来做什么?
当类的实例被创建的时候会被调用,至于这个方法做什么,你可以理解为JVM帮我们收集了对象的实例变量的显示赋值操作和非静态代码块,然后全部放到了init方法里(注意构造器中的代码当然在里面)。
class Init
private int a = 2; //实例变量的显示赋值操作
a = 3; //非静态代码块
public Init() //构造器
a =4;
1.2.那么实例变量赋值操作、非静态代码块、构造器这三者,哪一个会先执行呢?
- 实例变量赋值操作和非静态代码块是优先执行的,它们两者的代码谁在前面先执行谁。
- 构造器中的代码当然最后执行
如果你对父类+子类这种引用链的各大变量的初始化顺序不明白,请移步:
深入浅出Java复用类:https://blog.csdn.net/Kevinnsm/article/details/121392948?spm=1001.2014.3001.5501
1.3.那么既然赋值操作和非静态代码块优先级是相同的,那么看下面代码分析能不能这样操作?
public class ClinitAndInitTest
num = 2;
private Integer num = 5;
如果你思路很清晰,那么恭喜你,你已经了解了初始化机制。首先这样写肯定是正确的;这是因为其默认初始化num = 0早已在为该对象分配内存时完成,这说明内存中已经有了num这个变量了,你为其赋值当然可以。
如果你不明白默认初始化和显示初始化的顺序请移步:
深入浅出Java复用类:https://blog.csdn.net/Kevinnsm/article/details/121392948?spm=1001.2014.3001.5501
1.4.分析如下代码,看init方法字节码指令
class Init2
private int a = 3;
a = 4;
public Init2()
a = 5;
你会不会对上图中aload_0将引用压入操作数据栈中有疑问?我不是没创建对象嘛,哪里的引用呢?
这是因为每个方法内部都隐式地包含了this引用,在方法的局部变量标中都可以看到,它指向的是调用当前方法的对象;不信你可以看下面
- aload_0指令:将局部变量标中索引为0的引用压入操作数栈中
- iconst_3指令:将常量3压入操作数栈中
- putfield #2 指令:将常量和引用弹栈,并将#2引用的变量进行修改。(this.a = 2)
你可别认为这是顺序的原因喽,你无论把构造器放哪,它都是最后才会执行(注意我一直分析的是init方法,不包含类级别的变量)
1.5.分析如下代码,看你搞懂init方法没呢~!
class Init3
a = 40;
private int a = 3;
public Init3()
System.out.println(a);
a = 100;
System.out.println(a);
a = 200;
public static void main(String[] args)
Init3 it = new Init3();
结果如下:
请你尝试着分析它的字节码哈~
或许你会对3使用iconst指令,100使用bipush指令,200使用sipush指令有疑问?
iconst指令:这个指令的范围为-1~5,而且这个常量值已经隐含在了指令里面,为什么这样设计呢?其实是因为经过统计,在底层中-1 ~ 5使用的频率较高。
bipush指令:接收一个8位数正数作为参数,刚好对应一个字节(8个bit),范围刚好是-128~127(也就是byte的范围)
sipush指令:接收一个16位整数–》int范围
前两行调用了父类init方法(不懂初始化顺序:看深入浅出Java复用类)
2.Clinit方法
2.1.你会不会有个疑问,为什么需要Clinit方法?
众所周知,init是对对象级别的变量或非静态代码块进行初始化的;那么静态变量或者静态代码块谁来初始化呢?
没错,答案就是Clinit方法。(以下类级别的操作会在类加载阶段就完成)
如果你有疑问,请移步:看深入浅出Java复用类
2.2.Clinit方法什么时候被调用?
在类加载的的第三阶段初始化阶段会被调用
注意在类加载阶段,只会涉及类级别代码的执行,比如static修饰的域与代码块。上图只对类加载的三个过程做简单解释!
2.3.Clinit方法有什么作用呢?
该方法不需要我们创建,该方法是JVM帮我们自动收集代码中的所有的静态域的赋值操作和静态代码块合并而来。
class Init4
private static Integer a = 2; //静态域
static //静态代码块
System.out.println(a);
a = 3;
2.4.静态域赋值操作和静态代码块的顺序?
其实这和上面init方法中类似,这两者是同级的,也就是谁在前面先执行谁。
class Init4
private static Integer a = 2;
static
System.out.println(a);
a = 3;
public static void main(String[] args)
System.out.println(Init4.a);
执行结果:
2.5.那么既然静态域的赋值操作和静态代码块优先级是相同的,那么看下面代码分析能不能这样操作?
class Init5
static
a = 3;
private static int a = 2;
当然是当然可以,和1.2类似。根据我们上面分析Clinit的执行过程在类加载的初始化阶段进行显示初始化操作。那么默认初始化是在什么时候呢?
通过类比init的流程我们可以知道,当变量被分配空间时会被默认初始化为0或null或false。其实在类加载的第二阶段Linking中的准备阶段完成的
2.6.你会不会有疑问为什么类变量会被分配在方法区(JDK8开始为元空间)?
注意上图中有个隐含的问题,在JDK1.6及其以前类变量是被分配到方法区的;在JDK1.7及其以后,类变量和字符串常量池被移植到了堆中。(为了内存考虑?咱也不敢乱发表意见),毕竟JDK1.6及其以前永久代(方法区)和堆容易出现OOM,JDK1.8开始移除了方法区,改名为元空间,直接创建在本地内存中。【也不敢乱发表意见,为什么将类变量移植到堆】
2.7.从字节码角度分析以下代码的初始化流程?
2.8.分析如下代码,看你搞懂Cinit方法没呢~!
class Clinit7
static
a = 100;
private static int a = 200;
public static void main(String[] args)
System.out.println(a);
结果:200
请你尝试着分析它的字节码哈~
bipush和sipush指令我已经在上文init方法中解释过。
2.9.来自我的灵魂拷问:
(1)为什么静态方法中不能使用非静态变量?
(2)为什么静态方法中不能定义静态变量?【好像有点无用】
【这两个问题应该比较简单】
🎃个人介绍
🎈简介:一枚双非本科科班在读的学生,普普通通的生活却夹杂着不该有的梦想;一起学习交流WX:kht808
🎈博客主页:https://blog.csdn.net/Kevinnsm?spm=1001.2101.3001.5343
🎈热爱:广泛涉猎C/C++、JAVA、Python、Go,专注于服务端技术
以上是关于《JVM系列》深入浅出类加载机制中<init>和<Clinit>的区别一篇即可搞懂初始化机制的主要内容,如果未能解决你的问题,请参考以下文章