类的生命周期
一个类在运行过程中, 会经历五个阶段:
加载
连接
- 验证: 验证类的正确性
- 准备: 对类的静态成员变量进行初始化赋值(不是赋真正的值)
- 解析: 将类之间的符号引用改为直接引用
初始化
使用
卸载
<1> java虚拟机与程序的生命周期
<2> 在如下几种情况下, java虚拟机将结束生命周期:
<1> 执行了System.exit()方法
<2> 程序正常执行结束
<3> 程序在执行过程中遇到了异常或错误而异常终止
<4> 由于操作系统出现错误而导致java虚拟机进程终止
类的加载
类的加载指的时将类的.class文件中的二进制数据读入内存, 并将其放在运行时数据区的方法区内, 然后创建一
个对应的Class对象(规范并未说明Class对象位于哪里, HotSpot虚拟机将其放在方法区中)用来封装类在方法区
中的数据
类加载的方式
<1> 直接从本地加载.class文件
<2> 通过下载网络上的.class文件
<3> 从zip, jar等归档文件中加载.class文件
<4> 从专有数据库中提取.class文件(少用, 了解即可)
<5> 将java源文件动态编译为.class文件, 即动态生成java源文件
<1> 动态代理: 会自动生成类.java文件, 然后动态编译成.class文件后加载进内存
<2> jsp文件, 会编译成.java文件, 然后编译成.class文件
类的连接
类的连接分为三个步骤:
<1> 验证: 类文件的结构检查, 语义检查, 字节码验证, 二进制兼容性的验证
<2> 准备: 将类中的静态成员属性进行初始化赋默认值, 这个赋值和初始化阶段的赋值是不一样的
<3> 解析: 将类与类之间的符号引用转换为直接引用
class Test {
public static int a = 1;
}
对于上面这个类来说, 在连接阶段的准备步骤, 会将a初始化为0, 如果是布尔类型就初始化为false, 如果是对
象类型就初始化为null, 而在初始化阶段, 就会将真正的值赋予该静态成员属性
类的初始化之主动使用和被动使用
在说类的初始化之间, 我们先引入一句话: "除了对类的主动使用会导致类的初始化, 其它使用类的方式都被看做
是被动使用, 并且被动使用不会导致类的初始化, 所有的Java虚拟机实现必须在每个类或接口被Java程序"首次
主动使用"时才初始化它们", 看完这句话, 我们肯定会有疑问, 什么是类的主动使用和被动使用?
Java程序对类有两种使用方式: 主动使用和被动使用
主动使用:
<1> 创建类的实例
<2> 使用类的静态成员属性或者对静态成员属性进行赋值
<3> 使用类的静态成员方法
<4> 初始化一个类的子类(初始化一个类的子类会导致所有的父类被初始化)
<5> 反射( class.forName(类名) )来主动加载类
<6> Java虚拟机启动时被标明为主动使用的类( 如命令行运行 java Test )
<7> JDK1.7开始提供的动态语言支持:
java.lang.invoke.MethodHandle实例的解析结果REF_getStatic, REF_putStatic,
REF_invokeStatic句柄对应的类没有初始化, 则初始化
初始化阶段的内容:
Java虚拟机执行类的初始化语句, 为类的静态变量赋予初始值, 初始化有两种情况, 一种是通过静态成员变量
直接显示赋值, 一种是在静态代码块中赋予初始值, Java虚拟机会按照初始化语句在类中的顺序来进行初始化
初始化规范:
<1> 在初始化一个类的时候, 并不会先初始化它所实现的接口
<2> 在初始化一个接口时, 并不会先初始化它的父接口(接口的属性是final修饰的)
因此, 一个父接口并不会因为它的子接口或者实现类的初始化而初始化, 只有当程序首次使用特定接口的静态
变量时, 才会导致该接口的初始化(可以通过接口中放入一个线程变量来验证)
<3> 只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时, 才可以认为是对类或接口的主
动使用
<4> 调用ClassLoader类的loadClass方法加载一个类, 并不是对类的主动使用, 不会导致类的初始化
类的初始化例子
例一
class Parent { public static String str = "嘿嘿"; public static int a = 1; static { System.out.println( "父类被初始化" ); } } class Child extends Parent { public static int a = 2; static { System.out.println( "子类被初始化" ) } } 分析: 当调用Child.str时, 对于静态字段来说, 只有直接定义了该字段的类才会被初始化, 所以此时只有Parent 类被初始化, 所以会执行父类的静态代码块, 当调用Child.a时, 此时是对子类初始化了, 对子类的初始化会 导致所有的父类被初始化, 从而会使得父类和子类的静态代码块都被执行 注意: 对于" 只有直接定义了该字段的类才会被初始化 "这句话来说, 虽然在调用Child.str时Child的类没有被 初始化, 但是该类是被加载了的, 可以通过添加JVM参数-XX:+TraceClassLoading来查看
例二
public class TestClass2 { public void test () { System.out.println( TestClass2Parent.b1 ); } } class TestClass2Parent { public final static byte b1 = 100; static { System.out.println( "Parent被初始化" ); } } 分析: 这次仅仅在TestClass2Parent类的静态成员属性上添加了一个final, 然后我们就会发现静态代码块 就没有执行了, 这是因为对于final修饰的常量, 在编译时编译器能够确定b1的值, 所以就将其放入 了TestClass2这个类的常量池中, 这时我们即使把TestClass2Parent这个类的class文件删除也是 能够正常输出该值的 修改类TestClass2Parent: class TestClass2Parent { public final static byte b1 = (byte)( Math.random() * 100 ); static { System.out.println( "Parent被初始化" ); } } 分析: 我们这时再去执行的时候, 会发现静态代码块执行了, 这是因为我们在编译时不能确认b1的值, 所以就 会主动使用TestClass2Parent类, 从而加载了这个类, 这时我们如果把TestClass2Parent类的class 文件删除, 那么就会把类找不到异常了
编译期常量及运行期常量的区别
常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中, 本质上, 调用类并没有直接引用到定义常量
的类, 因此并不会触发定义常量的类的初始化
注意: 这里指的是将常量存放到了TestClass2类的常量池中, 之后Test和原来的类(TestClass2Parent)就没有
任何关系了, 甚至我们可以将原来的类的class文件删除
当一个常量的值并非编译期间可以确定的, 那么其值就不会被放到调用类的常量池中, 这时在程序运行时, 会导致
主动使用这个常量所在的类, 显然会导致这个类被初始化
数组是否会导致类的初始化
class TestClass2Parent {
public final static long b1 = 5;
static {
System.out.println( "Parent被初始化" );
}
}
public void test () {
TestClass2Parent[] arr = new TestClass2Parent[1];
}
分析: 我们运行这个test函数, 会发现TestClass2Parent这个类是没有被初始化的, 对于数组实例来说, 其类
型是由JVM在运行期动态生成的, 表示为[Lcom.xxx.xxx.xxx这种形式, 动态生成的类型, 其父类型就
是Object
类初始化之初始化顺序验证
class Singleton {
public static int a = 0;
public static int b;
private static Singleton instance = new Singleton();
private Singleton () {
a++;
b++;
}
public static Singleton getInstance () {
return instance;
}
}
public void test () {
Singleton.getInstance();
System.out.println( Singleton.a ); // 1
System.out.println( Singleton.b ); // 1
}
分析: 首先上面的Singleton是一个饿汉式单例模式, 在类中添加了两个静态成员变量a, b, 并且a被赋予初始
值0, 然后调用空参构造方法的时候会对a,b进行加1操作, 下面是一个测试类, 首先调用了getInstance
方法, 从而是主动使用Singleton类, 进而会对这个类进行初始化, 然后我们对a, b进行输出, 得到的
结果都是1, 接下来我们对Singleton类的代码进行下位置的调换
class Singleton {
private static Singleton instance = new Singleton();
public static int a = 0;
public static int b;
private Singleton () {
a++;
b++;
}
public static Singleton getInstance () {
return instance;
}
}
分析: 此时我们再去重新执行测试函数, 就会发现a的值为0, b的值为1, 仅仅只是调换了一下位置, 就会使得
结果不一样, 这是什么原因呢??
原因分析:
<1> 以第二种情况来分析, 首先执行Singleton.getInstance(), 这是对Singleton类的主动使用, 既然
是首次主动使用, 那么就会使得Singleton类被加载, 连接, 初始化
<2> 加载: 将Singleton类class文件二进制数据加载进内存, 创建Class对象
<3> 连接: 首先会验证Singleton类class文件的正确性, 然后为该类的静态成员变量进行内存空间的分配,
然后jvm会对这些变量进行赋初值, 即instance为null, a和b都为0, 最后执行解析操作, 将类与类之
间的符号引用转换为直接引用
<4> 初始化: instance初始化为Singleton类的对象, 然后执行了a++和b++, 此时a和b的值都为1, 然后
执行a = 0语句, 从而使得a的值由1变为0, b因为没有被主动赋予值, 所以保留原来b++后的值1
<5> 从而得出结果为0, 1
初始化对于类与接口的异同点
当Java虚拟机在初始化一个类的时候, 要求它的所有父类都被初始化, 但是这条规则并不适用于接口
<1> 在初始化一个类的时候, 并不会先初始化它所实现的接口
<2> 在初始化一个接口时, 并不会先初始化它的父接口
因此一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量
时, 才会导致该接口的初始化
初始化对于类与接口的异同点的验证
验证1: 初始化一个类的时候, 它的所有父类都会被初始化(不进行验证)
验证2: 初始化一个类的时候, 不会先初始化它所实现的接口(但是会加载)
第一步: 在类定义的时候, 可以引入一个对象代码块(非静态代码块), 之后每次创建这个代码块的实例都会执 行这个代码块, 如下: class ThreadChild extends Thread { { System.out.println( "ThreadChild 被创建实例" ); } @Override public void run () { } } 分析: 我们创建了一个线程Thread类的子类, 并添加了一个对象代码块, 之后我们每次调用new ThreadChild 都会执行这个对象代码块, 利用这个特性, 我们来验证标题的那句话 class ThreadChild extends Thread { { System.out.println( "ThreadChild 被创建实例" ); } @Override public void run () { } } interface T5Parent { public static final Thread thread = new ThreadChild(); } class T5Child implements T5Parent { public static int a = 1; static { System.out.println( "T5Child被初始化" ); } } 分析: 首先我们在T5Parent接口中放入了一个Thread类型的实例, 那么如果这个接口被初始化了, 那么就会 执行new ThreadChild()这个语句, 从而会执行ThreadChild中的对象代码块, 反之则不会执行 测试代码: System.out.println( T5Child.a ); 结果: [class,load] com.fightzhong.jvm.classloader.T5Parent source [class,load] com.fightzhong.jvm.classloader.T5Child source [class,load] com.fightzhong.jvm.classloader.ThreadChild source T5Child被初始化 1 分析: 首先我们通过添加了JVM参数来查看类加载的信息, 发现T5Parent, T5Child, ThreadChild都被加载 了, 但是"ThreadChild 被创建实例"这句话却并没有被执行, 从而得出了结论: 初始化一个类的时候, 不会先初始化它所实现的接口(但是会加载) 测试代码: System.out.println( T5Parent.thread ); 结果: com.fightzhong.jvm.classloader.T5Parent source com.fightzhong.jvm.classloader.ThreadChild source ThreadChild 被创建实例 Thread[Thread-0,5,main] 分析: 当我们主动使用该接口的时候, 就导致了该接口的初始化
验证3: 在初始化一个接口时, 并不会先初始化它的父接口(但是会加载)
这里我们简写一下Thread子类的创建方式, 通过内部类的形式来创建 // 父类接口 interface T6Parent { Thread thread1 = new Thread () { { System.out.println( "父类接口被实例化" ); } }; } // 子类接口 interface T6Child extends T6Parent { int a = 1; Thread thread2 = new Thread () { { System.out.println( "子类接口被实例化" ); } }; } 测试代码: System.out.println( T6Child.thread2 ); 结果: [class,load] com.fightzhong.jvm.classloader.T6Parent source [class,load] com.fightzhong.jvm.classloader.T6Child source [class,load] com.fightzhong.jvm.classloader.T6Parent$1 source [class,load] com.fightzhong.jvm.classloader.T6Child$1 source 子类接口被实例化 Thread[Thread-0,5,main] 分析: 通过结果可以看到, 我们输出了T6Child的thread2, 会加载T6Parent, T6Child, T6Parent$1, 以及 T6Child$1类, 但是只输出了语句"子类接口被实例化", 所以可以断定父类接口是没有被初始化的, 从而 验证了标题的话
类实例化
类实例化:
<1> 为新的对象分配内存
<2> 为实例变量赋予默认值
<3> 为实例变量赋予正确的初始值
java编译器为它编译的每一个类都至少生成一个实例初始化方法, 在java的class文件中, 这个实例初始化方法
被称为"<init>"。针对源代码中每一个类的构造方法, Java编译器都产生一个<init>方法
类的卸载
由Java虚拟机自带的类加载器所加载的类, 在虚拟机的生命周期中, 始终不会被卸载。前面已经介绍过, Java
虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。java虚拟机本身会始终引用这些类加载
器, 而这些类加载器则会始终引用它们所加载的类的class对象, 因此这些class对象始终是可触及的, 由用户
自定义的类加载器所加载的类是可以被卸载的