本篇的目的,是通过具体代码解释jvm类加载的详细过程。再结合之前的理论说明,完全可以应对所有类加载的相关问题
类加载的整体说明,请参看 《JVM类加载总结》
1、准备试验用代码
父类:
public class ParentClass {
public static final String PARENT_FINAL_P = "父类常量";
public static String parentP = "父类成员变量";
static{
System.out.println("父类静态代码块");
}
{
System.out.println("父类动态代码块");
}
public ParentClass() {
System.out.println("父类构造方法");
}
public static void parentStaticMethod(){
System.out.println("父类静态方法");
}
private String propertityA;
public String getPropertityA() {
return propertityA;
}
public void setPropertityA(String propertityA) {
this.propertityA = propertityA;
}
}
子类:
public class ChildClass extends ParentClass{
public static final String CHILD_FINAL_P = "子类常量";
public static String childP = "子类成员变量";
static{
System.out.println("子类静态代码块");
}
{
System.out.println("子类动态代码块");
}
static{
System.out.println("子类静态代码块2");
}
public ChildClass() {
System.out.println("子类构造方法");
}
public static void childStaticMethod(){
System.out.println("子类静态方法");
}
private String propertityB;
public String getPropertityB() {
return propertityB;
}
public void setPropertityB(String propertityB) {
this.propertityB = propertityB;
}
}
测试类:
(测试用的main()方法不能写在父类或者子类里面。原因:jvm会强制加载main()方法所在的类,这样会影响测试效果)
public class TestClass {
static {
System.out.println("测试类静态代码块");
}
{
System.out.println("测试类动态代码块");
}
public static void main(String[] args) throws ClassNotFoundException {
这里的内容下面分情况来写
}
}
2、引用常量不会引发类加载
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(ChildClass.PARENT_FINAL_P);
}
测试类静态代码块
父类常量
可以看到,引用的父类的常量(final),也不会触发子类的任何加载行为(当然父类也没有被加载)
3、引用父类的成员变量或是静态方法会执行初始化
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(ChildClass.parentP);
}
测试类静态代码块
父类静态代码块
父类成员变量
子类引用父类的成员变量时,触发了父类的初始化(“父类静态代码块”被执行),但是子类本身没有被初始化
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(ChildClass.childP);
}
测试类静态代码块
父类静态代码块
子类静态代码块
子类静态代码块2
子类成员变量
子类引用自身成员变量时,根据“虚拟机会保证首先执行父类的<clinit>()方法”的理论,父类先被初始化(“父类静态代码块”),然后再初始化子类(“子类静态代码块1、2”)
另外,关于静态变量、静态代码块初始化的执行顺序,代码中先定义的先执行(变量、动态代码块同理)
4、使用new关键字实例化对象,会初始化类,并触发对象的一系列初始化动作
public static void main(String[] args) throws ClassNotFoundException {
ChildClass child = new ChildClass();
}
测试类静态代码块
父类静态代码块
子类静态代码块
子类静态代码块2
父类动态代码块
父类构造方法
子类动态代码块
子类构造方法
可以看到,与第3节相比,不但执行了类加载(clinit()方法调用静态代码),而且先创建了父类对象(父类动态代码块、父类构造方法),然后再创建的子类对象(子类动态代码块、子类构造方法 )
所以在一些文章中,给出了这样的对象初始化顺序:
静态变量/静态代码块(先父类后子类) > main方法 > 变量/动态代码块(先父类后子类) > 构造器(先父类后子类)
其实只要弄明白类的加载原理(包含一点最基础的java内存模型知识),这种顺序完全不用死记硬背
5、ClassLoader.loadClass
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
classLoader.loadClass("com.demo.ChildClass");
}
测试类静态代码块
loadClass什么也没有做……
因为这个public的loadClass,调用的是ClassLoader的
protected Class<?> loadClass(String name, boolean resolve)
方法,并且resolve固定传false,意思是只进行加载和链接,不执行初始化过程。
6、Class.forName
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("com.demo.ChildClass");
}
测试类静态代码块
父类静态代码块
子类静态代码块
子类静态代码块2
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
Class.forName("com.demo.ChildClass", false, classLoader);
}
测试类静态代码块
Class.forName的情况与ClassLoader.loadClass不同,有两个重载方法。
只指定一个参数时,调用的是一个native方法
forName0(className, true, ClassLoader.getClassLoader(caller), caller);
其中第二个参数true,指定了要对类进行初始化;
而使用重载的三个参数的方法时,其方法定义是这样的
public static Class<?> forName(String name, boolean initialize,ClassLoader loader)
第二个参数指定是否初始化,然后调用了同样的native方法,将boolean变量传递了进去,传false的话就不执行初始化过程了
7、类的加载过程只执行一次
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(ChildClass.CHILD_FINAL_P);
System.out.println(">>><<<");
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
classLoader.loadClass("com.demo.ChildClass");
System.out.println(">>><<<");
Class.forName("com.demo.ChildClass");
System.out.println(">>><<<");
ChildClass child = new ChildClass();
}
测试类静态代码块
子类常量
>>><<<
>>><<<
父类静态代码块
子类静态代码块
子类静态代码块2
>>><<<
父类动态代码块
父类构造方法
子类动态代码块
子类构造方法
最开始只引用了一下子类成员常量,因为常量在方法区的常量池里面,没有触发类的任何动作;
第二步 classLoader.loadClass 只做了类的加载和链接,不进行初始化,并没有任何输出;
第三步 Class.forName ,前面已经完成了加载和链接,所以跳过(输出中无法清晰显示出这个结论),只做了类的初始化;
第四步进行对象的创建和初始化,由于类的加载全过程已经完成,所以new关键字并没有触发各种静态资源的初始化(因为已经完事了)
简单总结一下
-
编译时把常量都放进常量池,引用时不会涉及到类
-
类加载时,除了非静态成员变量不会被加载,其它的都会被加载(包括非静态方法——这一结论无法用示例来说明),加载完毕初始化
-
创建实例时,先执行动态方法块,再执行构造方法
-
先父类后子类
网友评论