在 Java 代码中,类型(class, interface, enum 等 )、连接与初始化过程都是在程序运行期间完成的。
static int a = 1
- 加载:查找并加载类的二进制数据,最常用的一种,将在磁盘上编写的类型加载到内存中
- 连接:
- 验证:确保被加载类的正确性
- 准备:为类的静态变量分配内存,并初始化为默认值,这一步的时候 a 是 int 类型的,默认值 为 0 ,
a = 0
- 解析:将类与类之间的关系确立(列如将一个类型的符号引用转换为直接引用)
- 初始化:对类中的静态变量赋予正确的初始值,这一步正确的初始值 1 赋值给 a,
a = 1
类加载器 class loader :将每个 Java 类型加载到内存中
在以下情况下,Java 虚拟机结束生命周期
- 执行了 System.exit() 方法
- 程序正常执行结束
- 程序正在执行过程中遇到了异常或者错误而异常终止
- 由于操作系统出现错误而导致 Java 虚拟机进程终止
初始化
所有的 Java 虚拟机实现必须在每个类或者接口被 Java 程序 首次主动使用 时才初始化。
主动使用:
- 创建类的实例
- 访问某个类或者接口的静态变量,或者是对这个静态变量赋值
- 调用类的静态方法
- 反射 (
Class.forName("java.lang.String")
) - 初始化一个类的子类
- 虚拟机启动时被标记为启动类的类 Main方法
除了上述的方法以外,其他使用类的方法都视为对类的 被动使用 不会对类进行 初始化。
下面看一个例子代码演示:
定义一个父类,含有一个静态代码块,一个静态字段
1 | class Parent{ |
再定义一个子类,同样含有一个静态代码快,一个静态字段
1 | class Child{ |
对于下面的 main 方法执行结果是什么呢?
1 | public static void main(String[] args){ |
执行的结果是:
1 | Parent static block |
那么,再看看对于下面的 main 方法执行结果又是什么呢?
1 | public static void main(String[] args){ |
执行结果是:
1 | Parent static block |
原来,对于静态字段来说,只有直接定义了该字段的类才会使用,所以对于第一次执行结果,只有父类的静态代码块得到了执行。
对于第二次执行结果,在初始化一个子类的时候,对于他的所有父类也视作主动使用,也会进行初始化。所以父类和子类的静态代码块都会得到执行。
但是修改一下 Parent 类的代码,将 str 设置为一个常量:
1 | class Parent{ |
这个时候执行下列 main 方法执行结果是什么呢?
1 | public class test{ |
结果是只会打印 hello,并不会打印出 Parent 类的 static 块。
主要原因是:常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中,本质上,调用类并没有直接引用到定义常量的类,因此定义常量的类并不会进行初始化
那么对于所有的常量都会被放到调用常量的类的常量池中吗?
修改 Parent 类的代码,添加一个 String 类型的常量,只不过这个常量在编译期间是不可确定的。
1 | class Parent { |
再在 main 方法中执行,结果是输出了随机 32 位的 ID 的同时也输出了 static 代码块的内容。
所以当一个常量的值并非编译期间可以确定的时候,这个常量并不会放到调用类的常量池中,当程序运行时,会导致主动使用这个常量所在的类,所以常量所在的类会进行初始化。
对于一下代码
1 | public class Test { |
执行结果是:
1 | 1 |
为什么会产生这样的结果呢?
是因为在 main 方法中是通过 getSingleton()
这个静态方法来获得 Singleton 的实例对象的。而首次调用一个类的静态方法会经过连接阶段的准备阶段,这个时候 num1 和 num2 是 int 类型的,所以都被初始化为 0。然后是通过 new 关键字获得一个实例对象,也就是初始化阶段。由类中定义的语句从上向下初始化,在构造方法中,num1 和 num2 都加一被改写为 1。而初始化阶段会把静态变量赋予正确的初始值,这个例子中 num2 正确的初始值是 0。所以最后结果 num1为 1,num2 为 0。但是构造方法内 num1 和 num2 都为 1。
类的加载
类的加载是指将类的 .class 文件中的二进制数据读入到内存中,将其放在运行是数据区 方法区 内,然后在内存中创建一个 Java.lang.class
对象,用来封装类在方法区内的数据结构。
加载 .class 文件的方式:
- 从本地系统中直接加载
- 通过网络下载 .class 文件
- 从 zip, jar 等归档文件中加载 .class 文件
- 从专有数据库中提取 .class 文件
- 将 Java 源文件动态编译为 .class 文件
虚拟机参数
-XX:+<option>
,表示开启 option 选项-XX:-<option>
, 表示关闭 option 选项-XX:<option> = <value>
, 表示将 option 选项的值设置为 value
通过在 jvm 虚拟机配置中添加参数:
-XX:+TraceClassLoading
可以打印出程序加载类的顺序
助记符
- ldc 表示将 int,float,String 类型的常量值从常量池中推送至栈顶
- bipush 表示将单字节 (-128 ~ 127) 的常量值推送至栈顶
- sipush 表示将一个短整型常量值 (-32768 ~ 32767) 推送至栈顶
- iconst_1 表示将 int 类型的 1 推送至栈顶,同样的还有 iconst_m1 ~ iconst_5
- anewarray 表示创建一个引用类型(如类,接口 等)的数组,并将其引用值压入栈顶
- newarray 表示创建一个指定的原始类型(如 int、float、char 等)的数组,并将其引用值压入栈顶
类的生命周期
- 加载:把二进制形式的 Java 类型读入 Java 虚拟机
- 验证:验证二进制形式的 Java 文件是否有错误
- 准备:为类变量分配内存,设置默认值。在初始化完成以前,类变量都没有初始化为正确的初始值
- 解析:在类常量池中寻找类,接口,字符和方法的符号引用,把这些符号引用替换为直接引用
- 初始化:为类变量赋予正确的初始值
- 类实例化:
- 为新的对象分配内存
- 为实例变量赋予默认值
- 为实例变量赋予正确的初始值
- Java 编译器为每个编译的类至少生成一个实例初始化方法,对应类的构造方法。在 class 文件对于每个构造方法,编译器都生成一个
<init>
方法
- 垃圾回收和对象终结
类加载器
Java 虚拟机提供了以下 3 种类加载器:
- 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录的,或者是通过
-Xborclasspath
参数指定路径中的,被虚拟机认可的(以文件名识别,如 jar 结尾的)类。 - 扩展类加载器(Extension ClassLoader): 负责加载 JAVA_HOME\lib\ext 目录中,或者是通过 Java.ext.dirs 系统变量指定路径中类库。
- 应用程序类加载器(Application Class Loader): 负责加载用户路径 ClassPath 上的类库
Java 虚拟机通过双亲委托模型来进行类的加载,当然也可以自定义类继承 java.lang.ClassLoader
实现自定义类加载器。
双亲委托:一个类加载器收到了类加载的请求,它首先自身不会尝试去加载这个类,而是把这个请求委托给父类去完成。只有当父类加载器无法完成这个加载请求的时候,它自身才会尝试加载。这样做的好处是,对于一个类保证了使用不同的类加载器都尽量获得同一个 Object 对象。