From:深入理解Java虚拟机
- 目录
BiBi - JVM -0- 开篇
BiBi - JVM -1- Java内存区域
BiBi - JVM -2- 对象
BiBi - JVM -3- 垃圾收集算法
BiBi - JVM -4- HotSpot JVM
BiBi - JVM -5- 垃圾回收器
BiBi - JVM -6- 回收策略
BiBi - JVM -7- Java类文件结构
BiBi - JVM -8- 类加载机制
BiBi - JVM -9- 类加载器
BiBi - JVM -10- 虚拟机字节码
BiBi - JVM -11- 编译期优化
BiBi - JVM -12- 运行期优化
BiBi - JVM -13- 并发
Java天生的动态扩展语言特性是依赖运行期动态加载和动态连接这个特点实现的。
例如:在编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类,用户可以通过Java预定义的或自定义的类加载器,让一个本地的应用程序可以在运行时从网络或其它地方加载一个二进制流作为程序代码的一部分。
类的生命周期
1)加载
2)连接:验证 -> 准备 -> 解析
3)初始化
4)使用
5)卸载
其中【解析】阶段在某些情况下可以在【初始化】阶段之后再开始,这是为了支持Java语言的运行时绑定【动态绑定】。
有且只有5种情况必须立即进行【初始化】
1)遇到new、getstatic、putstatic、invokestatic,对应的场景:使用new关键字实例化对象、读取或设置一个类的静态字段【非final】、调用一个类的静态方法。
2)使用反射对类进行调用。
3)当一个类初始化时,先初始化其父类。
4)当虚拟机启动时,用户需要指定一个要执行的主类【包含main方法】,虚拟机会先初始化这个主类。
5)使用JDK1.7的动态语言支持时,如果一个MethodHandler实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄。
- 例子:
public class A {
public static int value = 20;
public static final String name = "ljg";
}
public class B extends A {
}
通过子类引用父类中的静态字段,不会初始化子类。
即:B.value=33; 会实例化父类A,而不会实例化类B。
对于静态字段,只有直接定义这个字段的类才会被初始化。
访问被final修饰的静态字段,不会初始化该类。
即:访问A.name时,不会实例化类A。因为:被final修饰,已经在编译期把结果放入到常量池了。
数组定义引用类,不会触发该类的初始化。
即:A[ ] arr = new A[7],不会实例化A。
- 接口与类的区别
与上述中的【有且只有5种情况】,只有第3)种情况不同。一个类初始化时,要求先初始化其父类,但一个接口在初始化时,并不要求其父接口全部都完成初始化,只有在真正使用到父类接口的时候才会初始化。
1. 加载
加载阶段虚拟机完成的3件事:
1)通过一个类的全限定名来获取定义此类的二进制字节流。 获取的方式有:
(1)从ZIP包中读取,如:jar、ear、war。
(2)从网络中获取,如:Applet。
(3)运行时计算生成,如:动态代理。
(4)其它文件生成,如:由JSP文件生成对应的Class类。
2)将这个字节流代表的结构转化为方法区运行时的数据结构。
3)在内存中生成一个代表这个类的Class对象,作为方法区中这个类的各种数据的访问入口。【该Class对象可以在Java堆中,也可以像HotSpot虚拟机那样放在方法区】
数组类本身不通过类加载器创建,它由Java虚拟机直接创建。但数组类的元素是由类加载器去创建。
2. 验证
为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。Class文件并不一定要求用Java源码编译而出来,比如:用十六进制编辑器直接编写产生Class文件,可能会存在数组越界,类型转换异常的问题,尽管使用Java编译器编译时,会拒绝编译。
验证阶段对虚拟机来说不是必要的,对于已经被反复使用和验证过的代码,可以通过参数来关闭类的验证,以缩短虚拟机类加载的时间。
3. 准备
准备阶段正式为【类变量】分配内存并设置【类变量初始值】的阶段。注意:这时只对类变量【被static修饰的变量】进行内存分配,在【方法区】中进行分配。这个阶段不包含实例变量。
public static int value = 123;
准备阶段只是为value设初始值0而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法中,所以把value赋值为123的动作将在【初始化】阶段执行。
public static final int value = 123;
由于被final修饰,编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。
所以由final修饰的static静态常量,在准备阶段就会赋用户定义的值。
4. 解析
解析阶段是虚拟机将常量池内的【符号引用替换为直接引用】的过程。
invokedynamic指令用于动态语言支持,必须等到程序实际运行到这条指令的时候,解析动作才能进行。相对的,其余可触发解析的指令都是【静态】的,可以在刚刚完成加载阶段,还没有开始执行代码时就进行解析。
5. 初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余都是有虚拟机主导和控制。初始化阶段才是真正执行Java程序代码。在准备阶段,变量已经赋过一次系统要求的初始值,在初始化阶段则是初始化程序员主动计划的赋值。
类构造器<clinit>()
实例构造器<init>()
<clinit>()方法是由编译器自动收集类中的所有【类变量,非final修饰】的赋值动作和【静态语句块static{ }】中的语句合并产生的,收集的顺序由语句在源文件中出现的顺序决定。【静态语句块中只能访问到定义在静态语句块之前的变量,对于定义在它之后的变量,只能赋值,但不能访问】。如:
public class Test {
static {
i = 0; //ok,可以对i进行赋值
System.out.print(i); //error,不能访问
}
static int i = 1;
}
<clinit>()方法,虚拟机会保证先执行父类中的方法,而不需要显示调用。因此虚拟机中第一个被执行的<clinit>()方法的类肯定是Object。
接口中不能使用静态语句块,并且执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都阻塞等待,直到活动线程执行<clinit>()方法完毕。执行完毕后,其它线程不会再执行<clinit>()方法,因为同一个类加载器下,一个类型只会初始化一次。
网友评论