对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");
网友评论