Java反射与类加载机制

前面在学习Xposed Hook的时候经常会用到反射与类加载器,这次就学习下Java中的反射与类加载机制。

1. Java反射

我们知道Java中的对象有编译类型和运行类型(例如多态),举个例子:

Object obj = new java.util.Date();

编译类型为Object,运行类型(即obj对象真实的类型)其实为java.util.Date,那么如果想要根据obj对象调用Date类中的一个方法例如toLocaleString,应该怎么做?我们首先想到的可能就是多态中常见的使用方法-强制类型转换:

Date d = (Date)obj;
d.toLocaleString();

但是这种方法有个前提是我们必须知道这个obj对象的真实类型是什么,如果不知道obj的真实类型,那就得用到反射了。所谓反射,就是可以在运行时,而非编译时,动态获取类型的信息,比如接口信息、成员信息、方法信息、构造方法信息等,根据这些动态获取到的信息创建对象、访问/修改成员、调用方法等。

获取Class对象的三种方式

在Java中每个已加载的类在内存都有一份类信息,每个对象都有指向它所属类信息的引用。Java中类信息对应的类就是java.lang.Class,三种方式如下:

  • Class.forName(“包名.类名”),且基本类型不支持forName方法;
    Class.forName的第二种方法的ClassLoader参数表示类加载器(如下代码所示),initialize表示加载后,是否执行类的初始化代码(如static语句块)。第一种方法中没有传这些参数,相当于调用Class.forName(className, true, currentLoader),currentLoader表示加载当前类的ClassLoader。
    另外Class其实有两种静态方法可以根据类名加载类,其他的还有之前我们hook多分Dex情况时用到的Classloader的loadClass()方法,这里先介绍前者,与后者的区别后面学习类加载机制的时候再说。
public static Class<?> forName(String className)//相当于调用Class.forName(className, true, currentLoader)
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)//ClassLoader参数表示类加载器
   
  • 包名.类名.class ;Class<?> clz = Date.class;
  • 对象.getClass 的方式获得。 Class<?> clz = obj.getClass();

获取到Class对象以后(假设为clz),我们就能使用这个对象调用类的实例化方法:例如clz.newInstance()实例化对象,也可以通过clz. getDeclaredFields等API获取名称、属性和方法包括构造方法等。以下列举了一下常用的API:

1.1 名称信息

拿到Class对象后有如下方法,可以获取与名称有关的信息:getSimpleName返回的名称不带包信息,getName返回的是Java内部使用的真正的名称,getCanonicalName返回的名称更为友好,getPackage 返回的是包信息,都是之前在编写Xposed插件的时候很熟悉的API:
在这里插入图片描述

1.2 字段/属性(包括静态和实例)信息

类中定义的静态和实例变量都被称为字段,用类Field表示,位于包java.util.reflect下,Class有四个获取字段信息的方法,返回值为Field类数组或者Field类,getDeclaredFields/getDeclaredField可以用于访问私有字段:

public Field[] getFields()//返回所有的public字段,包括其父类的,如果没有字段,返回空数组
public Field[] getDeclaredFields()//返回本类声明的所有字段,包括非public的,但不包括父类的
public Field getField(String name)//返回本类或父类中指定名称的public字段,找不到抛出异常NoSuchFieldException
public Field getDeclaredField(String name)//返回本类中声明的指定名称的字段,找不到抛出异常NoSuchFieldException

并且,返回的Field类也有很多方法,可以获取字段的信息,也可以通过Field访问和操作指定对象中该字段的值,例如:

public String getName()//获取字段的名称
public boolean isAccessible()//判断当前程序是否有该字段的访问权限
public void setAccessible(boolean flag)//flag设为true表示忽略Java的访问检查机制,以允许读写非public的字段
public Object get(Object obj)//获取指定对象obj中该字段的值
public void set(Object obj, Object value)//将指定对象obj中该字段的值设为value
......

在get/set方法中,对于静态变量,obj被忽略,可以为null;如果字段值为基本类型,get/set会自动在基本类型与对应的包装类型间进行转换;而对于private字段来说,直接调用get/set会抛出非法访问异常IllegalAccessException,因此应该先调用setAccessible(true)以关闭Java的检查机制。这里就有个疑问,既然Java反射可以访问和修改私有成员变量,那封装成private还有意义么?推荐阅读知乎解释:private想表达的不是“安全性”的意思,而是OOP(面向对象程序设计)的封装概念。就像是一家没人的店挂了个牌子“闲人免进”,但你真要进去还是有各种办法可以办到。所以封装成private是有一层隐含的含义:如果你按照遵守这套规则,开发者可以保证不存在问题(不考虑bug的情况下),否则后果自负。

1.3 方法信息

类中定义的静态和实例方法都被称为方法,用类Method表示,Class有四个获取方法信息的方法,返回值为Method类数组或者Method类,getDeclaredMethods/getDeclaredMethod可以用于访问私有方法:

public Method[] getMethods()//返回所有的public方法,包括其父类的,如果没有方法,返回空数组
public Method[] getDeclaredMethods()//返回本类声明的所有方法,包括非public的,但不包括父类的
//返回本类或父类中指定名称和参数类型的public方法,找不到抛出异常NoSuchMethodException
public Method getMethod(String name, Class<?>... parameterTypes)
//返回本类中声明的指定名称和参数类型的方法,找不到抛出异常NoSuchMethodException
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)

并且,返回的Method类也有很多方法,可以获取方法的信息,也可以通过Method调用对象的方法,例如:

public String getName()//获取方法的名称
public void setAccessible(boolean flag)//flag设为true表示忽略Java的访问检查机制,以允许调用非public的方法
//在指定对象obj上调用Method代表的方法,传递的参数列表为args
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException
......

对于invoke方法,如果Method为静态方法,obj被忽略,可以为null,args可以为null,也可以为一个空的数组,方法调用的返回值被包装为Object返回,如果实际方法调用抛出异常,异常被包装为InvocationTargetException重新抛出。另外和字段信息一样,私有(private)方法的调用,要注意要设置其访问权限setAccessible(true)。

1.4 创建对象和构造方法

Class有一个方法,可以用它来创建对象:

public T newInstance() throws InstantiationException, IllegalAccessException

例如:

Map<String,Integer> map = HashMap.class.newInstance();
map.put("hello", 123);

另外,newInstance只能使用默认构造方法,如果类没有该构造方法,会抛出异常InstantiationException。

Class还有一些方法,可以获取所有的构造方法,getDeclaredConstructors/getDeclaredConstructor可获取私有构造方法:

public Constructor<?>[] getConstructors()//获取所有的public构造方法,返回值可能为长度为0的空数组
public Constructor<?>[] getDeclaredConstructors()//获取所有的构造方法,包括非public的
//获取指定参数类型的public构造方法,没找到抛出异常NoSuchMethodException
public Constructor<T> getConstructor(Class<?>... parameterTypes)
//获取指定参数类型的构造方法,包括非public的,没找到抛出异常NoSuchMethodException
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)

返回值为Constructor类表示构造方法,通过它也可以创建对象:

public T newInstance(Object ... initargs) throws InstantiationException,  IllegalAccessException, IllegalArgumentException, InvocationTargetException

例如:

Constructor<StringBuilder> contructor= StringBuilder.class.getConstructor(new Class[]{int.class});
StringBuilder sb = contructor.newInstance(100);
    

延伸:私有构造方法
构造方法可以私有,即修饰符可以为private,为什么需要私有构造方法呢?

  • 1)不能创建类的实例,类只能被静态访问,如Math和Arrays类,它们的构造方法就是私有的。
  • 2)能创建类的实例,但只能被类的静态方法调用。有一种常见的场景:类的对象有但是只能有一个,即单例模式。在这种场景中,对象是通过静态方法获取的,而静态方法调用私有构造方法创建一个对象,如果对象已经创建过了,就重用这个对象。
  • 3)只是用来被其他多个构造方法调用,用于减少重复代码。
1.5 反射与数组

对于数组类型,有一个专门的方法,可以获取它的元素类型:

public native Class<?> getComponentType()

例如如下代码,输出为"class java.lang.String":

String[] arr = new String[]{};
System.out.println(arr.getClass().getComponentType());

java.lang.reflect包中有一个针对数组的专门的类Array(非java.util中的Arrays),提供了对于数组的一些反射支持,以便于统一处理多种类型的数组,主要方法有:

public static Object newInstance(Class<?> componentType, int length)//创建指定元素类型、指定长度的数组,
public static Object newInstance(Class<?> componentType, int... dimensions)//创建多维数组
public static native Object get(Object array, int index)//获取数组array指定的索引位置index处的值
public static native void set(Object array, int index, Object value)//修改数组array指定的索引位置index处的值为value
public static native int getLength(Object array)//返回数组的长度

例如:

int[] intArr = (int[])Array.newInstance(int.class, 10);
String[] strArr = (String[])Array.newInstance(String.class, 10);

2. 类加载机制

前面说过Java文件从编码完成到最终执行,一般主要包括两个过程:编译和运行,编译就是通过javac命令将.java文件编译成字节码即.class文件,运行则是把编译生成的.class文件交给Java虚拟机(JVM)执行。类加载过程就是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。另外JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。

类的加载机制可以分为加载-链接-初始化三个阶段,链接又可以分为验证、准备、解析三个过程,并且加载、链接、初始化的各个阶段并不是彼此独立,而是交叉进行,比如会在一个阶段执行的过程中调用/激活另外一个阶段。
在这里插入图片描述

2.1 加载

加载指的是把从各个来源得到的class字节码文件通过类加载器装载入内存中,并且会在内存中生成一个代表这个类的 java.lang.Class 对象,作为这个类的数据请求入口。这里需要了解两个知识点:

  • 来源:一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译
  • 类加载器:从JVM的角度,类加载器是分为了启动类加载器和其他类加载器(包括扩展类加载器,应用类加载器,以及用户的自定义类加载器)两种
    • 启动类加载器(Bootstrap ClassLoader): Bootstrap 类加载器负责加载< JAVA_HOME >/lib/ rt.jar 中的 JDK 类文件,它是所有类加载器的父加载器,一般是C++实现的,我们日常用的Java类库比如String, ArrayList等都位于该包内。Bootstrap 类加载器没有任何父类加载器,如果调用 String.class.getClassLoader(),会返回 null,任何基于此的代码会抛出 NUllPointerException 异常。
    • 扩展类加载器(Extension ClassLoader):Extension 类加载器将加载类的请求先委托给它的父加载器,也就是Bootstrap,如果没有成功加载的话,再从 jre/lib/ext 目录下或 java.ext.dirs 系统属性定义的目录下加载类。Extension 由加载器sun.misc.Launcher$ExtClassLoader 实现。
    • 应用程序类加载器(Application ClassLoader):默认的类加载器,是ClassLoader#getSystemClassLoader()的返回值,故又称为系统类(System)加载器,实现类是sun.misc.Launcher$AppClassLoader。它负责加载应用程序的类,包括自己写的和引入的第三方法类库,即所有在类路径中指定的类。
    • 自定义类加载器(User ClassLoader):如果以上类加载起不能满足需求,可自定义类加载器。比如说App安全防护时加的壳,如果需要对自己的代码做防护,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。几种类加载器的关系如下:
      在这里插入图片描述

延伸:每个类加载器都拥有一个独立的类名称空间,它不仅用于加载类,还和这个类本身一起作为在JVM中的唯一标识。所以比较两个类是否相等,只要看它们是否由同一个类加载器加载,即使它们来源于同一个Class文件且被同一个JVM加载,只要加载它们的类加载器不同,这两个类就必定不相等。

2.2 链接

2.2.1 验证

主要是对一些词法、语法进行规范性校验,避免对 JVM 本身安全造成危害。当然也可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。包括对文件格式验证,元数据验证,字节码验证,符号引用的验证等。

  • 文件格式验证:验证字节流是否符合Class文件格式的规范以及是否能被当前版本的虚拟机处理。例如常量中是否有不被支持的常量?
  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。例如该类是否继承了被final修饰的类?类中的字段/方法是否与父类冲突?是否出现了不合理的重载?
  • 字节码验证:对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。例如要保证类型转换的合理性。
  • 符号引用验证:对类自身以外(如常量池中的各种符号引用)的信息进行匹配性校验。该验证发生在虚拟机将符号引用转化为直接引用的时候,即解析阶段。
    • 符号引用:即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法/一个变量/一个类的相关信息。
    • 直接引用:可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量。
    • 例如调用方法hello(),这个方法的地址是0x1234567,那么hello就是符号引用,0x1234567就是直接引用。
2.2.2 准备

准备阶段主要是为类变量分配内存,因为这里的变量是由方法区分配内存的,所以仅包括类变量而不包括实例变量,后者将会在对象实例化时随着对象一起分配在Java堆。 比如 static int a=11,会被初始化成成 a=0;如果是 static double a =11,则会被初始化成 a=0.0,final static tmp = 1, 那么该阶段tmp的初值就是1。

2.2.3 解析

将常量池内的符号引用替换为直接引用的过程。在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。

2.3 初始化

对类的静态变量和静态块中的变量进行初始化。如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
初始化阶段和准备阶段的区别是准备阶段的静态变量赋初始零值,而初始化阶段会根据Java程序的设定去初始化类变量和其他资源,或者说是执行类构造器< clinit >()的过程,< clinit >()是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生。

  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
  • 在遇到new、getstatic、putstatic或invokestatic这4个字节码指令时必须立即对类进行初始化
延伸:class.forName和 classloader的区别?

Class.forName 和 ClassLoader 都可以用来装载类,如前面说的包含加载、链接、初始化等操作,但是它们装载类的方式是有区别,简单来说:ClassLoader的loadClass不会执行类的初始化代码
 
首先看一下 Class.forName(…),forName(…)方法有一个重载方法 forName(className,boolean,ClassLoader),它有三个参数,第一个参数是类的包路径,第二个参数是boolean类型,为 true 地表示 Loading 时会进行初始化,第三个就是指定一个加载器;前面也说过当调用class.forName(…)时,默认调用的是有三个参数的重载方法,第二个参数默认传入true,第三个参数默认使用的是当前类加载时用的加载器。

ClassLoader.loadClass()也有一个重载方法,从源码中可以看出它默认调的是它的重载方法 loadClass(name, false),当第二参数为 false 时,说明类加载时不会被链接。这也是两者之间最大区别,前者在加载的时候已经初始化,后者在加载的时候还没有链接。如果你需要在加载时初始化一些东西,就要用 Class.forName 了,比如我们常用的驱动加载, 实际上它的注册动作就是在加载时的一个静态块中完成的,所以它不能被 ClassLoader 加载代替。

3. 双亲委派机制

简单来说包含如下几步:

  1. 判断是否已经加载过了,加载过了,直接返回Class对象,一个类只会被一个ClassLoader加载一次
  2. 如果没有被加载,先让父ClassLoader去加载,如果加载成功,返回得到的Class对象
  3. 在父ClassLoader没有加载成功的前提下,自己尝试加载类

为什么要先让父ClassLoader去加载呢?其实这样可以避免Java类库被覆盖的问题,比如用户程序也定义了一个类java.lang.String,通过双亲委派机制,java.lang.String只会被Bootstrap ClassLoader加载,避免自定义的String覆盖Java类库的定义。

4. 类加载顺序

看一下以下程序的执行顺序:

public class Book {
    public static void main(String[] args)
    {
        System.out.println("Hello ShuYi.");
    }

    Book()
    {
        System.out.println("书的构造方法");
        System.out.println("price=" + price +",amount=" + amount);
    }

    {
        System.out.println("书的普通代码块");
    }

    int price = 110;

    static
    {
        System.out.println("书的静态代码块");
    }

    static int amount = 112;
}

前面在讲初始化时提到当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类,我们会进行类的初始化。在代码中只有一个构造方法,但实际上Java代码编译成字节码之后,是没有构造方法的概念的,只有类初始化方法 和对象初始化方法 。

  • 类初始化方法:编译器会按照其出现顺序,收集类变量的赋值语句、静态代码块,最终组成类初始化方法。类初始化方法一般在类初始化的时候执行。
  • 对象初始化方法:编译器会按照其出现顺序,收集成员变量的赋值语句、普通代码块,最后收集构造函数的代码,最终组成对象初始化方法。对象初始化方法一般在实例化类对象(也就是new)的时候会立即执行。

因此,上述代码的类初始化方法是:

  static
  {
     System.out.println("书的静态代码块");
  }
  static int amount = 112;
  

上述代码的对象初始化方法是(注意在实例化类对象(也就是new)的时候会立即执行):

    {
        System.out.println("书的普通代码块");
    }
    int price = 110;
    Book()
    {
        System.out.println("书的构造方法");
        System.out.println("price=" + price +",amount=" + amount);
    }
    

当然上述代码中没有new,因此对象初始化方法不会执行,只会执行类初始化方法,因此输出为:

书的静态代码块
Hello ShuYi.

再看下面另一个程序,当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类,也就是Book类:

public class Book {
    public static void main(String[] args)
    {
        staticFunction();
    }

    static Book book = new Book();

    static
    {
        System.out.println("书的静态代码块");
    }

    {
        System.out.println("书的普通代码块");
    }

    Book()
    {
        System.out.println("书的构造方法");
        System.out.println("price=" + price +",amount=" + amount);
    }

    public static void staticFunction(){
        System.out.println("书的静态方法");
    }

    int price = 110;
    static int amount = 112;
}

上述代码的类初始化方法是:

  static Book book = new Book();
  static
    {
        System.out.println("书的静态代码块");
    }
  static int amount = 112;
  

可以看到第一行出现了new字样,那么就要先立即跳转执行对象初始化方法,注意此时类初始化方法重的静态代码块和静态变量static int amount的初始化还未进行,也就是此时amount的值=0:

   {
       System.out.println("书的普通代码块");
   }

   Book()
   {
       System.out.println("书的构造方法");
       System.out.println("price=" + price +",amount=" + amount);
   }
   int price = 110;
    

于是此时 price 赋予 110 的值,输出书的普通代码块、书的构造方法、price=10,amount = 0,当对象实例化完成之后,JVM 继续进行类初始化方法:接着new之后,输出书的静态代码块、amount = 112。到这里,类的初始化已经完成,JVM 执行 main 方法的内容,输出书的静态方法。因此上面程序的最终输出为:

书的普通代码块
书的构造方法
price=110,amount=0
书的静态代码块
书的静态方法

参考文章:

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值