在前面的日志当中,已经介绍过类的生命周期
类加载
将已经存在的class文件从磁盘当中加载到内存中,查找类的二进制数据,如果不存在直接抛出异常
连接
验证:确保被加载类的正确性,确保字节码没有被恶意修改
准备:为类的静态变量分配内存,并将其初始化为默认值,整型的默认值是0,引用类型是null, 程序当中赋值是1,但是这里会把int默认值设置为0,
解析:将类当中的符号引用转化为直接引用,符号引用是间接引用的表达方式,直接引用通过指针的方式指向目标方法的内存地址。
初始化
对类当中静态变量进行赋值,为类的静态变量赋予初始值
使用
在程序当中使用
卸载
在内存当中销毁,如果卸载掉,不能通过类创建对象
在什么情况下,类会进行初始化
所有的java虚拟机实现必须在每个类或者接口被java程序首次主动使用时才会初始化他们。
主动使用有以下七种情况
1、创建类的实例
2、访问某个类或者接口的静态变量,或者是对静态变量的赋值
3、调用类的静态方法(本质上和第二种情况一样的)
4、使用反射(如:Class.forName("java.lang.String"))
5、初始化一个类的子类(同时也会初始化这个子类的父类)
6、java虚拟机启动时被标记为启动类的类,包含了main方法的类,程序的入口
7、JDK1.7开始提供的动态语言支持:java.lang.invoke.MethodHandler实例的解析结果REF_getStatic, REF_putStatic,REF_invokeStatic 句柄对应的类,如果没有初始化,则进行初始化。
除了以上七种方式,其他方式情况下调用类,都不会触发类的初始化,有可能会进行类的加载,进行连接,但是并不会导致初始化。
接下来,使用具体的例子,来看下以上几种情况
public class Test1 {
public static void main(String[] args) {
System.out.println(MyChild1.str);
// System.out.println(MyChild1.str2);
}
}
class MyParent1{
public static String str = "hello world";
static {
System.out.println("my parent static block");
}
}
class MyChild1 extends MyParent1{
public static String str2 = "str2";
static {
System.out.println("my child static block");
}
}
在这两个类当中,分别都创建的有静态代码块,如果类被初始化的时候,就会打印出其对应的静态代码块
当我们打印MyChild1.str, 即父类的静态变量时,这个时候的代码执行结果如下:
my parent static block
hello world
虽然我们是通过MyChild的方式去访问父类当中的静态变量的,但是并没有初始化MyChild,只初始化了MyParent类,但是有没有加载MyChild1我们可以通过设置JVM参数为-XX:+TraceClassLoading查看是否加载了MyChild1
如果我们调用MyChild.str2,即MyChild2当中的静态变量,我们看下执行结果如下
my parent static block
my child static block
str2
可以看到这个时候,不仅初始化了MyParent还初始化了MyChild类,
关于我们上面定义的7种主动使用场景,上面这个例子就满足了两种,调用类的静态变量,和初始化子类时,初始化父类。
还需要注意一点:对于静态代码字段来说,只有直接定义了该字段的类才会被初始化。
Example2:
public class Test2 {
public static void main(String[] args) {
System.out.println(MyParent2.str);
}
}
class MyParent2 {
public static final String str = "hello";
static {
System.out.println("my parent2 static block");
}
}
执行结果:
hello
可以看到,MyParent2当中,变量str是一个常量值,并且没有初始化MyParent2这个类,原因在于,final类型的变量,是个常量值,在编译阶段,这个常量就会被保存到调用这个常量的方法所在的类的常量池当中,我们当前这个类中,str的值“hello”会被放在Test2类的常量池中,本质上,调用类并没有直接引用到定义常量的类,不会触发定义常量的类初始化。这个时候,如果我们手动删除掉MyParent2编译的class文件,再次执行程序,是不会有影响的,因为字符串str已经被放入到Test2的常量池中了
Example3:
public class Test3 {
public static void main(String[] args) {
System.out.println(MyParent3.str);
}
}
class MyParent3{
public static final String str = UUID.randomUUID().toString();
static {
System.out.println("My Parent3 static code");
}
}
执行结果:
My Parent3 static code
9a33d583-41a3-4f28-bf02-27b4b5bcea40
这个例子当中我们访问的变量也是一个静态常量,但是却初始了MyParent3这个类,两者不同的地方在于,一个是编译期常量,一个是运行期常量,如果一个常量的值不是在编译期可以确定的话,那么它的值就不会被放置在调用方法所在的类的常量池当中,在程序运行时,会导致主动使用这个常量所在的类,就会导致这个类的初始化。
Example4:
public class Test4 {
public static void main(String[] args) {
MyParent4 p = new MyParent4();
}
}
class MyParent4{
static {
System.out.println("my parent4 static code");
}
}
上面这个例子很明显会打印出来一条语句,复合我们前面说的,实例化一个类的时候,就需要初始化这个类。
Example5:
public class Test4 {
public static void main(String[] args) {
MyParent4[] arr = new MyParent4[1];
}
}
class MyParent4{
static {
System.out.println("my parent4 static code");
}
}
上述例子执行之后,并不会初始化MyParent4,原因在于new一个数组时,确实创建了一个实例,但是这个实例的类型并不是我们的MyParent4类型,可以打印出来,看到数组类型是 [Lcom.jvm.classloader.MyParent4 ,在原来类型的基础上添加了[L, 可以看到我们代码当中并没有声明这个类型。对于数组实例,其实在运行时,虚拟机动态生成的。
Example6:
public class Test5 {
public static void main(String[] args) {
System.out.println(MyChild5.a);
}
}
interface MyParent5{
public static final int a = 5 ;
public static Thread thread1 = new Thread(){
{
System.out.println("my parent invoked!");
}
};
}
interface MyChild5 extends MyParent5 {
public static int b = new Random().nextInt(4);
public static Thread thread = new Thread(){
{
System.out.println("mychild invoked");
}
};
}
1、如果程序当中打印的MyChild5.a 执行结果是 只有5
2、如果程序当中打印的是MyChild5.b 执行结果是 mychild invoked 和数字3
interface当中的变量都是static final 类型的,如果变量是编辑期变量,那么变量的值会在编译期拷贝至调用方法所在类的常量池当中,如果调用的变量是运行期常量,那么需要初始化变量所在的接口,或者类,这里我们定义了个一个Thread的静态变量,如果接口进行了初始化操作,那么需要给静态变量进行赋值,就会打印出来信息。
总结:在一个接口进行初始化时,并不要求其父接口也完成初始化,同样,如果子类在初始化时,并不要求其父接口进行初始化
Example7
public class MyTest10 {
static {
System.out.println("myTest10 block");
}
public static void main(String[] args) {
Parent10 parent10;
System.out.println("-----");
parent10 = new Parent10();
System.out.println("-------");
System.out.println(parent10.a);
System.out.println("-------");
System.out.println(Child10.b);
}
}
class Parent10{
static int a = 3;
static{
System.out.println("parent 2");
}
}
class Child10 extends Parent10{
static int b = 4;
static{
System.out.println("child 2");
}
}
执行结果:
myTest10 block
-----
parent 2
-------
3
-------
child 2
4
结合我们上面所说的主动使用的七种情况,MyTest10当中包含main方法,因此会初始化,如果只是声明了一个类引用,但是并没有指向具体的对象,并不会初始化,只有在实例化时,才会初始化这个对象
Example8
class Parent11{
static int a = 3;
static {
System.out.println("parent11 static block");
}
static void doSomeThing() {
System.out.println("do something");
}
}
class Child11 extends Parent11{
static{
System.out.println("child11 static block");
}
}
public class MyTest11 {
public static void main(String[] args) {
// System.out.println(Child11.a);
Child11.doSomeThing();
}
}
执行结果
parent11 static block
do something
调用静态方法也会导致类的初始化
Example9
class CL{
static {
System.out.println("CL static");
}
}
public class MyTest12 {
public static void main(String[] args)throws Exception {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class<?> clazz = classLoader.loadClass("com.shengsiyuan.jvm.classloader.CL");
System.out.println(clazz);
System.out.println("------");
clazz = Class.forName("com.shengsiyuan.jvm.classloader.CL");
System.out.println(clazz);
}
}
执行结果
class com.shengsiyuan.jvm.classloader.CL
------
CL static
class com.shengsiyuan.jvm.classloader.CL
调用loadClass并不会初始化类,只有使用反射时,才会初始化类
以上就是所有主动使用的情况下,初始化类的操作。如有错误欢迎指正~
参考:圣思园-jvm课程