什么是java类
类文件指的是 .class文件
生命周期
各个阶段 按部就班地开始
注:是按部就班地"开始",而不是按部就班地"进行"或“完成”。(这些阶段通常都
是互相交叉地混合进行的, 会在一个阶段执行的过程中调用、 激活另一个阶段。)
解析==(涉及到动态绑定(晚期绑定))==可以在初始化之后再开始。
- java虚拟机规范没有强制规范“加载”的时机,但规范了初始化阶段的时机(加载,验证,准备自然需要已经完成了)
类加载时机
初始化时机
《Java虚拟机规范》严格规定只有六种情况必须立刻对类进行初始化
1) 遇到new、 getstatic、 putstatic或invokestatic这四条字节码指令时, 如果类型没有进行过初始
化, 则需要先触发其初始化阶段。
能够生成这四条指令的典型Java代码场景有:
- 使用new关键字实例化对象的时候。
- 读取或设置一个类型的静态字段(被final修饰、 已在编译期把结果放入常量池的静态字段除外)
的时候。 - 调用一个类型的静态方法的时候。
2) 使用java.lang.reflect包的方法对类型进行反射调用的时候, 如果类型没有进行过初始化, 则需
要先触发其初始化。
3) 当初始化类的时候, 如果发现其父类还没有进行过初始化, 则需要先触发其父类的初始化。
4) 当虚拟机启动时, 用户需要指定一个要执行的主类( 包含main()方法的那个类) , 虚拟机会先
初始化这个主类。
5) 当使用JDK 7新加入的动态语言支持时, 如果一个java.lang.invoke.MethodHandle实例最后的解
析结果为REF_getStatic、 REF_putStatic、 REF_invokeStatic、 REF_newInvokeSpecial四种类型的方法句
柄, 并且这个方法句柄对应的类没有进行过初始化, 则需要先触发其初始化。
6) 当一个接口中定义了JDK 8新加入的默认方法( 被default关键字修饰的接口方法) 时, 如果有
这个接口的实现类发生了初始化, 那该接口要在其之前被初始化。
这六种称为主动引用,其他所有引用类型都不会触发初始化,称为被动引用。
例:
package org.fenixsoft.classloading;
/**
* 被动使用类字段演示一:
* 通过子类引用父类的静态字段, 不会导致子类初始化
**/
public class SuperClass {
static {
System.out.println("SuperClass init!");
} p
ublic static int value = 123;
} p
ublic class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
} /
**
* 非主动使用类字段演示
**/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
上述代码运行之后, 只会输出“SuperClass init! ”, 而不会输出“SubClass init! ”。 对于静态字段,
只有直接定义这个字段的类才会被初始化,
package org.fenixsoft.classloading;
/**
* 被动使用类字段演示二:
* 通过数组定义来引用类, 不会触发此类的初始化
**/
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}
此处并没有触发org.fenixsoft.classloading.SuperClass的初始化阶段。 但是这段代码里面触发了另一个名为“[Lorg.fenixsoft.classloading.SuperClass”的类的初始化阶段, 对于用户代码来说, 这并不是一个合法的类型名称, 它是一个由虚拟机自动生成的、 直接继承于java.lang.Object的子类, 创建动作由字节码指令newarray触发。
这个类代表了一个元素类型为org.fenixsoft.classloading.SuperClass的一维数组, 数组中应有的属性和方法(用户可直接使用的只有被修饰为public的length属性和clone()方法) 都实现在这个类里。 Java语言中对数组的访问要比C/C++相对安全, 很大程度上就是因为这个类包装了数组元素的访问, 而C/C++中则是直接翻译为对数组指针的移动。 在Java语言里, 当检查到发生数组越界时会抛出java.lang.ArrayIndexOutOfBoundsException异常, 避免了直接造成非法内存访问。
package org.fenixsoft.classloading;
/**
* 被动使用类字段演示三:
* 常量在编译阶段会存入调用类的常量池中, 本质上没有直接引用到定义常量的类, 因此不会触发定义常量的
类的初始化
**/
public class ConstClass {
static {
System.out.println("ConstClass init!");
} p
ublic static final String HELLOWORLD = "hello world";
} /
**
* 非主动使用类字段演示
**/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}
上述代码运行之后, 也没有输出“ConstClass init! ”, 这是因为虽然在Java源码中确实引用了ConstClass类的常量HELLOWORLD, 但其实在编译阶段通过常量传播优化, 已经将此常量的值“hello world”直接存储在NotInitialization类的常量池中, 以后NotInitialization对常量 ConstClass.HELLOWORLD的引用, 实际都被转化为NotInitialization类对自身常量池的引用了。 也就是说, 实际上NotInitialization的Class文件之中并没有ConstClass类的符号引用入口, 这两个类在编译成Class文件后就已不存在任何联系了。
类加载(Class Loading)过程
加载(Loading)
java允许类预加载,但如果遇到.class缺失或者存在错误,类加载器必须在程序首次主动使用该类才报告错误(LinkageError)
加载是由类加载器完成的
具体类加载器加载类的过程见:
1) 通过一个类的全限定名来获取定义此类的二进制字节流。
类的全限定名:类名全称,带包路径的用点隔开,例如:java.lang.String
非限定类名:不带包名的(例如:String)
此处规定很宽松 所以:
- 从ZIP压缩包中读取, 这很常见, 最终成为日后JAR、 EAR、 WAR格式的基础。
- 从网络中获取, 这种场景最典型的应用就是Web Applet。
- 运行时计算生成, 这种场景使用得最多的就是动态代理技术, 在java.lang.reflect.Proxy中, 就是用
了ProxyGenerator.generateProxyClass()来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。 - 由其他文件生成, 典型场景是JSP应用, 由JSP文件生成对应的Class文件。
- 从数据库中读取, 这种场景相对少见些, 例如有些中间件服务器(如SAP Netweaver) 可以选择
把程序安装到数据库中来完成程序代码在集群间的分发。 - 可以从加密文件中获取, 这是典型的防Class文件被反编译的保护措施, 通过加载时解密Class文
件来保障程序运行逻辑不被窥探。
所以相较类加载其他阶段,非数组类型的加载阶段使开发人员可控性最强的阶段
数组: 数组类本身不通过类加载器创建(它是java虚拟机动态构造的)
但它的元素类型(Element Type 去掉所有维度的类型)还是要通过类加载器完成加载的,数组会根据不同的数组组件类型(Component Type指数组去掉一个纬度的类型)把数组与某个类加载器绑定(一个类型必须与类加载器一起确定唯一性)
如果数组的组件是引用类型,则数组将被标识在加载该组件类型的类加载器的类名称空间上
如果数组的组件类型不是引用类型,eg:int[]的组件类型为int,则Java虚拟机会把数组标记为与引导类加载器相关联。
数组类的可访问性与它的组件类型的可访问性一致, 如果组件类型不是引用类型, 它的数组类的
可访问性将默认为public, 可被所有的类和接口访问到。
2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
1.7的实现是老年代,1.8的实现是元空间
3) 在内存中生成一个代表这个类的java.lang.Class对象, 作为方法区这个类的各种数据的访问入口。
验证
验证是连接阶段的第一步
确保Class文件的字节流中包含的信息符合《Java虚拟机规范》 的全部约束要求, 保证这些信息被当作代码运行后不会危害虚拟机自身的安全
java源码编译产生的class一般无问题,但class文件来源很多,甚至是用键盘0,1地在二进制编辑器敲出来的。
准备
准备阶段是正式为类中定义的变量(即静态变量, 被static修饰的变量) 分配内存并设置类变量初
始值的阶段, 从概念上讲, 这些变量所使用的内存都应当在方法区中进行分配, 但必须注意到方法区
本身是一个逻辑上的区域, 在JDK 7及之前, HotSpot使用永久代来实现方法区时, 实现是完全符合这
种逻辑概念的; 而在JDK 8及之后, 类变量则会随着Class对象一起存放在Java堆中, 这时候“类变量在
方法区”就完全是一种对逻辑概念的表述了。
解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程
初始化
类的初始化阶段是类加载过程的最后一个步骤, 之前介绍的几个类加载的动作里, 除了在加载阶
段用户应用程序可以通过自定义类加载器的方式局部参与外, 其余动作都完全由Java虚拟机来主导控
制。 直到初始化阶段, Java虚拟机才真正开始执行类中编写的Java程序代码, 将主导权移交给应用程
序。
进行准备阶段时, 变量已经赋过一次系统要求的初始零值, 而在初始化阶段, 则会根据程序员通
过程序编码制定的主观计划去初始化类变量和其他资源。 我们也可以从另外一种更直接的形式来表
达: 初始化阶段就是执行类构造器()方法的过程。 ()并不是程序员在Java代码中直接编写
的方法, 它是Javac编译器的自动生成物, 但我们非常有必要了解这个方法具体是如何产生的, 以及
()方法执行过程中各种可能会影响程序运行行为的细节, 这部分比起其他类加载过程更贴近于
普通的程序开发人员的实际工作。
()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块) 中的
语句合并产生的, 编译器收集的顺序是由语句在源文件中出现的顺序决定的, 静态语句块中只能访问
到定义在静态语句块之前的变量, 定义在它之后的变量, 在前面的静态语句块可以赋值, 但是不能访
问,
public class Test {
static {
i = 0; // 给变量复制可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}s
tatic int i = 1;
}
内容来自: 深入理解Java虚拟机:JVM高级特性与最佳实践(第三版)- 周志明