JAVA类加载和初始化
类的生命周期如下图所示:
Java类加载机制
Java类加载器
引导(启动)类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等 bootstrtload------>jre/lib目录
扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JA类包 extClassLoad------->jre/lib/ext目录下
应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类 appClassLoad------>classPath目录下
自定义加载器:负责加载用户自定义路径下的类包
Java类加载原理
类加载原理:主要使用双亲委派机制
双亲委派机制:一个类进来先由应用类加载器------->扩展类加载器------->启动类加载器 然后启动类建加载器尝试加载 因为启动类加载器去加载jre包下面的lib目录 而我们写的类在classpath下面 启动类加载器加载不到就会像扩展类加载器(默认加载ext包下面的)去下发------->应用类加载器 (从claaPath下面查找)
为什么要设计双亲委派机制?
沙箱安全机制:自己写的java.lang.String.class类不会被加载(包名和类名和jdk自带的一样 运行不成功),这样便可以防止核心API库被随意篡改(自己在自定义类加载器里面要加载java.lang…类似于和核心包里面系统的包名称 jvm直接报一个安全异常) jvm加载到和自己核心类库里面相同的类必须用自己类库里面的类。
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次(父类就直接返回了),保证被加载类的唯一性(相同包名类名只加载一份)。
Java程序初始化顺序:
初始化是执行类构造器clinit()方法的过程。
clinit()方法是由编译器自动收集类中的所有类变量(被static修饰的变量)和静态代码块(static{}块)中的语句合并产生的。
所以验证类有没有被初始化就可以看它的静态块有没有执行。
- 父类的静态代码块
- 子类的静态代码块
- 父类的普通代码块
- 父类的构造方法
- 子类的普通代码块
- 子类的构造方法
- 为什么静态优先,构造随后?
首先static修饰的成员变量和方法是从属于类的, 而普通变量和方法是从属于对象的,在调用某个类的构造方法之前,应该先加载类信息,包括静态初始化块!之后才能创建对象! - 为什么先父再子?
由于子类继承了父类,就拥有了父类的所有属性和方法(除了构造方法),但不一定可以直接访问(私有属性和方法) 子类去创建对象的时候,构造方法第一句总是:super(…)来调用父类对应的构造方法。
测试代码
class Father {
static {
System.out.println("父类静态代码块");
}
{
System.out.println("父类非静态代码块");
}
public Father() {
System.out.println("父类构造函数");
}
}
public class Son extends Father {
static {
System.out.println("子类静态代码块");
}
{
System.out.println("子类非静态代码块");
}
public Son() {
System.out.println("子类构造函数");
}
public static void main(String[] args) {
Son son = new Son();
}
}
结果
下面的输出有些不一样,非静态代码块和静态代码块中的方法只有在调用的时候才会输出
class Father {
static {
System.out.println("父类静态代码块");
}
public void say()
{
System.out.println("父类非静态代码块");
}
public Father() {
System.out.println("父类构造函数");
}
}
public class Son extends Father {
static {
System.out.println("子类静态代码块");
}
public void say1()
{
System.out.println("子类非静态代码块");
}
public Son() {
System.out.println("子类构造函数");
}
public static void main(String[] args) {
Son son = new Son();
}
}
父类静态代码块
子类静态代码块
父类构造函数
子类构造函数Process finished with exit code 0
Java类不会初始化的情况
1、2、3是子类不会初始化的情况
1、通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。子类和父类都会被加载。
2、定义对象数组,不会触发该类的初始化。该类会加载
3、子类调用的是父类的final方法或者字段
4、final定义常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。也不会加载常量所在的类。运行期间确定值得常量除外
5、通过类名获取 Class 对象,不会触发类的初始化,会加载。
6、通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。会加载类。
7、通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作(加载了,但是不初始化)
测试代码
子类
public class A extends B{
static {
System.out.println("A初始化");
}
}
父类
public class B {
public static int b = 10;
static {
System.out.println("B初始化");
}
}
C类
public class C{
public static final int NUM = new Random().nextInt(10);
public static final String HAN="CaptHua";
//public static String HAN="CaptHua";
static {
System.out.println("C初始化");
}
}
测试类
public class Test {
PrintClassInfo printClassInfo = new PrintClassInfo();
public static void main(String[] args) throws Exception {
//1、通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。子类和父类都会被加载。
System.out.println(A.b);
//2、定义对象数组,不会触发该类的初始化。该类会加载
B[] b = new B[10];
A[] a = new A[10];
//3、调用的是父类的final方法或者字段
System.out.println(A.HAN);
//4、final定义的常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。也不会加载常量所在的类。
System.out.println(C.HAN);
//运行期间确定值得常量除外
System.out.println(C.NUM);
//5、通过类名获取 Class 对象,不会触发类的初始化,会加载。
System.out.println(B.class);
System.out.println(A.class);
//6、通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。会加载类。
Class.forName("practical.B",false,Thread.currentThread().getContextClassLoader());
Class.forName("practical.A",false,Thread.currentThread().getContextClassLoader());
//7、通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作(加载了,但是不初始化)
ClassLoader classLoader=Thread.currentThread().getContextClassLoader();
classLoader.loadClass("practical.B");
classLoader.loadClass("practical.A");
PrintClassInfo.printLoadedClass(null);
}
}
辅助工具类
public class PrintClassInfo {
public static void printLoadedClass(String packageNameFilter)throws Exception{
if(packageNameFilter==null){
packageNameFilter="practical";
}
final String condition=packageNameFilter;
ClassLoader classLoader=Thread.currentThread().getContextClassLoader();
Class loader=ClassLoader.class;
Field classesFiled = loader.getDeclaredField("classes");
classesFiled.setAccessible(true);
Vector<Class> classVector = (Vector<Class>)classesFiled.get(classLoader);
List<Class> classes=new ArrayList<>(classVector);
classes = classes.stream().filter(clazz->clazz.getName().contains(condition)).collect(Collectors.toList());
System.out.println("加载的类:");
classes.forEach(clazz-> System.out.println(clazz.getName()));
}
}
结果
1、通过子类引用父类的静态字段或静态方法,只会触发父类的初始化,而不会触发子类的初始化。子类和父类都会被加载。
2、定义对象数组,不会触发该类的初始化。该类会加载
后面一样只要加载子类就会加载父类
3、子类调用的是父类的final方法或者字段
4、final定义的常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。也不会加载常量所在的类。
public static final String HAN="C";
System.out.println(C.HAN);
public static String HAN="C";//调用非final定义的常量会初始化类
System.out.println(C.HAN);
System.out.println(C.NUM);
4、通过类名获取 Class 对象,不会触发类的初始化,会加载。
6、通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。会加载类。
7、通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作(加载了,但是不初始化)