类加载机制详解 动态加载类与静态加载类的对比

通过反射机制进一步深入虚拟机中了解类的加载机制

什么是java反射机制?

  1. 当程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言。我们认为java并不是动态语言,但是它却有一个非常突出的动态相关机制,俗称:反射。
    IT行业里这么说,没有反射也就没有框架,现有的框架都是以反射为基础。在实际项目开发中,用的最多的是框架,填的最多的是类,反射这一概念就是将框架和类揉在一起的调和剂。所以,反射才是接触项目开发的敲门砖。

  2. java中的new方法是静态加载,因为new方法是在编译阶段就会检查,而不是在运行阶段。反射是可以在运行时创建对象,调用对象的方法、变量等。

  3. Java反射机制主要提供了以下功能:在运行时判断任意一个对象所属的类;在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的成员变量和方法;在运行时调用任意一个对象的方法;生成动态代理。

类加载的主要流程

在java中我们要想编译执行java程序那么我们就要进行编译,那么这个编译过后.java文件就会被编译成.class文件最后在终端显示执行结果

  1. 装载(重点)

    1. 通过类的属性和类名在定义类的二进制字节流
    2. 将字节流中的静态存储结构转换为方法去中的数据结构
    3. 在java堆中生成一个类的java.lang.Class对象,作为方法区这些数据的访问入口
    4. 在此阶段完成之后,jvm外的二进制字节流会按照jvm要求的格式的二进制字节流进入jvm中的方法区中
  2. 链接:将Java类的二进制代码合并到jvm的运行状态之中的过程
    验证:确保加载的类信息符合JVM规范,没有安全方面的问题
    准备:正式为类变量(static)分配内存并设置类变量默认初始值得阶段,这些内存都在方法区中进行分配
    解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程

  3. 初始化

  • 执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语言合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。

  • 当前初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的构造器)。

  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。

类加载的内存分析

我们可以先通过以非常简单的代码了解类在加载过程中的内存分配

package reflection;

public class reflection_class {
    public static void main(String[] args) {
        A a = new A();
        System.out.println(a.m);
    }
}
class A {
    public A(){
        System.out.println("A类的无参构造初始化");
    }
    static {
        System.out.println("A类静态代码块的初始化");
        m=22200;
    }
    static int m = 222;
}

我们简单将虚拟机内区分为三块区域:方法区(特殊的堆)、堆、栈

首先方法区先将main方法的reflection_class类中的数据(静态变量、静态方法、常量池、代码等)加载进方法区内,之后A类中的数据也会被加载进方法区内。

加载完成后会在堆中会为reflection_class类产生一个java.lang.Class对象代表它,同样A类也会产生一个java.lang.Class对象。

之后在栈中执行main()方法,并且初始化m默认值为0(可参考准备链接中的阶段)

在我们执行main()方法时,由A a = new A()语句会在堆中产生一个A对象a。a会向java.lang.Class对象获取数据。

在初始化中静态变量m会被()方法合并的出m的值为222。

参考资料:b站狂神的注解与反射视频

分析类的初始化

什么时候会发生类初始化?

类的主动引用 (一定会发生类的初始化)

  • 当虚拟机启动,先初始化main方法所在的类
  • new一个类的对象
  • 调用类的静态成员(除final常量)和静态方法
  • 使用java.lang.reflect包的方法对类进行反射调用
  • 当初始化一个类,如果其父类没有被初始化,则会先初始化它的父类

类的被动引用(不会发生类的初始化)

  • 当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:当通过子类引用父类的静态变量,不会导致子类初始化
  • 通过数组定义引用,不会触发此类的初始化
  • 引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)

除此之外,下面几种情形需要特别指出

对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。
Java编译器会在编译时直把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。
反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值。
如果通过该类来访问它的静态变量,则会导致该类被初始化。
package reflection;

public class reflection_class2 {
    static {
        System.out.println("Main类被加载");
    }
    public static void main(String[] args) throws ClassNotFoundException {
        //使用一行后注释一行会发现结果不全相同
        
        //Son son = new Son();//1.主动引用
        
        //Class Son = Class.forName("reflection.Son");//2.反射也会产生主动引用

        //System.out.println(Son.b);//通过子类使用了父类的静态变量b只父类初始化

        //Son[] array = new Son[5];//只是main方法中创建一个数组的代码而已,该数组被命名为Son

        System.out.println(Son.s);//调用常量不会初始化类
    }
}

class  Son extends Father{
    static {
        System.out.println("子类被加载");
        m = 300;
    }
    static  int m = 100;
    static final int s =11;
}
class Father{
    static int b = 2;
    static {
        System.out.println("父类被加载")
    }
}

分析代码

第一、二行中的代码都为主动引用所以一定会发生类的初始化,并且new子类需要先初始化父类再去初始化被new的子类。如果父类被初始化,则不需要。

第三行代码,通过子类使用了父类的静态变量b只父类初始化。具体看被动引用的第一条。

第四行代码只是main方法中创建一个数组的代码而已,该数组被命名为Son

第五行代码引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)

类加载器

类加载器负责加载所有的类,其为所有被载入内存的是所有类生成一个java.lang.Class实例对象。

一旦一个类被加载到JVM中,同一个类就不会被载入了。正如一个对象有一个唯一的标识一样,一个载入JVM中的类也有唯一的标识。

在java中类的唯一标识为全限定类名(包括包名和类名)作为标识;但是在JVM中唯一标识是全限定类名+其类加载器。

例如如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。

这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

加载器分类

  1. 启动(Bootstrap)类加载器
    启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分

它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

  1. 扩展(Extension)类加载器
    扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

  2. 系统(System)类加载器
    也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

应用程序类加载器,该加载器是由sun.misc.Launcher$AppClassLoader实现,该类加载器负责加载用户类路径上所指定的类库。开发者可通过ClassLoader.getSystemClassLoader()方法直接获取,故又称为系统类加载器。当应用程序没有自定义类加载器时,默认采用该类加载器。

类加载器加载Class大致分为8步

第一步. 检测该类是否载入过内存中,就是在缓冲区中是否有该类,如果没有就进入下一步,有就直接第八步

第二步. 判断是否有父类加载器,一种是父类加载器为根加载器,另一种是本身就是根加载器,直接进入第4步。如果有父类加载器则进入下一步

第三步. 使用父类加载器去载入目标类,如果载入成功则进入第八步,失败则进入第五步

第四步. 使用根类加载器去加载目标类,成功则进入第八步,失败进入第七步

第五步. 当前类加载器尝试找到Class文件,成功进入第六步,失败进入第七步

第六步. 从文件中载入Class,成功进入第八步

第七步. 抛出ClassNotFoundException异常

第八步. 返回对应的java.lang.Class对象

扩展

动态加载类和静态加载类的区别

首先,由于动态加载类大多应用在框架上而鄙人还未开始走上,所以只是通过一个简单的例子。

一.什么是动态加载类 什么是静态加载类

Class.forName 不仅表示类的类类型,还代表了动态加载类。编译时加载是静态加载类,

运行时加载是动态加载类。

请大家区分编译 运行。

二.为何要使用动态加载类

我们写了一个程序 并没有写A类和B类以及start方法

public class Main{
	public static void main(String args[]){
		if("A".equals(args[0])){
			A a=new A();
			a.start();
		}
		if("B".equals(args[0])){
			B b=new B();
			b.start();
		}
	}
}

我们会发现,我们并不一定用到A功能或B功能,可是编译却不能通过。而在日常的项目中,如果我们写了100个功能,因为一个功能的原因而导致所有功能不能使用,明显使我们不希望的。在这里,为什么会在编译时报错呢?new 是静态加载类,在编译时刻就需要加载所有可能使用到的功能。所以会报错。而在日常中我们希望用到哪个就加载哪个,不用不加载,就需要动态加载类。

类缓存

标准的JavaSE类加载器可以按要求查找类,但是一旦某个类别加载到类加载器中,他将维持加载一段时间。不过JVM垃圾回收机制可以回收这些Class对象。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值