美文网首页java
JVM--类加载与反射

JVM--类加载与反射

作者: aruba | 来源:发表于2021-10-04 21:49 被阅读0次

    对JVM的内存有了一定理解后,再来看JVM是如何加载类,以及Java的反射机制

    一、类加载过程

    有了前面的了解,我们知道Java文件先要编译成class文件,再由JVM加载class到方法区成为类元信息,最后实例化class对象,加载类的过程又可以细分为:加载、连接、初始化、使用、卸载

    类加载生命周期
    1.加载(Loading)

    Java编译为class文件后,在使用类时,JVM如果没有加载过class,则会先加载class文件,加载可以用读文件操作来理解,就是将文件内容加载到内存中,转化为类元信息,作为方法区这个类的各种数据访问入口,并实例化Class对象,存放在堆中

    2.连接(Linking)
    2.1 验证(Verifivation)

    class文件为字节码,根据字节码对应表进行验证,如:对该class文件进行标志头校验,class文件的前4个字节都是 “0xCAFEBABE”

    2.2 准备(Preparation)

    根据字节码,如果有静态成员变量,那么在方法区为它们分配内存,并将内存清零(类似c语言memset函数),如果是静态常量(final修饰),那么此时就会赋值,字符串比较特殊,它会分配在字符串常量池中

    2.3 解析(Resolution)

    根据字节码对照表把Constant Pool Table中的符号转换成直接引用
    每个符号为一个指针,解析时,将符号指向对应的内存首地址(变量、函数、函数类型结构体等)
    栈帧中的动态链接也是使用这种机制,一个方法对应一个指针,指向了常量池中的符号,符号指向一个方法,来执行方法中的代码

    下面class文件反编译的内容,可以作为参考:

    public class Hello {
        public String name = "aaa";
        private final static int nameConst = 123;
        private static int nameStatic = 1234;
    
        private int test() {
            int a = 3;
            int b = 4;
    
            return a + b;
        }
    
        public int test(int a) {
            int b = 4;
    
            return a + b;
        }
    
    }
    
    Classfile /C:/Users/tyqhc/Documents/javaworkspace/myJava/out/production/myJava/Hello.class
      Last modified 2021-10-13; size 651 bytes
      MD5 checksum 57a1c2ff200580304191ccda4feaea70
      Compiled from "Hello.java"
    public class Hello
      SourceFile: "Hello.java"
      minor version: 0
      major version: 51
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #6.#29         //  java/lang/Object."<init>":()V
       #2 = String             #30            //  aaa
       #3 = Fieldref           #5.#31         //  Hello.name:Ljava/lang/String;
       #4 = Fieldref           #5.#32         //  Hello.nameStatic:I
       #5 = Class              #33            //  Hello
       #6 = Class              #34            //  java/lang/Object
       #7 = Utf8               name
       #8 = Utf8               Ljava/lang/String;
       #9 = Utf8               nameConst
      #10 = Utf8               I
      #11 = Utf8               ConstantValue
      #12 = Integer            123
      #13 = Utf8               nameStatic
      #14 = Utf8               <init>
      #15 = Utf8               ()V
      #16 = Utf8               Code
      #17 = Utf8               LineNumberTable
      #18 = Utf8               LocalVariableTable
      #19 = Utf8               this
      #20 = Utf8               LHello;
      #21 = Utf8               test
      #22 = Utf8               ()I
      #23 = Utf8               a
      #24 = Utf8               b
      #25 = Utf8               (I)I
      #26 = Utf8               <clinit>
      #27 = Utf8               SourceFile
      #28 = Utf8               Hello.java
      #29 = NameAndType        #14:#15        //  "<init>":()V
      #30 = Utf8               aaa
      #31 = NameAndType        #7:#8          //  name:Ljava/lang/String;
      #32 = NameAndType        #13:#10        //  nameStatic:I
      #33 = Utf8               Hello
      #34 = Utf8               java/lang/Object
    {
      public java.lang.String name;
        flags: ACC_PUBLIC
    
      public Hello();
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: aload_0
             5: ldc           #2                  // String aaa
             7: putfield      #3                  // Field name:Ljava/lang/String;
            10: return
          LineNumberTable:
            line 6: 0
            line 7: 4
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                   0      11     0  this   LHello;
    
      public int test(int);
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=3, args_size=2
             0: iconst_4
             1: istore_2
             2: iload_1
             3: iload_2
             4: iadd
             5: ireturn
          LineNumberTable:
            line 19: 0
            line 21: 2
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                   0       6     0  this   LHello;
                   0       6     1     a   I
                   2       4     2     b   I
    
      static {};
        flags: ACC_STATIC
        Code:
          stack=1, locals=0, args_size=0
             0: sipush        1234
             3: putstatic     #4                  // Field nameStatic:I
             6: return
          LineNumberTable:
            line 9: 0
    }
    
    
    3.初始化(Initialization)

    类初始化阶段是类加载过程的最后一步。在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的java程序代码(字节码)。
    在准备阶段,只是对静态成员变量进行了内存分配和内存初始化。初始化阶段才会对成员变量进行赋值,相当于执行构造函数中的内容,以及执行static代码块中的内容

    4.为对象分配内存的两种方式
    • 指针碰撞:结合上次垃圾回收机制的知识,如果内存是规整的,那么只要移动尾部指针,就可以给对象分配内存了,类似双指针算法
    • 空闲列表:如果有内存碎片,那么会维护一张空闲列表,来记录连续的内存空间,从这个列表中查找可以存放的下的内存空间

    二、类加载时机

    我们了解了类加载的流程后,试想下,什么时候会触发类的加载呢?


    类加载时机

    三、类加载器-双亲委派机制

    类加载时,如果以前加载过,那么就不需要加载该类,实现这个机制的,就是双亲委派
    子加载器不断往上询问是否加载过,再有顶至下加载该类,可以加载就直接加载,否则往下委派加载

    双亲委派机制

    四、反射

    反射是Java中一种机制,它能够帮助我们动态的使用一个类,其本质就是获取类元信息,并通过符号引用来操作内存或调用方法
    例子使用的类如下:

    public class Hello {
        public String name;
        private String namePrivate;
    
        public int test() {
            int a = 3;
            int b = 4;
    
            return a + b;
        }
    
        public int test(int a) {
            int b = 4;
    
            return a + b;
        }
    
    }
    
    1.获取Class对象

    JVM加载类后,会在方法区存在一份类元信息,我们可以通过以下方法获取它:

    • Class.forName("xx") : 根据包名加类名获取
    • Hello.class:通过类.class获取
    • h.getClass():实例化后,对象的getClass方法获取
    2.获取方法和调用方法

    获取到Class后,我们就可以通过以下两种方式,获取方法对象Method,并通过Method的invoke方法反向调用方法

    • getMethod:获取类中的公有方法和父类的公有方法
        //getMethod只能获取公有方法,但是也可以获取父类公有方法
        Method method = helloClass.getMethod("test", int.class);
        method.invoke(h, 5);
    
    • getDeclaredMethod:获取该类中的所有方法
        //getDeclaredMethod可以获取该类中的所有方法,但是不可以获取父类方法
        Method method = helloClass.getDeclaredMethod("test");
        method.setAccessible(true);
        method.invoke(h);
    
    3.获取构造方法和通过Class实例化对象

    Class对象还可以获取构造方法:

        Constructor<Hello> constructor = helloClass.getConstructor();
    

    同样的,私有构造方法可以通过getDeclaredConstructor方法获取,setAccessible(true)后才可以调用

    我们可以通过构造方法来实例化对象:

        Hello h2 = constructor.newInstance();
    

    还可以通过Class直接实例化,该方式只能是无参构造

        Hello h3 = helloClass.newInstance();
    
    4.获取属性和设置属性

    通过以下两种方式,获取属性Field对象,并通过Filed的set方法,来为对象设置新值

    • getField:获取公有属性和父类共有属性
        Field name = helloClass.getField("name");
        name.set(h, "hello");
    
    • getDeclaredField:获取类的所有属性
        Field name = helloClass.getDeclaredField("namePrivate");
        name.setAccessible(true);
        name.set(h, "helloPrivate");
    

    相关文章

      网友评论

        本文标题:JVM--类加载与反射

        本文链接:https://www.haomeiwen.com/subject/qquvnltx.html