大家好,我是
node哥哥
,一个被Bug耽误了才艺的程序员,专注于Java领域的知识分享和技术交流,每天会以各种好看好玩的形式带给大家Java学习的小技巧,喜欢我的同学可以点赞+关注
哦~
文章目录
- 故事
- 题目
- 类加载过程
- 1.加载阶段
- 2.验证阶段
- 3.准备阶段
- 4.解析阶段
- 5.初始化阶段
- 题目分析
咳咳,最近剧本写得有点感觉了哈哈,越写越舒服,在此确定了以后文章的风格就是剧本+段子
的形式哈。用大白话讲技术,用吹牛逼的方式讨论原理,不管你是小白还是大白,看了我的文章包你爽就完了…当然爽完能学到知识,何乐而不为呢?别问,问就是牛逼!
今天故事的主角是刚认识的新朋友阳哥。这小伙,还是挺腼腆的,平时斯斯文文的,不怎么说话。我好话求了好半天,并许诺给他隔壁班新来小姐姐的微信,阳哥才终于答应爆照!
(阳哥本尊)
哈哈~别看阳哥这么可爱迷人,学习Java那可是一点都不耽误啊,每天早上5点起床,晚上搞到12点多才睡,这学习的劲头直逼当年的我啊,不得不佩服!
故事
话说那是一个夏日的午后,空气中弥漫着栀子花的香味~我们的阳哥却无心赏花,正坐在小板凳上埋头研读《Java从入门到如土》第4版,只见他双眉紧锁,苦苦思考。那副神情,不知道的还以为他在验证哥德巴赫猜想。
这时,我悄悄靠近,不动声色,想瞄一眼阳哥在学啥高深的学问。
Java的类加载机制这几个大字标题映入我的眼前。呵~ 我轻蔑的一笑,一屁股坐在阳哥旁边,准备啃我刚买的内蒙古奶油冰棍儿。
阳哥这时才注意到有人来,略微收起鼓了很久的腮帮子。有人打扰他独立思考让他很不开心,回头发出一声猪叫(额,说错了,说错了,只是嘟囔了一句…),一看是我,眼睛突然一亮:哎,node老哥,你怎么来了?(顺手抢走了我刚拆开包装的冰棍儿)
我顺手准备夺回,却未料到阳哥身手敏捷,书都扔地上了,慌忙把冰棍儿含在嘴里…
我:卧槽,阳哥,你过分了,我来指导你学习的,你却抢我冰棍吃,做人怎么这么不厚道?
阳哥:(笑出了猪叫)哈哈哈…嗝…你说做人?
我:哦,我忘记了,你是迷人的小可爱…
阳哥:滚!!(准备发怒)
我:好了,好了…看在你一个人这么寂寞的份上,冰棍算我请你的了,可以不?
阳哥:不可以!你一说我就来气。上次你说给我介绍小姐姐一起学习Java,结果那天下午我没睡午觉就出了宿舍,特地找隔壁宿舍老大哥借了200块钱搞了一个最近很流行的锡纸烫,还买了一双最新款的AK,想给小姐姐一个好印象。结果那天我在图书馆角落里足足等了她俩小时,Java编程思想都快翻完一遍了,人还不来!临走的时候,看到她跟另一个哥们手牵手一起从我旁边路过??还跟我打了个招呼?我嘞个擦???
我:害!我说给你介绍小姐姐认识,又没说她单身!你可能是误会我的意思了哈…我是说那个小姐姐是学霸,Java学的很6,你可以跟她请教疑惑,不是你想的那种的啊…阳哥,你是不是最近一个人太寂寞了?要不…
阳哥:(脸色一阵青一阵红,感觉随时要火山爆发)你*的!
我:(赶紧岔开话题)额,阳哥…那个…我不也是一番好心吗?别爆粗口啊!刚才看你好像在看那本最近很火的Java书?看的咋样了?有没有新的学习感悟分享分享?
果然,阳哥是个十足的Java迷,一听说讨论Java,冰棍都不吃了,马上捡起书开始跟我掰扯,我悄悄的松了一口气…
阳哥:哈哈,我跟你说老哥,不是我吹牛逼,我TM就是个天才!你别看我学Java时间短,但是我却对原理研究的非常深入!你看这道题,我们班没一个人会,我却把它搞得明明白白,已经完全分析透了,要不我给你也讲讲?
我半信半疑地拿过来阳哥的书,果然看到Java的类加载机制这一章后面有几道练习题,前面几题确实确实比较基础,最后一题题目还挺长的:
题目
// 有下面的一个类,请问最后main方法输出是什么?
public class Main {
public static int k = 0;
static Main t1 = new Main("t1");
public static Main t2 = new Main("t2");
public static int i = print("i");
public static int n = 99;
public int j = print("j");
static {
print("静态块");
}
public Main(String str) {
System.out.println((++k) + ": " + str + " i=" + i + " n=" + n);
++i;
++n;
}
public static int print(String str) {
System.out.println((++k) + ": " + str + " i=" + i + " n=" + n);
++n;
return ++i;
}
public static void main(String[] args) {
new Main("init");
}
}
拿到这个题目,说实话有点绕,但我觉得难不倒我,毕竟混迹Java这么多年了,别问,问就是牛逼!
那么牛逼回来了,这个输出到底是什么呢?我开始有点慌了…
我:额,那个…阳哥啊,刚才买冰棍的时候我忘记付钱了,我先过去把账结了哈,毕竟咱们都是高素质的人不是?不能白嫖啊!
阳哥:(很鄙视的看了我一眼)做不出来就直说,装什么大佬?
我:别扯了,这啥基础题啊这么简单,你等我5分钟,我回来给你讲的明明白白!
阳哥更加不屑了:好!我就在这等你,5分钟你要是做不出来,你直播吃**!
我:(有点心虚,但气势不能输)好!你等着吧,我付完账回来你看我怎么把它安排了吧!
说完我就头也不回地溜了。
说实话,这道题真的有点把我整懵了,居然一点思路都没有?怎么那么多个静态成员变量…又是静态变量,又是静态代码块,还有静态方法…构造函数…这都啥啊,忒复杂了吧!不行不行,我得赶紧回去充充电,不能让阳哥嘲笑我啊!不然这以后怎么混?
要知道这道题怎么做,我们先看下类加载的过程。
类加载过程
当我们使用main方法new一个对象的时候,其实是类在JVM(Java虚拟机)中被加载的过程。如果该类还未被加载到内存中,则JVM会通过加载、链接(验证、准备、解析)、初始化这5个步骤来对该类进行初始化。
五个阶段中加载,验证,准备和初始化发生的顺序是确定的,解析过程则不确定,我们都知道java支持动态绑定(多态),运行时才知道最终对象的引用,所以解析阶段可能发生在初始化阶段之后。虽然这几个阶段是按顺序开始的,但他们不是按顺序执行的,也就是说他们的执行和完成的时间不分先后,通常都是相互交叉进行。
JVM中Java类会在首次被使用时执行初始化,为类的(静态)变量赋予正确的初始值(例如int类型的变量初始值为0)。但这不代表类加载器只有等到某个类被首次主动使用时才加载,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
下面我们来针对类加载的这整个过程逐一进行拆分讲解:
1.加载阶段
类的加载阶段是指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象。类加载主要做下面几件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。
- 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口。
类加载器通常无须等到首次使用该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。类的来源通常有以下几种:
- 本地编译好的class文件
- jar包中的class文件
- 动态代理生成的class文件
- 压缩文件中的jar,class,war文件
2.验证阶段
此阶段主要为保证加载类的正确性。
主要根据以下几种方法对类信息进行验证:
- 主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。b比如常量池中是否有不被支持的常量类型,指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。
- 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
3.准备阶段
类准备阶段负责为类的静态变量分配内存,并设置默认初始值,注意是虚拟机给各个类型的变量定义的默认值,不是实际程序中的赋值。例如:
public staic int a = 1;
准备阶段a的值为默认值0,而不是1
对于基本类型的静态变量一般被赋予0,对于引用的静态类型,一般被赋予null,如果是boolean型数据,则为false,对于final修饰的变量,会在这个阶段就直接赋值,成员变量则不会赋初始值。
4.解析阶段
把类中的符号引用转化为直接引用。
什么是符号引用?
符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关,
引用的目标不一定已经加载到内存中
。
什么是直接引用?
是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且
一定是已经加载进来的
。
虚拟机的解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号的引用进行。
5.初始化阶段
初始化阶段就是为类的静态变量依次赋予实际的初始值(注意与前面的默认值区分开,这里指的是赋予代码中的值),包括静态代码块也会执行。前面的代码:
public staic int a = 1;
在这个时候a就是1。
在这个阶段,会执行类的构造函数,并且JVM也负责对类成员变量进行初始化赋值。例如:
public int b = 2;
这个时候b会被赋值2。
类加载的最终产品是位于堆中的Class对象,封装了类在方法区内的数据结构,并向java程序员提供了访问方法区内数据结构的接口。
好了,类的初始化过程我们已经研究明白了,下面我们来分析下阳哥的题目。
题目分析
我们先从程序的入口开始看:
public static void main(String[] args) {
new Main("init");
}
这里做了什么呢?new了一个Main类的对象,往Main的构造函数里传了一个初始化的字符串 “init”。那么对应到类的初始化过程,我们看看,实际会发生什么?
- 初始化Main类的所有静态成员变量(加载,验证,一直到准备阶段):
给int类型的静态成员变量赋值为0,给引用类型的静态成员变量赋值为null,也就是说, k=0,i=0,n=0,j=0,t1=null,t2=null - 按顺序依次给静态成员变量赋值:
k赋值为0
t1=new Main(“t1”) 注意,这里又进行了一次new Main(),但是这次因为类已经初始化过了,过了准备阶段,已经有了默认值,所以就直接给各个静态成员变量赋实际值。因为 j 是成员变量,非静态变量,他是在对象实例化之前初始化的,所以先直接执行了方法print(“j”)
,
public static int print(String str) {
System.out.println((++k) + ": " + str + " i=" + i + " n=" + n);
++n;
return ++i;
}
第一行输出结果:
此时,k=1,n=1,i=1。执行完此方法后,开始实例化t1对象,执行构造函数:
public Main(String str) {
System.out.println((++k) + ": " + str + " i=" + i + " n=" + n);
++i;
++n;
}
第二行输出:
至此static Main t1 = new Main(“t1”);
完成了,此时 k=2,i=2,n=2。
接着到了赋值public static Main t2 = new Main(“t2”);
的时候了,它也和t1类似。所以第三行第四行会输出下面内容:
执行完t2的构造函数,k=4,i=4,n=4。
下面开始继续给静态成员 i 变量再次赋值:public static int i = print(“i”);
输出结果:
执行完,k=5,i=5,n=5。
再往下:给静态成员变量n再次赋值: public static int n = 99;
,不输出结果,n=99。
再往下,静态成员变量全部完成了赋值,开始执行静态代码块:
static {
print("静态块");
}
执行结果:
执行完,k=6,i=6,n=100。
最后,Main的全部成员变量赋值完毕,开始初始化入口对象 new Main(“init”),它的套路也是跟t1一样,先初始化成员变量,再执行构造函数,输出:
执行完:k=8,i=8,n=102。
完整的执行结果如下:
呼~ 我长舒了一口气,为了解这道题可是花费了我不少时间啊!幸亏好好复习一遍类加载的知识把它们都搞懂了。
哎,又到了凌晨1点多了,估计这会阳哥睡了吧?完了,这下不得丢人丢到外太空去?这么着吧,我就说我付钱看到小偷了,当时我为了见义勇为只能牺牲小我了,哎,谁让咱们是社会的三好青年呢?优秀的让人没办法啊~
看来只能下次再跟阳哥吹嘘了,哈哈哈~
看着窗前的夜色,不禁诗兴大发,决定吟诗一首:
唯有夜共语,
更无人通言。
寂寥如是斯,
我心独自燃。
匿了匿了…
创作不易,如果您喜欢这篇文章的话,请你
点赞
支持一下作者好吗?您的支持是我创作的源泉哦!如果您有什么好的建议或者意见也欢迎在评论区跟我讨论一下的哦。喜欢Java,热衷学习的小伙伴可以加我微信: xia_qing2012,大家一起学习进步,成为大佬!