java类的加载机制及加载过程,java类加载方式
文章转载自:http://www.pythonheidong.com/blog/article/1152/
在很多Java访谈中,我们经常看到对Java类加载机制的调查,比如下面这个问题:
班爷爷{
静电
{System.out.println(爷爷在静态代码块中);
}
}
班父延爷爷{
静电
{System.out.println (Dad在静态代码块中);
}
公共静态int因子=25;
公共父亲()
{System.out.println(我是爸爸~ );
}
}类子扩展父{
静电
{System.out.println(静态代码块中的儿子);
}
公有子()
{System.out.println(我是儿子~ );
}
}
公共类初始化演示{
公共静态void main(String[] args)
{System.out.println(爸爸的年龄:儿子.因子);//入口
}
}请写出最终的输出字符串。
正确答案是:
爷爷在静态代码块里
爸爸在静态代码块里。
爸爸年龄:25。相信很多同学看到这个题目后表情都崩溃了,不知道从何下手。有的甚至见了几次面,还是找不到正确的解决方法。
其实这种面试题考察的就是你对Java类加载机制的理解。
如果你不了解Java的加载机制,那么你就无法解决这个问题。
所以在这篇文章里,我会带你先学习Java类加载的基础知识,然后在实战中分析几个题目,让你掌握思路。
我们先来学习一下Java类加载机制的七个阶段。
推荐教程: 《java视频教程》
Java类加载机制的七个阶段
当我们的Java代码被编译后,就会生成相应的类文件。然后当我们运行java演示命令时,我们实际上启动了JVM虚拟机来执行类字节码文件的内容。JVM虚拟机执行类字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。。
加载
以下是最官方的加载过程描述。
实际上,加载阶段,一句话,就是将代码数据加载到内存中。这个过程和我们解决这个问题没有直接关系,但是它是一个类加载机制的过程,所以必须提一下。
验证
JVM加载类字节码文件并在方法区创建相应的类对象后,JVM将启动字节码流的验证,只有符合JVM字节码规范的文件才能被JVM正确执行。这种验证过程可以大致分为以下几种类型:
JVM规范校验。JVM会检查字节流的文件格式,确定是否符合JVM规范,是否可以被当前版本的虚拟机处理。比如文件是否以0x cafe bene开头,主版本号和次版本号是否在当前虚拟机的处理范围内等等。代码逻辑校验。JVM会检查代码组成的数据流和控制流,保证JVM运行字节码文件后不会出现致命错误。例如,一个方法需要传入一个int参数,但是当使用它时,传入一个String参数。方法需要String类型的结果,但它不需要。代码引用了一个名为Apple的类,但实际上并没有定义Apple类。当代码数据被加载到内存中时,虚拟机将检查代码数据,以查看代码是否真的按照JVM规范编写。这个过程和我们解题没有直接关系,但是要理解类加载机制,必须知道这个过程的存在。
准备(重点)
当字节码文件被检查时,JVM将开始分配内存并初始化类变量。这里有两个要点需要注意,即内存分配的对象和初始化的类型。
内存分配的对象。Java中有两种类型的变量:类变量和类成员变量。类变量是指被static修改的变量,其他类型的变量都属于类成员变量。在准备阶段,JVM只为类变量分配内存,而不为类成员变量分配内存。类成员变量的内存分配需要等到初始化阶段。例如,在准备阶段,下面的代码只为factor属性分配内存,不为website属性分配内存。
公共静态int因子=3;公串网址= www . cn blogs . com/chanshuyi ;初始化的类型。在准备阶段,JVM会为类变量分配内存并初始化。但是,这里的初始化是指在Java语言中给一个变量赋这个数据类型的零值,而不是用户代码中的初始化值。例如,在下面的代码中,在准备阶段之后,sector的值将是0而不是3。
公共静态int扇区=3;但是,如果一个变量是一个常量(由static final修改),那么在准备阶段属性将被赋予所需的值。例如,在下面的代码中,在准备阶段之后,number的值将是3而不是0。
public static final int number=3;之所以会直接复制static final,并给静态变量赋零值。其实稍微想一想就能想出来。
这两个语句的区别在于,一个用final关键字修饰,另一个没有。final关键字在Java中的意思是不可变的,也就是说number的值一旦赋值,就不会改变。由于赋值永远不会再改变,所以必须从一开始就给它取所需的值,所以final修饰的类变量在准备阶段就给它取所需的值。但是,没有被final修饰的类变量可能会在初始化阶段或运行时阶段发生变化,因此没有必要在准备阶段给它指定所需的值。
解析
通过准备阶段后,JVM解析接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符的类或七个类引用。这个阶段的主要任务是将它在常量池中的符号引用替换为它在内存中的直接引用。
其实这个阶段对我们来说几乎是透明的,了解一下就好。
初始化(重点)
在初始化阶段,用户自定义的Java程序代码才真正开始执行。在这个阶段,JVM将根据语句执行的顺序初始化类对象。一般来说,当JVM遇到以下五种情况时,初始化就会被触发:
在遇到new、getstatic、putstatic、invokestatic这四个字节码指令时,如果类还没有初始化,就需要先触发其初始化。生成这四条指令最常见的Java代码场景是:用new关键字实例化一个对象的时候,读取或设置一个类的静态字段的时候(final修饰的静态字段除外,已经被编译器放入常量池的结果),调用一个类的静态方法的时候。当使用java.lang.reflect包的方法对一个类进行反射调用时,如果该类还没有初始化,则需要先触发其初始化。在初始化一个类的时候,如果发现其父类还没有初始化,就需要先触发其父类的初始化。虚拟机启动时,用户需要指定一个要执行的main类(包含main()方法的类),虚拟机首先初始化这个main类。使用JDK1.7动态语言支持时,如果java.lang.invoke.MethodHandle实例的最后一个解析结果ref _ getstatic、ref _ putstatic和ref _ invokestatic的方法句柄未初始化,则需要先触发其初始化。看到以上条件你可能会头晕,但没关系,不需要背。了解一下就行了,回头再去找就行了。
使用
JVM完成初始化阶段后,JVM开始从入口方法执行用户的程序代码。在这个阶段,你可以只知道它。
卸载
执行用户程序代码时,JVM开始销毁创建的类对象,最后负责运行的JVM也退出内存。在这个阶段,你可以只知道它。
看了Java的class loading wit,是不是有点傻?不,让我们用一个小例子来唤醒。
public class Book { public static void main(String[]args){
System.out.println(你好,亦舒);
}
图书()
{
System.out.println(图书构造方法);
System.out.println(价格=价格,金额=金额);
}
{
System.out.println(一本书的公共代码块);
} int price=110静电
{
System.out.println(书的静态代码块);
}静态int金额=112;
}想想上面的代码输出了什么?
你有5分钟思考时间,5分钟交卷,哈哈。
怎么样?你想过吗?答案已经公布了。
本书的静态代码块
你好,亦舒。怎么样?你做对了吗?是不是和你想的有点不一样?
我们来简单分析一下。首先,根据上面提到的触发初始化的五种情况中的第四种(虚拟机启动时,用户需要指定一个要执行的main类(包含main()方法的类,虚拟机首先初始化这个main类),我们将初始化该类。
那么类的初始化顺序是怎样的呢?
重点来了!
重点来了!
重点来了!
在我们的代码中,我们只知道有一个构造方法,但实际上Java代码编译成字节码后,并没有构造方法的概念,只有类初始化方法和对象初始化方法。
那么这两种方法是怎么来的呢?
初始化类方法。编译器会将类变量的赋值语句和静态代码块按照出现的顺序收集起来,最终形成类初始化方法。类初始化方法一般在类初始化的时候执行。上面的例子,其类的初始化方法是下面的代码:
静电
{
System.out.println(书的静态代码块);
}静态int金额=112;对象初始化方法。编译器会按照成员变量的出现顺序收集成员变量的赋值语句和公共代码块,最后收集构造函数的代码,最后形成对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。在上面的例子中,对象初始化方法是下面的代码:
{
System.out.println(一本书的公共代码块);
} int price=110
System.out.println(图书构造方法);
System.out.println(价格=价格,金额=金额);初始化完类方法和对象初始化方法,再来看这个例子,我们不难得到上面的答案。
但细心的朋友会发现,其实上面的例子并没有实现对象初始化方法。
因为我们真的没有实例化Book类对象。如果在main方法中加入new Book()语句,会发现执行的是对象的初始化方法!
有兴趣的朋友可以自己试试,这里就不做了。
通过以上的理论和简单的例子,让我们进入更复杂的实战分析。
实战分析
班爷爷{
静电
{System.out.println(爷爷在静态代码块中);
}
}
班父延爷爷{
静电
{System.out.println (Dad在静态代码块中);
}
公共静态int因子=25;
公共父亲()
{System.out.println(我是爸爸~ );
}
}类子扩展父{
静电
{System.out.println(静态代码块中的儿子);
}
公有子()
{System.out.println(我是儿子~ );
}
}
公共类初始化演示{
公共静态void main(String[] args)
{System.out.println(爸爸的年龄:儿子.因子);//入口
}
}想一想,上面这段代码最后的输出结果是什么?
最终的输出结果是:
爷爷在静态代码块里
爸爸在静态代码块里。
爸爸年龄:25。也许有人会问为什么不输出字符串“静态代码块中的儿子”?
这是因为对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块)。因此,通过子类引用父类中定义的静态字段只会触发父类的初始化,而不会触发子类的初始化。
在上面的例子中,我们可以从入口开始,一路向下分析:
首先,程序转到main方法,在Son类中使用标准化的output factor类成员变量,但这个类成员变量没有在Son类中定义。于是我们去了父类,我们在父类中找到了对应的类成员变量,触发了父的初始化。但是根据上面提到的五种初始化情况中的第三种(在初始化一个类的时候,如果发现其父类还没有初始化,就需要先触发其父类的初始化)。我们需要先初始化父类的父类,也就是先初始化爷爷类,再初始化父类。于是我们先初始化爷爷类输出:“爷爷在静态代码块里”,然后初始化父亲类输出:“爸爸在静态代码块里”。最后,所有的父类初始化完毕后,子类就可以调用父类的静态变量,从而输出:“爸爸的年龄:25”。怎么样?有没有豁然开朗的感觉?
让我们看一个更复杂的例子,看看输出是什么。
班爷爷{
静电
{System.out.println(爷爷在静态代码块中);
}
public爷爷(){system.out.println(我是爷爷~ );
}
}班父延爷爷{
静电
{System.out.println (Dad在静态代码块中);
}
公共父亲()
{System.out.println(我是爸爸~ );
}
}类子扩展父{
静电
{System.out.println(静态代码块中的儿子);
}
公有子()
{System.out.println(我是儿子~ );
}
}
公共类初始化演示{
公共静态void main(String[] args)
{新子();//入口
}
}输出结果是:
爷爷在静态代码块里
爸爸在静态代码块里。
静态代码块中的子
我是爷爷~
是我爸~
我是我儿子~怎么样?你觉得这个问题和上面的问题不一样吗?
让我们仔细看看上面代码的执行过程:
首先,我们在入口处实例化一个Son对象,它会触发Son类的初始化,Son类的初始化会驱动父类和祖父类的初始化,从而执行相应类中的静态代码块。所以会输出:“爷爷在静态代码块中”、“爸爸在静态代码块中”、“儿子在静态代码块中”。儿子类初始化时,会调用儿子类的构造函数,儿子类的构造函数的调用也会带动父亲类和爷爷类的构造函数的调用。最后会输出:“我是爷爷~”“我是爸爸~”“我是儿子~”。看了两个例子,相信大家都很自信。
这里有一个特殊的例子。有点难!
public class Book { public static void main(String[]args){
static function();
}静态书book=新书();静电
{
System.out.println(书的静态代码块);
}
{
System.out.println(一本书的公共代码块);
}
图书()
{
System.out.println(图书构造方法);
System.out.println(价格=价格,金额=金额);
} public static void static function(){
System.out.println(书籍的静态方法);
} int price=110静态int金额=112;
}以上示例的输出结果是:
书籍的公共代码块
图书建设的方法
价格=110,金额=0
本书的静态代码块
下面我们来一步步分析一下代码的整个执行过程。
在上面的两个例子中,因为main方法所在的类中没有多余的代码,所以我们直接忽略main方法所在的类的初始化。
但是在这个例子中,main方法所在的类中有很多代码,所以我们不能直接忽略它们。
当JVM处于准备阶段时,它将分配内存并初始化类变量。此时,我们的book实例变量被初始化为null,amount变量被初始化为0。进入初始化阶段后,由于Book方法是程序的入口,根据上面提到的类初始化五种情况中的第四种(虚拟机启动时,用户需要指定一个要执行的main类(包含main()方法的类),虚拟机先初始化这个main类)。于是JVM初始化Book类,也就是执行类构造函数。初始化Jvmbook类,第一步是执行类构造函数(收集类中所有静态代码块和类变量赋值语句,以便形成类构造函数),然后执行对象构造函数(依次收集成员变量赋值和常用代码块,最后收集对象构造函数,最后形成对象构造函数)。对于Book类,其类构造方法()可以简单表示如下:
静态书book=新书();静态{
System.out.println(书的静态代码块);
}静态int金额=112;然后先执行静态Book Book=new Book();这个语句,反过来触发类的实例化。然后,JVM执行对象构建器,并收集对象构建器代码:
{
System.out.println(一本书的公共代码块);
} int price=110
图书()
{
System.out.println(图书构造方法);
System.out.println(价格=价格,金额=金额);
}所以这个时候price给的值是110,输出:“书籍的常用代码块”和“书籍的构造方法”。此时price的值是110,amount的赋值语句还没有执行,所以在准备阶段只给出了一个零值,所以之后输出“price=110,amount=0”。
当类实例化完成时,JVM继续初始化类构造函数:
静态书book=新书();//完成静态类实例化{
System.out.println(书的静态代码块);
}静态int金额=112;输出是“书的静态代码块”,然后把112的值赋给金额。
至此,类的初始化已经完成,JVM执行main方法的内容。公共静态void main(String[] args){
static function();
}即输出:“书之静法”。
方法论
从上面的例子可以看出,分析一个类的执行顺序大致可以遵循以下步骤:
确定类变量的初始值。在类加载的准备阶段,JVM会用零值初始化类变量,此时类变量会有一个初始零值。如果是final修饰的类变量,会直接初始化成用户想要的值。初始化入口方法。进入类加载初始化阶段后,JVM会寻找整个main方法入口,从而初始化main方法所在的整个类。当一个类需要初始化时,首先初始化类构造函数(),然后初始化对象构造函数()。初始化类构造器。JVM会按顺序收集类变量的赋值语句和静态代码块,最后形成一个类构造器由JVM执行。初始化对象构造器。JVM会收集成员变量的赋值语句,公共代码块,最后收集构造方法,形成对象构造器,最终由JVM执行。如果在初始化main方法所在的类时遇到其他类的初始化,先加载相应的类,加载完成后再返回。重复这个循环,最后回到main方法所在的类。
看完上面的分析,去看看第一个问题是不是简单很多。很多事情都是这样的。掌握了一定的方法和知识后,原本很难的事情变得简单了很多。
一时不明白也不要灰心。毕竟我花了很长时间才明白。如果不明白,可以多看几遍,或者加入亦舒的技术交流群,和男生交流。
原地址:3359 www . cn blogs . com/Xiong batianskjdskjdksjdskdtuti/p/11356706 . html
以上是java类加载机制的详细内容。请多关注我们的其他相关文章!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。