类加载机制与反射

562 阅读15分钟

JVM和类

当调用java命令运行某个Java程序时,该命令将会启动一个Java虚拟机进程,不管该Java程序有多么复杂,该程序启动了多少个线程,它们都处于该Java虚拟机进程里。同一个JVM的所有线程、所有变量都处于同一个进程里,它们都使用该JVM进程的内存区。当系统出现以下几种情况时,JVM进程将被终止:

  • 程序运行到最后正常结束;
  • 程序运行到使用System.exit()或Runtime.getRuntime.exit()代码处结束程序;
  • 程序执行过程中遇到未捕获的异常或错误而结束;
  • 程序所在平台强制结束了JVM进程

当Java程序运行结束时,JVM进程结束,该进程在内存中的状态将会丢失:

public class A
{
	public static int a = 6;  //定义该类的类变量
}
public class TestA
{
	public static void main(String[] args)
	{
		A a = new A();
		a.a++;
		System.out.println(a.a);   //输出7
    }
}
public class TestA2
{
	public static void main(String[] args)
	{
		A b = new A();
		System.out.println(b.a);   //输出6
    }
}

运行TestA和TestA2是两次运行JVM进程,第一次运行JVM结束后,它对A类所做的修改将全部丢失-----第二次运行JVM时将再次初始化A类;

两个JVM之间不会共享数据;

类的生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:

  1. 加载(Loading)
  2. 验证(Verification)
  3. 准备(Preparation)
  4. 解析(Resolution)
  5. 初始化(Initialization)
  6. 使用(Using)
  7. 卸载(Unloading)

7个阶段,其中验证、准备、解析3个部分统称为连接(Linking),这7个阶段的发生顺序为:

在这里插入图片描述 其中,加载、验证、准备、初始化、卸载这5个阶段的顺序是固定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某种情况下可以在初始化阶段之后再开始,这是为了支持 Java 语言的运行时绑定;

什么情况下需要开始类加载过程的第一个阶段:加载?Java 虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行初始化:

  1. 遇到 new 、getstatic、putstatic 或 invokestatic 这 4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的 Java 代码场景是:使用 new 关键字实例化对象的时候、读取或设置一个类的静态字段(被 final 修饰,已在编译期把结果放入常量池的静态字段除外)的时候、以及调用一个类的静态方法的时候;
  2. 使用 java,..lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化;
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化;
  4. 当虚拟机启动时,用户需要指定一个执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
  5. 当使用JDK1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;

这5种场景种的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用;

被动引用例子一

public class SuperClass {
	static {
		System.out.println("SuperClass init!");
	}
	
	public static int valeu = 123;
}
class SubClass extends SuperClass{
	static {
		System.out.println("SubClass init!");
	}
}

class NotInitializating{
	public static void main(String[] args) {
		System.out.println(SubClass.valeu);
	}
}

//输出
SuperClass init!
123

对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

被动引用例子二

public class ConstClass {
	static {
		System.out.println("ConstClass init!");
	}
	
	public static final int valeu = 123;
}


class NotInitializating{
	public static void main(String[] args) {
		System.out.println(ConstClass.valeu);
	}
}
//输出
123

没有输出"ConstClass init!",这是因为虽然在 Java 源码中引用了 ConstClass 类中的常量 valeu,但其实在编译阶段通过常量传播优化,已经将此常量的值 123 存储到了 NotInitializating 类的常量池中,以后 NotInitializating 对常量 ConstClass.valeu 的引用实际都被转化为 NotInitializating 类对自身常量池的引用。也就是说,实际上 NotInitializating 的 Class 文件之中并没有 ConstClass 类的符号引用入口,这两个类在编译成 Class 之后就不存在任何联系了;

类的加载

当Java程序中需要使用到某个类时,虚拟机会保证这个类已经被加载、连接和初始化。而连接又包含验证、准备和解析这三个子过程,这个过程必须严格的按照顺序执行

类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象; 在这里插入图片描述 类的加载由类加载器完成,类加载器通常由JVM提供。类加载器通常无须等到“首次调用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类;

类的连接

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。 类连接可分为三个阶段:

  • 验证:确保被加载类的正确性。class文件的字节流中包含的信息符合当前虚拟机要求,不会危害虚拟机自身安全;(例如用 UE、editPlus 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行会报错)

    Error: A JNI error has occurred, please check your installation and try again
    Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value
    3405691578 in class
    
  • 准备:为类的静态变量分配内存,并将其初始化为默认值。此阶段仅仅只为静态类变量(即static修饰的字段变量)分配内存,并且设置该变量的初始值。(这里所说的初始值通常情况下是数据类型的零值,例如: public static int value = 123; 那变量 value 在准备阶段过后的初始值为 0 而不是 123,因为这时候尚未开始执行任何 Java 方法,所以把 value 赋值为123 的动作将在初始化阶段才会执行)。对于final static修饰的变量,编译的时候就会分配了,也不会分配实例变量的内存; 在这里插入图片描述

  • 解析:把类中的符号引用转换为直接引用。符号引用就是一组符号来描述目标,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄;

类的初始化

Java类中对类变量指定初始值有两种方式:

  1. 声明类变量时指定初始值
  2. 使用静态初始化块为类变量指定初始值

JVM初始化一个类包含如下几个步骤:

  1. 假如这个类还没有被加载和连接,则程序先加载并连接该类;
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类;
  3. 假如类中有初始化语句,则系统依次执行这些初始化语句;

系统对直接父类的初始化步骤也遵循此步骤1~3;如果该直接父类又有直接父类,则系统再次重复这三个步骤来先初始化这个父类。依次类推,所以JVM最先初始化的总是java.lang.Object类;

类初始化的时机

当Java程序首次通过下面6种方式来使用某个类或接口时,系统就会初始化该类或接口:

  • 创建类的实例。为某个类创建实例的方式包括:使用new操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例;
  • 调用某个类的类方法(静态方法);
  • 访问某个类或接口的类变量,或为该类变量赋值;
  • 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象;
  • 初始化某个类的子类。当初始化某个类的子类时,该子类的所有父类都会被初始化;
  • 直接使用java.exe命令来运行某个主类。当运行某个主类时,程序会先初始化该主类;

对于一个final型的类变量,如果该类变量的值在编译时就可以确定下来,那么这个类变量就相当于“宏变量”(常量)。Java编译器会在编译时直接把这个类变量出现的地方替换成它的值;

反之,如果final修饰的类变量的值不能在编译时确定下来,则必须等到运行时才可以确定该类变量的值,如果通过该类来访问它的类变量,则会导致该类被初始化;

当使用ClassLoader类的loadClass()方法加载某个类时,该方法只是加载该类,并不会执行该类的初始化。使用Class的forName()静态方法才会导致强制初始化该类;

类加载器简介

当JVM启动时,会形成由三个类加载器组成的初始化类加载器层次结构:

  • Bootstrap ClassLoader:根类加载器(并不是Java实现的),负责加载Java的核心类(%JAVA_HOME%/jre/lib/rt.jar);

    	public static void main(String[] args) throws Exception {
    		ClassLoader cl = Object.class.getClassLoader();
    	    System.out.println(cl);//根类加载器打印出来的结果是null
    	}
    
  • Extension ClassLoader:扩展类加载器,负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext)中JAR包的类;

  • System ClassLoader:系统类加载器,负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径;

    public class ClassLoaderDemo {
        public static void main(String[] args) {
            //自己编写的类使用的类加载器
            ClassLoader classLoader = ClassLoaderDemo.class.getClassLoader();
            System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader
        }
    }
    

系统类加载器是AppClassLoader的实例,扩展类加载器是ExtClassLoader的实例。这两个类都是URLClassLoader类的实例;

类加载机制

JVM的类加载机制主要有如下三种:

  • 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入;
  • 父类委托:先让parent(父)类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类;
  • 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜索该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。这就是为什么修改了Class后,必须重启JVM,程序所做的修改才会生效的原因;

在这里插入图片描述

public class ClassLoaderDemo1 {
    public static void main(String[] args) throws Exception{
        //演示类加载器的父子关系
        ClassLoader loader = ClassLoaderDemo1.class.getClassLoader();
        while(loader!=null){
            System.out.println(loader);
            loader = loader.getParent();
        }
    }
}
运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586

所有的类加载器(除了根类加载器)都必须继承java.lang.ClassLoader!

类加载器加载Class步骤:

  1. 检测此Class是否载入过(即在缓存区中是否有此Class)。如果有则直接进入第8步,否则接着执行第2步;

  2. 如果父类加载器不存在(如果没有父类加载器,则要么parent一定是根类加载器,要么本身就是根类加载器),则跳到第4步执行;如果父类加载器存在,则接着执行第3步;

  3. 请求使用父类加载器去载入目标类,如果成功载入则跳到第8步,否则接着执行第5步;

  4. 请求使用根类加载器来载入目标类,如果成功载入则跳到第8步,否则跳到第7步;

  5. 当前类加载器尝试寻找Class文件(从与此ClassLoader相关的类路径中寻找),如果找到则执行第6步,如果找不到则跳到第7步;

  6. 从文件中载入Class,成功载入后跳到第8步;

  7. 抛出ClassNotFoundException异常;

  8. 返回对应的java.lang.Class对象;

使用双亲委派机制的好处:

  • 可以避免类的重复加载,当父类加载器已经加载了该类时,就没有必要子ClassLoader再加载一次
  • 考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Object的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Object,而直接返回已加载过的Object.class,这样便可以防止核心API库被随意篡改

URLClassLoader

在java.net包中,JDK提供了一个更加易用的类加载器URLClassLoader,它扩展了ClassLoader,能够从本地或者网络上指定的位置加载类。我们可以使用该类作为自定义的类加载器使用。

构造方法:

public URLClassLoader(URL[] urls):指定要加载的类所在的URL地址,父类加载器默认为系统类加载器。

public URLClassLoader(URL[] urls, ClassLoader parent):指定要加载的类所在的URL地址,并指定父类加载器。

案例1:加载磁盘上的类

public static void main(String[] args) throws Exception{
		File file = new File("d:/");
		URI uri = file.toURI();
		URL url = uri.toURL();
        URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
        System.out.println(classLoader.getParent());
        Class aClass = classLoader.loadClass("com.itheima.Demo");
        Object obj = aClass.newInstance();
    }

案例2:加载网络上的类

public static void main(String[] args) throws Exception{
		URL url = new URL("http://localhost:8080/examples/");
        URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
        System.out.println(classLoader.getParent());
        Class aClass = classLoader.loadClass("com.itheima.Demo");
        aClass.newInstance();
}

自定义类加载器

我们如果需要自定义类加载器,只需要继承ClassLoader类,并覆盖掉findClass方法即可!

D盘文件: 在这里插入图片描述 Test.java:

public class Test {
    public  void test() {
    	System.out.println("TestAAAAAAAAAAAA");
    }
}

自定义类加载器:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class MyFileClassLoader extends ClassLoader{
    private String directory;//被加载的类所在的目录

    /**
 	   * 指定要加载的类所在的文件目录
     * @param directory
     */
    public MyFileClassLoader(String directory,ClassLoader parent){
        super(parent);
        this.directory = directory;
    }
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            //把类名转换为目录
            String file = directory+File.separator+name.replace(".", File.separator)+".class";
            //构建输入流
            InputStream in = new FileInputStream(file);
            //存放读取到的字节数据
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte buf[] = new byte[1024];
            int len = -1;
            while((len=in.read(buf))!=-1){
                baos.write(buf,0,len);
            }
            byte data[] = baos.toByteArray();
            in.close();
            baos.close();
            return defineClass(name,data,0,data.length);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws Exception {
        MyFileClassLoader myFileClassLoader = new MyFileClassLoader("d:/",null);
        Class clazz = myFileClassLoader.loadClass("Test");
        clazz.getMethod("test").invoke(clazz.newInstance());
    }
}
//控制台打印:TestAAAAAAAAAAAA

通过反射查看类型信息

Java程序中的许多对象在运行时都会出现两种类型:

  • 编译时类型
  • 运行时类型

例如代码:Persion p = new Student();这行代码将会生成一个p变量,该变量的编译时类型为Persion,运行时类型为Student;

某些情况下,程序需要在运行时发现对象和类的真实信息,解决该问题有以下两种做法:

  • 第一种做法是假设在编译时和运行时都完全知道类型的具体信息,在这种情况下,可以先使用instanceof运算符进行判断,再利用强制类型转换将其转换成其运行时类型的变量即可;
  • 第二种做法是编译时根本无法预知该对象和类可能属于哪些类,程序只依靠运行时信息来 发现该对象和类的真实信息,这就必须使用反射;

获得Class对象

使用反射机制可以动态获取当前class的信息 比如方法的信息、注解信息、方法的参数、属性等;

public class User {
    private String name;
    private Integer age;

    public User(String name, Integer age) {
        System.out.println("我是有参构造函数");
        this.name = name;
        this.age = age;
    }

    public User(){
        System.out.println("我是无参构造函数");
    }
    
    private void UserPrivateMethod(){
        System.out.println("我是User的私有方法");
    }
}

在Java程序中获得Class对象通常有如下三种方式:

  • 使用Class类的forName(String clazzName)静态方法。该方法需要传入字符串参数,值是某个类的全限定类名(必须添加完整包名);

    Class<?> aClass = Class.forName("com.example.demo.entity.User");
    
  • 调用某个类的class属性来获取该类对应的Class对象。

    Class<User> aClass = User.class;
    
  • 调用某个对象的getClass()方法。该方法是java.lang.Object类中的一个方法,所以所有的Java对象都可以调用该方法,该方法将会返回该对象所属类对应的Class对象;

    User user = new User();
    Class<? extends User> aClass = user.getClass();
    

1、使用无参构造初始化对象

Class<?> aClass = Class.forName("com.example.demo.entity.User");

//newInstance()执行无参数构造函数,大部分框架底层都是用的newInstance()来初始化对象,这也是为什么当我们对象没有无参构造时会报错的原因
User user = (User) aClass.newInstance();
user.setName("甲");
user.setAge(12);
System.out.println(user);

//控制台打印
我是无参构造函数
User{name='甲', age=12}

2、使用有参构造初始化对象

Class<?> aClass = Class.forName("com.example.demo.entity.User");
//执行有参数构造函数
Constructor<?> constructor = aClass.getConstructor(String.class, Integer.class);
User user2 = (User) constructor.newInstance("乙", 24);
System.out.println(user2);

//控制台打印
我是有参构造函数
User{name='乙', age=24}

3、给私有属性赋值

Class<?> aClass = Class.forName("com.example.demo.entity.User");
User user1 = (User) aClass.newInstance();
Field name = aClass.getDeclaredField("name");
Field age = aClass.getDeclaredField("age");

//如果使用反射给私有属性或者调用私有的方法都需要设置权限,否则会报错:Class XXX can not access a member of class XXX with modifiers "private"
name.setAccessible(true);
age.setAccessible(true);
name.set(user1,"丙");
age.set(user1,36);
System.out.println(user1);

4、使用反射机制调用方法

Class<?> aClass = Class.forName("com.example.demo.entity.User");
User user = (User) aClass.newInstance();
Method method = aClass.getDeclaredMethod("UserPrivateMethod", null);
method.setAccessible(true);
Object invoke = method.invoke(user, null);
System.out.println(invoke);

类的显式与隐式加载

类的加载方式是指虚拟机将class文件加载到内存的方式;

显式加载是指在java代码中通过调用ClassLoader加载class对象,比如:

  • Class.forName(String name);
  • this.getClass().getClassLoader().loadClass()加载类;

隐式加载指不需要在java代码中明确调用加载的代码,而是通过虚拟机自动加载到内存中。比如在加载某个class时,该class引用了另外一个类的对象,那么这个对象的字节码文件就会被虚拟机自动加载到内存中;

书籍:疯狂Java讲义 学习所做的笔记,特此记录下来