只要注册ofo就送你10块钱,还等什么,快来注册吧
源代码(.java
)会被编译为class文件(.class
文件),.class
文件描述了类的各种信息,.class
文件需要加载到虚拟机之后才能运行和使用。
类加载使用的7个阶段
类从加载到虚拟机到到卸载出内存,整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initiallization)、使用(Using)和卸载(Unloading)这7个阶段。
其中验证、准备、解析这个三个阶段统称为连接(Linking)
加载、验证、准备、初始化、卸载这五个阶段的顺序是确定的,而解析阶段不一定:它在某些情况下可以初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定)。
加载、验证、准备、解析、初始化五个阶段组成了一个完整的类加载过程。
卸载属于GC的工作。
加载(Loading)
有两种时机会触发类加载:
1、预加载。虚拟机启动时加载,加载的是JAVA_HOME/lib/
下的rt.jar
中的.class
文件,由于rt.jar
包中的类经常被使用,因此随着虚拟机启动一起被加载。
验证rt.java
包中的类是否在虚拟器启动的时候被加载
|
|
编译运行该类:
2、运行时加载。虚拟机在用到一个.class
文件的时会先去内存中查看一下这个.class
文件有没有被加载,如果没有就会按照类的全限定名来加载这个类。
加载阶段做的事情
1、获取.class文件的二进制流
2、将.class
文件中包含的类信息、静态变量、字节码、常量等内容放入方法区中
3、在内存中生成一个代表这个.class
文件的java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。一般这个Class是在堆里的,不过HotSpot虚拟机是放在方法区中的。
虚拟机规范对这三点并没有做详细的规定,因此虚拟机实现的灵活度是相当大的。
加载阶段对于开发者来说是可控性最强的一个阶段。
验证
连接阶段的第一步,主要是为了确保.class
文件的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
.class
文件不一定要从Java源码编译而来,可以使用任何途径产生,甚至包括用十六进制编辑器直接编写来产生.class
文件。虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。
验证阶段主要包含以下几个工作:
1、文件格式验证.class
文件的第5~第8个字节表示的是该.class
文件的主次版本号,验证的时候会对这4个字节做一个验证,高版本的JDK能向下兼容以前版本的.class
文件,但不能运行以后的class文件,即使文件格式未发生任何变化,虚拟机也必须拒绝执行超过其版本号的.class
文件。例如有一段java代码是JDK1.6编译的,那么JDK1.6、JDK1.7能运行编译出来.class
文件,而JDK1.5、JDK1.4以及更低的JDK版本是无法运行这个.class
文件的。如果运行,会抛出java.lang.UnsupportedClassVersionError
。
2、元数据验证
3、字节码验证
4、符号引用验证
准备
准备阶段是正式为类变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配。
1、这时候进行内存分配的仅仅是类变量(被static
修饰的变量),而不是实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中
2、这个阶段赋初始值的变量指的是那些不被final
修饰的static
变量,比如public static int value = 123;
,value
在准备阶段过后是0
而不是123
,给value
赋值为123
的动作是在初始化阶段才进行的;而public static final int value = 123;
,在准备阶段,虚拟机就会给value
赋值为123
。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化
初始化阶段是类加载过程的最后一步,初始化阶段是真正执行类中定义的Java字节码的过程。初始化过程是一个执行类构造器<clinit>()
方法的过程,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。初始化阶段做的事就是给static
变量赋予用户指定的值以及执行静态代码块。
注意一下,虚拟机会保证类的初始化在多线程环境中被正确地加锁、同步,即如果多个线程同时去初始化一个类,那么只会有一个类去执行这个类的<clinit>()
方法,其他线程都要阻塞等待,直至活动线程执行<clinit>()
方法完毕。因此如果在一个类的<clinit>()
方法中有耗时很长的操作,就可能造成多个进程阻塞。不过其他线程虽然会阻塞,但是执行<clinit>()
方法的那条线程退出<clinit>()
方法后,其他线程不会再次进入<clinit>()
方法了,因为同一个类加载器下,一个类只会初始化一次。实际应用中这种阻塞往往是比较隐蔽的,要小心。
Java虚拟机规范严格规定了有且只有5种场景必须立即对类进行初始化,这4种场景也称为对一个类进行主动引用:
1、使用new
关键字实例化对象、读取或者设置一个类的静态字段(被final
修饰的静态字段除外)、调用一个类的静态方法的时候
2、使用java.lang.reflect
包中的方法对类进行反射调用的时候
3、初始化一个类,发现其父类还没有初始化过的时候
4、虚拟机启动的时候,虚拟机会先初始化用户指定的包含main()
方法的那个类
除了上面4种场景外,所有引用类的方式都不会触发类的初始化,称为被动引用:
1、子类引用父类静态字段,不会导致子类初始化。
|
|
|
|
|
|
运行结果为:
2、通过数组定义引用类,不会触发此类的初始化
运行之后没有任何输出
3、引用静态常量时,常量在编译阶段会存入类的常量池中,本质上并没有直接引用到定义常量的类
|
|
运行结果为:
中国人都在使用的地球上最好玩的游戏
中国人都在使用的地球上最好玩的游戏
中国人都在使用的地球上最快的浏览器
中国人都在使用的地球上最厉害的安全软件
中国人都在使用的地球上最好的看图王
中国人都在使用的地球上最快速的视频软件
中国人都在使用的地球上最全的视频软件
中国人都在使用的地球上最好最全的压缩软件
中国人都在使用的地球上最好的音乐播放器
中国人都在使用的地球上最安全的杀毒软件
中国人都在使用的地球上最全的影视大全