JVM学习笔记:类加载、连接及初始化
类加载:
在JAVA代码中,类型的加载,连接,初始化都是在程序运行期间完成的。
类型:
指的是定义class,或interface或者枚举,并不涉及对象或实例。
为什么要在运行期间完成类型的加载,连接,初始化:
为了提供更灵活的使用方式,例如动态代理。
类型的加载:
最常见的情况的是把编译好的字节码文件从硬盘上加载到内存中。
类型的连接:
将类与类之间的关系处理好,并且对于字节码的相关处理也是在这一阶段完成的。
虚拟机处理的对象就是字节码。字节码有问题,虚拟机就会不执行。
另外,字节码虽然是大部分情况是编译器帮助产生的,但本身是可以被人为操纵的,因此有存在错误的可能,所以需要进行处理。
子步骤如下:
1、验证阶段:确保被加载的类的正确性。
2、准备阶段:为类的静态变量分配内存,并将其初始化为默认值。
为什么是静态变量?
类的静态变量或静态方法通常具有“全局”属性,在这时候类实例还没被创建。为了之后的创建类实例的过程使用静态变量,所以在这个时候初始化静态变量。静态变量在准备阶段的初始化赋初值是0或null。
public class Test{
public static int a = 1;
}
在该类的加载中,在连接的准备阶段,JVM会为静态整型变量a开辟一段内存空间,并且赋的值是0 而不是1。1的赋值是在类型初始化这一阶段完成的。
3、解析阶段:
把类的符号引用转化为直接引用:
符号引用:间接的符号引用
直接引用:指针的引用
例如:调用某方法a()为符号引用方式,而在直接引用则是将这个a()转为直接的指针引用关系。
类型的初始化:
这一阶段的主要工作是给初始化的静态变量赋初值
类加载器:
JAVA的每一个类型,最终的数据结构通过类加载器被纳入到JVM的管理的内存下。就是加载类的工具。
另外还有两个阶段:类的使用阶段,类的卸载阶段
使用阶段:就是类在运行使用的过程
卸载阶段:类从内存中消除的过程
类的加载、连接、初始化过程中的细节
类的加载细节:
类的加载是指将.class文件加载至内存中,放在运行时数据区的方法区内,然后在内存中创建一个java.lang.Class对象(JVM规范并为说明这个对象放置在上面区域,hotspot放置在了方法区中),这个Class对象用于封装类在方法区内的数据结构。
(没错,反射使用的class对象就是这个class对象)
加载Class的方式:
1、通过类路径加载(文件系统加载)
2、通过网络下载.class文件
3、从zip,jar等归纳文件中加载.class文件
4、从专有的数据库中提取.class文件
5、将java源文件动态编译成.class文件(动态代理,运行时编译)
类的初始化细节
JAVA在类的初始化阶段,由于不同的使用方式分为两种
1、主动使用:
所有的JVM的实现必须要在每个类或者接口在JAVA程序首次主动使用才进行初始化
内涵:主动,只有程序主动调用才会进行初始化。首次,也就是类型的初始化只会调用一次
大概包含7种情况:
1、创建类的实例
2、访问某个接口或者类的静态变量或者修改静态变量的值
3、访问静态方法
4、反射
5、当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化
6、JAVA虚拟机启动时被标明为启动类的类(包含main方法的类)
7、从JDK1.7后提供的动态语言支持
总之把握一个原则:某个类在首次使用时才会被初始化。
主动使用例子:
/**
* -XX:+TraceClassLoading jvm参数 追踪类的加载的信息并打印
* -XX:+<option> 表示开启option选项
* -XX:-<option> 表示关闭option选项
* -XX:<option> = <value> 表示将option的选项设置为value
*/
class SuperClass{
public static String str1 = "SuperClass";
static {
System.out.println("初始化SuperClass");
}
}
class SubClass extends SuperClass{
public static String str2 = "SubClass";
static {
System.out.println("初始化SubClass");
}
}
public class Test1 {
public static void main(String[] args) {
System.out.println(SubClass.str2);
}
}
结果:
初始化SuperClass
初始化SubClass
SubClass
虽然调用的子类的静态字段,由于符合主动使用的第五种情况即“当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化”,所以先对父类进行初始化,执行父类的静态代码块,然后再初始化子类
利用JVM参数,观察类的加载
可以发现,最先加载的类都是java.lang.Object这个类
加载顺序是 父类-子类
2、被动使用:
除了上述7种,其他的使用都是被动使用,并不会导致类的初始化。而加载和连接是有可能的。
class SubClass2 {
public final static String str = "SubClass2";
static {
System.out.println("SubClass2 block");
}
}
public class Test2 {
public static void main(String[] args) {
System.out.println(SubClass2.str);
}
}
输出结果:
SubClass2
静态常量在编译时就会被放入调用类的常量池中,因此不会使用到定义常量的类,不符合主动使用的任一情况,因此不会触发类的初始化。
常量池中的常量与调用类没有任何关系了,你甚至可以在编译完后将调用类的.Class字节码文件删除,然后也可以输出该静态常量的值
当然,如果把final关键字去掉,就会主动调用SubClass,这种情况就变成了主动使用,因此会对该类进行初始化。
反编译之后的代码:
ldc助记符表示:将int,float或者string类型的常量值从常量池推至栈顶
JAVA虚拟机与程序的生命周期:
在如下几种情况下,JAVA虚拟机将结束生命周期:
1、显示调用System.exit()方法
2、程序正常执行结束(最常见)
3、程序在执行过程当中遇到错误或者异常导致异常终止
4、由于操作系统出现的错误导致的JVM终止