Thinking in java-23 类的加载、链接和初始化

时间:2023-01-02 18:01:14

一个Java类从编辑的文本文件如: Test.java文件,需要经过编译器编译、加载-链接-初始化才能被使用。其中编译是java编译器将Test.java文件编译成Test.class字节码文件,这里主要是:load-link-initialization

1.加载load

Java中类的加载是通过类加载器完成的。类加载器最终要完成的功能是定义一个java类,即把java字节码文件转换为JVM中java.lang.Class对象。

  • bootstrap classloader: 启动类加载器是由JVM原生代码实现的,
  • user-defined classloader:用户自定义的类加载器都继承自java.lang.ClassLoader类。

Class.forName(“SomeClass”)和ClassLoader.getSystemClassLoader().loadClass(“SomeClass”):

这里有相关详细链接。
这2种方式都是动态地装载类到classpath的方法,但两者在1).是否在装载时初始化; 2).从哪里被装载;这两点问题上有所不同。

Class.forName(“SomeClass”)

  • 默认地,类在装载时被初始化。这意味着类中静态变量被初始化了。
  • 其次,类从当前类装载器中被装载。当调用Class.forName()装载JDBC驱动类时,该驱动类被装载到你所调用的类装载器那里。简言之,被装载类被装载到调用者的类装载器中。
  • Class.forName(className, true, currentLoader), 这是一个被重载的方法。可以选择性地加入第二或第三个参数来改变一些行为。

ClassLoader.loadClass()

  • 默认地,类在装载时并没有被初始化。类在classpath中被装载,并可用;但变量只有在被调用者调用时才被初始化。
  • 该方法的一个优势是,可以选择装载类到特定的类装载器中。
package com.fqyuan.thinking;

public class Person {

public static void main(String[] args) {
ClassLoader cl1 = Thread.currentThread().getContextClassLoader();
System.out.println(cl1.toString());
ClassLoader cl2 = ClassLoader.getSystemClassLoader();
System.out.println(cl2.toString());

try {
Class<?> c1 = cl1.loadClass("com.fqyuan.thinking.ForLoadTest");
System.out.println(c1.hashCode() + " \n" + c1.getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
try {
Class<?> c2 = Class.forName("com.fqyuan.thinking.ForLoadTest");
System.out.println(c2.hashCode() + " \n" + c2.getClassLoader());
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

//Running result:
sun.misc.Launcher$AppClassLoader@2a139a55
sun.misc.Launcher$AppClassLoader@2a139a55
2018699554
sun.misc.Launcher$AppClassLoader@2a139a55
Static Initializer Called!!
2018699554
sun.misc.Launcher$AppClassLoader@2a139a55

JLS有详细解释。
1. Verification: 验证保证了二进制的类或接口结构上是正确的。
2. Preparation:包括创建类或接口的静态域,并把这些域初始化到默认值。
3. Resolution:将运行时常量池中的符号引用动态解析成实际值的方式。即:保证当前类的引用被正确得找到。

3.初始化initialization

注意,这里是Class的初始化,而不是instance对象实例的初始化。当Java类第一次被真正使用时,JVM负责初始化该类。实际做的工作是:

  • 执行静态代码块;
  • 初始化静态数据域
package fqy.iss.thinking.reuse;

import static fqy.iss.utils.Print.print;

class Insect
{
private int i = 9;
protected int j;

Insect()
{
print("i = " + i + ", j = " + j);
j = 39;
}

private static int x1 = printInit("static Insect.x1 initialized");

static int printInit(String s)
{
print(s);
return 47;
}
}

public class Beetle extends Insect
{

private int k = printInit("Beetle.k initialized");

public Beetle()
{
print("k = " + k);
print("j = " + j);
}

private static int x2 = printInit("static Beetle.x2 initialized");

public static void main(String[] args)
{
print("Beetle constructor");
Beetle b = new Beetle();
}

}

//Running result
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39

产生上述结果分析;

  1. 首先按照从父类到基类的顺序,类被加载。完成静态数据域和静态代码块的执行。然后类加载器ClassLoader就把Class返回,故而出现了运行结果中的前2行。
  2. 然后开始执行main() 方法。main()方法中new了基类对象。实际执行顺序是:先初始化父类中的实例方法,再执行父类构造方法;再初始化子类中的实例方法,然后执行字类的构造方法。