从一个简单的main方法执行谈谈JVM工作机制

本文通过一个简单的main方法执行示例,详细介绍了JVM的工作流程,包括JVM启动、类加载过程、main方法的执行步骤等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        对java coder来说, 经常接触JVM,可能不需要熟悉JVM工作原理,也能根据业务需求,通过代码实现其功能模块,一般不需要对JVM有特别的了解。但是,如果想精通java开发,需要对JVM的工作原理有一定的理解。本来JVM的工作原理浅到可以泛泛而谈,但如果真的想把JVM工作机制弄清楚,实在是很难,涉及到的知识领域太多。所以,本文通过简单的mian方法执行,浅谈JVM工作原理,看看JVM里面都发生了什么。

        先上代码:

package org.test;

public class Test {
	
	private int invar = 1;
	
	public static String concat(String str1, String str2) {
		return str1+str2;
	}
	
	public int f() {
		return invar;
	}
	
	public static void main(String[] args) {
		Test test = new Test();
		test.f();
		int localInt1 = 1;
		int localInt2 = 2;
		int sum = localInt1 + localInt2;
		String str = concat("string ", "concat");
		System.out.println(str);
	}
	
}
      再看看JVM内部结构:


        上图是对《The Java® Virtual Machine Specification Java SE 7 Edition》中JVM内部结构的一个描述简图,“The Java® Virtual Machine Specification”是JVM的一个抽象规范,主流JVM实现的主要有Oracle HotSpot、IBM J9等。

        现在来看看启动org.test.Test的main方法,JVM(本文涉及到的JVM implementation是针对JDK 7的HotSpot,下同)会做些什么:

1. JVM启动:

        根据JVM的启动参数分配JVM runtime data area内存空间,如根据-Xms、-Xmx分配Heap大小;根据-XX:PermSize、-XX:MaxPermSize分配Method area大小;根据-Xss分配JVM Stack大小。注意,Method area、Heap是所有JVM线程都共享的,在JVM启动时就会创建且分配内存空间;JVM Stack、PC Register、Native Method Stack是每个线程私有的,都是在线程创建时才分配。在HotSpot中,没有JVM Stacks和Native Method Stacks之分,功能上已经合并。官方说明:Both Java programming language methods and native methods share the same stack.

2. Loading, Linking, and Initializing

        所有这些工作都是Class Loader Subsystem来完成,对org.test.Test.class进行加载、链接、初始化。

2.1 Loading

       将Test.class加载为Method area的Test Class data,Test.class是一个二进制文件,里面是JVM编译器对org.test.Test.java编译成的字节码,关于.class字节码的解读,请看《实例分析Java Class的文件结构》,讲得非常透彻。Test.class在Method area是如何存储的?这个问题的解答,首先还是要对Method area有一个认识。先看看《The Java® Virtual Machine Specification Java SE 7 Edition》中对ClassFile的定义:


        也就是说Test.class里面的二进制码是按照ClassFile的结构一个一个字节来存储相应的Test.java编译后的信息。所有这些信息被类加载器加载会对应地存储到Method area中,尽管体现的方式不一样,例如:Test.class中的Constant pool对应Method area中的Runtime constant pool,Runtime constant pool中的Constant中的信息都是从Constant pool中获取到的,Runtime constant pool里面都是些符号引用、字符串字面值以及整形、浮点型常量。下面是“JVM Method Area Class Data结构示意图”:

                                                               

        实际上,在JVM的method area中,一个class或interface是由其全限定名称和真正加载该类或接口的类加载器(即the defining classloader)来唯一确定的,因为一个类(如果无特殊说明,“类”表示class或interface,下同)可以由不同的类加载器加载。

        这里要弄清楚The Java® Virtual Machine Specification Java SE 7 Edition》中关于类加载器的两个概念:the defining loader和the initiating loader。假设有两个类加载器classloader1和classloader2,classloader1是classloader2的parent classloader(委派的),一个待加载的类A,现在classloader2加载类A,首先委派classloader1去加载类A,结果classloader1加载类A成功了,那这里classloader1就是类A的“the defining loader”即,classloader2就是类A的the initiating loader即,因为类加载动作由classloader2发起的。关于类的可见性:对于类A、类B,类A是否对类B可见(即类B能找到类A)取决于类B的“the defining loader”能否自己或通过委派加载类A成功。

        所以在method area中,首先根据“the defining loader”来划分所加载的类的范围,这是逻辑意义上的概念,而对于每一个Class Data里面存储什么内容,上图已经比较清晰给出答案。这里需要注意的是Class的static变量是存储在Field data中,而final static则存储在Run-time constant pool中,Code的JVM指令存储在Method data中,另外,Class Data中还有两个引用:一个是指向“the defining loader”的引用,该引用在Class的动态链接解析时需要用到,如果该Class中引用了其他类,那么在动态链接解析时,就用该类的“the defining loader”去找其他类,完成其加载、链接、初始化动作;另一个是指向Class实例(即java.lang.Class对象)的引用,而JDK提供java.lang.Class出来,这样java开发者就可以通过该引用获取到存储在method area中的Class Data,即有哪些Field、哪些Method,这就是大家熟悉的反射。

      JVM在加载Test.class到Method area后,在使用其之前,还需要做一些工作,就是接下来的Linking和Initializing

2.2 Linking

        链接一个类包括对该类、其直接超类、其直接superinterfaces进行验证(verifying)、准备(preparing),如果该类为数组类型,还包括对数组元素类型的链接。Loading, Linking, and Initializing的时间顺序遵循以下两个原则:

(1)一个类一定要在链接之前加载完成;

(2)一个类的验证和准备一定要在初始化之前完成。

        关于类中的符号引用什么时候进行解析,这个要看JVM的实现。例如有的JVM实现采用懒解析("lazy"or "late" resolution),即在需要进行该符号引用的解析时才去解析它,这样的话,可能该类都已经初始化完成了,如果其他的类链接到该类中的符号引用,需要进行解析,这个时候才会去解析;还有的JVM实现采用静态解析("eager" or "static" resolution),即在该类在进行验证时一次性将其中的符号引用全部解析完成。在JVM的实现中,一般采用懒解析。


2.2.1 verifying

        验证.class文件在结构上满足JVM规范的要求,验证工作可能会引起其他类的加载但不进行验证和准备。

2.2.2 preparing

       给Class static变量分配内存空间,初始化为默认值即将内存空间清零。

       关于对类的方法声明,需要满足以下加载约束条件:

假设为类C的the defining loader”即为类D的the defining loader”即,D为C的超类或superinterface,对于C中所Override D的方法m,m的返回类型为,参数类型为,如果不是数组类型,假设,否则的元素类型,对于i=1,...,n,如果不是数组类型,假设,否则的元素类型,则对于i=0,...,n满足:,即都能自己或通过委派成功加载

       更进一步,假设为C的superinterface,为C的superclass,中声明了方法m,中声明且实现了方法m,则对于i=0,...,n满足:

2.2.3 resolution

       详细见:Resolution in《The Java® Virtual Machine Specification Java SE 7 Edition》

2.3 Initializing

        详细见:Initialization in《The Java® Virtual Machine Specification Java SE 7 Edition》

3. 启动main线程

       在JVM实现中,线程为Execution Engine的一个实例,main函数是JVM指令执行的起点,JVM会创建main线程来执行main函数,以触发JVM一系列指令的执行,真正地把JVM run起来。在创建main线程时,会为其分配私有的PC Register、JVM Stack、Native Method Stack,当然在HotSpot的实现中,JVM Stack、Native Method Stack功能上已经合并,下面以HotSpot为例来说说main函数的执行。

4. 执行main函数

      先用javap -c Test.class,通过反汇编把Test.class对应的JVM机器码弄出来:

Compiled from "Test.java"
public class org.test.Test {
  public org.test.Test();
    Code:
       0: aload_0
       1: invokespecial #10                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_1
       6: putfield      #12                 // Field invar:I
       9: return

  public static java.lang.String concat(java.lang.String, java.lang.String);
    Code:
       0: new           #20                 // class java/lang/StringBuilder
       3: dup
       4: aload_0
       5: invokestatic  #22                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
       8: invokespecial #28                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
      11: aload_1
      12: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      15: invokevirtual #35                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      18: areturn

  public int f();
    Code:
       0: aload_0
       1: getfield      #12                 // Field invar:I
       4: ireturn

  public static void main(java.lang.String[]);
    Code:
       0: new           #1                  // class org/test/Test
       3: dup
       4: invokespecial #46                 // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #47                 // Method f:()I
      12: pop
      13: iconst_1
      14: istore_2
      15: iconst_2
      16: istore_3
      17: iload_2
      18: iload_3
      19: iadd
      20: istore        4
      22: ldc           #49                 // String string
      24: ldc           #51                 // String concat
      26: invokestatic  #52                 // Method concat:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
      29: astore        5
      31: getstatic     #54                 // Field java/lang/System.out:Ljava/io/PrintStream;
      34: aload         5
      36: invokevirtual #60                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      39: return
}

        JVM指令后面的#index表示ClassFile中常量池“数组”的索引,实际上线程中每一个函数的执行都对应一个帧在JVM Stack中的压入弹出,帧包含局部变量数组(用来存储函数参数、局部变量等)、操作数栈(用于配合JVM指令的执行,存储来自局部变量数组和类属性的值及中间结果,其中操作数栈中的值可以为直接引用)、一个指向当前方法所在类的runtime constant pool(用于符号引用解析):


main线程调用main函数时,先创建一个main帧,根据编译时期就已经确定的局部变量数组和操作数栈的大小分配内存空间,将内存空间清零,将main帧压入main线程 Stack中:


看看main中的指令:

0: new  #1    // class org/test/Test
3: dup

0: new  #1:#1表示Test.class中常量池“数组”的索引,该索引位置为CONSTANT_Class_info常量,表示Test class,这里的new指令表示new一个org/test/Test对象,且将其引用压入操作数栈中;3: dup:在操作数栈中,复制栈顶的操作数,同时将其压入栈顶。

在执行JVM指令的过程中,main线程的PC register会记录当前所执行的JVM指令的地址。执行完这两条指令后,只有操作数栈有变化:

4: invokespecial #46                 // 调用Method "<init>":()V
这条指令操作是:弹出操作数栈顶的元素,在该元素所指向的Test对象上,调用<init>方法,其实就是对该对象进行初始化,<init>的参数为空,返回为V的类型即为空,执行完这条指令后,main帧如下:


7: astore_1 // 弹出操作数栈顶元素,将该引用存放到局部变量“数组”索引1的位置
8: aload_1  // 将局部变量“数组”索引1的位置的引用压入操作数栈
执行完后,main帧如下:

9: invokevirtual #47     // 在Test对象上,调用Method f:()I
在执行invokevirtual指令时,在main函数中又调用了函数f(),所以main线程会创建一个帧即f帧,为f帧分配相应的内存空间,保存main帧的状态,即保存局部变量数组、操作数栈中的值,以便f()调用 完成后再恢复,,将f帧压入main线程的Stack,将f帧切换成当前帧,执行f()函数的字节码,main线程PC register记录当前执行的指令地址:


f帧中局部变量“数组”索引0的位置为什么是reference 1?这是因为在invokevirtual指令执行过程中,首先会将main帧中操作数栈中的栈顶元素弹出,将其传给f帧,f帧将其存放到局部变量”数组“中,下面是执行f()函数中的指令:

0: aload_0 // 将局部变量“数组”索引0位置的引用压入操作数栈中

1: getfield      #12   // 弹出操作数栈顶元素即Test对象引用,在该引用指向的对象上获取属性Field invar:I的值,再将该值压入操作数栈中
4: ireturn     //  弹出f帧中操作数栈顶元素即1,将其压入main帧中的操作数栈

执行ireturn指令,实际上表明f()函数调用完成返回,main线程会释放f帧及其内存空间,将main帧切换成当前帧,恢复main帧的状态,main线程的PC register记录main帧中当前执行指令的地址,继续执行完main帧后面的指令。

5. JVM退出

        释放main线程所占用资源及内存空间,如PC register、JVM Stack等,释放JVM所占用的内存空间,如Heap、Method area,JVM退出。

虽然只是一个简单的main方式执行,但通过这个简单的示例可以看到JVM完整的工作流程。

<think>首先,用户的问题是:“我想了解Java反射机制的概念和用法请问Java反射机制详解”。这是一个关于Java反射机制的详细说明请求。用户提供了几个引用:-引用[1]:提到了反射相关的主要API,如java.lang.Class,java.lang.reflect.Method等,以及类的加载过程。-引用[2]:解释了编译期和运行期的概念,以及反射的基本定义:运行时动态获取类的信息并调用方法和属性。-引用[3]:演示了如何通过Class类获取实现的接口。-引用[4]:描述了如何获取构造方法。我的角色是生成相关问题,并根据系统级指令回答。系统级指令包括:-使用$...$格式表示数学表达式(但这里可能不涉及)。-独立公式使用$$...$$格式(可能不适用)。-LaTeX语法正确(确保代码和公式正确)。-使用中文回答。-生成相关问题:在回答后添加相关问题。-引用的段落末尾自然地添加引用标识,如[^1]。所以,我需要在回答中使用中文,结构清晰,帮助用户逐步理解反射机制。内容提纲:1.**概念介绍**:从引用[2]开始,解释反射机制的定义:Java在运行时加载未知类、获取构造方法、实例化对象、调用方法等。2.**核心API**:基于引用[1],列出关键类和它们的作用:Class,Method,Field,Constructor。3.**类和对象加载**:简要描述类的加载过程(引用[1]提到类的加载过程,但没有细节,我可以简单提及或省略)。4.**用法示例**:参考引用[3]和[4],提供基本代码示例:-获取Class对象。-获取方法、构造器。-调用方法、访问字段。5.**详细解释**:覆盖反射的主要方面:动态加载、获取成员、调用、安全性等。确保回答真实可靠:基于提供的引用和我的知识。我是AI,但我会依赖引用内容。引用[^1],[^2],[^3],[^4]要自然地整合。回答结构:-引言:解释反射是什么。-核心概念:使用引文支持。-详细用法:分步骤或子主题解释。-代码示例:使用Markdown代码块显示Java代码。-总结。-生成相关问题。最终输出:用户可见层,回答结构清晰。生成相关问题在回答后添加。引用标识:在引用的部分末尾添加,例如:[^1],[^2]等。根据内容合理添加。计划回答:1.**Java反射机制的概念**:-定义反射:解释运行时动态性。-引用[2]:编译期vs运行期,反射的意义[^2]。2.**核心API详解**:-Class类:代表类。-Method类:代表方法。-Field类:代表字段。-Constructor类:代表构造器。-基于引用[1][^1]。3.**类加载过程简介**(可选,简要)。4.**用法细节**:-如何获取Class对象(Class.forName(),.class语法)。-获取构造器并实例化对象:引用[4]的例子[^4]。-获取方法并调用:如引用[3]的例子[^3]。-访问字段。-处理访问控制:getDeclaredMethods等。5.**优势与注意事项**:动态性、性能影响等。6.**完整代码示例**:展示一个完整的反射使用场景。7.**生成相关问题**。在回答中自然地添加引用标识。最终回答以中文书写。</think> ### Java 反射机制详解 Java 反射机制(Reflection)是 Java 语言的核心特性之一,它允许程序在运行时动态地加载、检查和操作类、对象、方法和字段,而不需要在编译时静态地知道这些细节。这种机制提供了极高的灵活性和动态性,常用于框架开发(如Spring)、测试工具、序列化/反序列化库等场景。下面,我将基于核心概念和相关API,详细解释反射的用法。 #### 1. **反射机制的概念** Java 反射机制的本质是:**在运行时获取类的信息并操作对象的行为**。编译期(编译器将Java代码转换为字节码文件)与运行期(JVM执行字节码)分离后,Java程序可以通过反射加载运行时才得知类名的类,获取类的完整结构(如构造方法方法、字段),实例化对象,并调用其方法或访问属性。这称为“动态反射操作”,它突破了静态编译的限制[^2]。例如: - **动态加载类**:在运行时加载未知类名或未在编译期导入的类。 - **动态调用**:调用未知类的方法或访问属性。 - **核心目标**:提高代码的灵活性和可扩展性,常用于框架实现。 反射的优势在于能处理运行时变化的类,但需要注意性能开销(运行时反射比静态调用慢)和安全性(可能访问私有成员)。 #### 2. **反射的核心 API** Java 反射机制依赖于 `java.lang.reflect` 包中的关键类,每个类代表一种类的结构元素: - **`java.lang.Class`**:代表一个类本身,是反射的入口点。它可以通过多种方式获取,如 `类名.class` 或 `Class.forName("类名")`。 - **`java.lang.reflect.Method`**:代表类的方法,可以调用该方法的 `invoke()` 执行。 - **`java.lang.reflect.Field`**:代表类的成员变量(字段),可以获取或设置字段值。 - **`java.lang.reflect.Constructor`**:代表类的构造器,用于实例化对象[^1]。 这些 API 让您能通过 `Class` 对象获取所有类元信息,并进行操作。例如,`Class.getDeclaredMethods()` 获取所有方法(包括私有方法),而 `Class.getMethods()` 只获取公开方法。 #### 3. **反射的用法详解** 反射的用法主要包括获取类信息、操作对象和访问成员。以下是关键步骤和代码示例(所有代码使用标准 Java 语法)。 ##### **步骤 1: 获取 Class 对象** 获取 `Class` 对象是反射的第一步,常见方式包括: - 使用 `.class` 后缀:静态获取。 - 使用 `Class.forName()`:动态加载类(适用于运行时类名未知)。 ```java // 示例:获取 String 类的 Class 对象 Class<?> strClass = String.class; // 静态方式 Class<?> intClass = Class.forName("java.lang.Integer"); // 动态加载类 System.out.println("类名: " + intClass.getName()); // 输出: java.lang.Integer ``` ##### **步骤 2: 获取并调用方法** 通过 `Class` 对象获取 `Method`,并用 `invoke()` 调用方法。示例:获取 `Integer` 类的公开方法并调用。 ```java import java.lang.reflect.Method; public class ReflectionExample { public static void main(String[] args) throws Exception { // 获取 Integer 类的 Class 对象 Class<?> intClass = Class.forName("java.lang.Integer"); // 获取特定方法,例如 parseInt 方法 Method parseIntMethod = intClass.getMethod("parseInt", String.class); // 调用方法: 参数为待转换的字符串 int result = (Integer) parseIntMethod.invoke(null, "123"); System.out.println("parseInt 结果: " + result); // 输出: 123 // 获取所有方法(包括继承和接口) Method[] methods = intClass.getMethods(); for (Method method : methods) { System.out.println("方法名: " + method.getName()); } } } ``` 此示例展示了动态调用静态方法 `parseInt`,并通过 `getMethods()` 列出所有公开方法[^3]。 ##### **步骤 3: 获取并实例化对象(构造器)** 使用 `Constructor` 类创建对象实例。示例:获取私有构造器并强制访问(需设置访问权限)。 ```java import java.lang.reflect.Constructor; public class ReflectionExample { public static void main(String[] args) throws Exception { Class<?> listClass = Class.forName("java.util.ArrayList"); // 获取无参公开构造器并实例化 Constructor<?> publicConstructor = listClass.getConstructor(); Object listObject = publicConstructor.newInstance(); System.out.println("实例化对象: " + listObject.getClass()); // 输出: class java.util.ArrayList // 获取私有构造器(需访问控制) Constructor<?>[] allConstructors = listClass.getDeclaredConstructors(); for (Constructor<?> constructor : allConstructors) { constructor.setAccessible(true); // 开启访问权限 Object instance = constructor.newInstance(); // 实例化 System.out.println("使用私有构造器: " + instance); } } } ``` 这里,`getDeclaredConstructors()` 获取所有构造器(包括私有),`setAccessible(true)` 可绕过访问修饰符限制[^4]。 ##### **步骤 4: 访问和修改字段** 使用 `Field` 类读取或设置对象字段。示例:修改对象的私有字段。 ```java import java.lang.reflect.Field; public class ReflectionExample { public static void main(String[] args) throws Exception { Class<?> personClass = Class.forName("Person"); Object person = personClass.getConstructor().newInstance(); // 获取私有字段 "name" Field nameField = personClass.getDeclaredField("name"); nameField.setAccessible(true); // 开启访问 nameField.set(person, "John Doe"); // 设置值 System.out.println("姓名: " + nameField.get(person)); // 输出: John Doe } } // 辅助类 Person class Person { private String name = ""; public Person() {} } ``` 此例中,`getDeclaredField()` 获取私有字段,`set()` 修改其值[^1]。 ##### **步骤 5: 其他操作:接口和类加载** - **获取实现接口**:使用 `Class.getInterfaces()`,如引用[3]中的示例[^3]。 - **类加载过程**:类在运行时由 ClassLoader 加载到 JVM,形成 `Class` 对象(涉及加载、链接、初始化阶段)[^1]。 #### 4. **反射的实际应用场景和注意事项** - **典型应用**:常用于框架开发(如Spring依赖注入)、单元测试(Mock对象)、序列化库(JSON解析)、动态代理。 - **注意事项**: - **性能**: 反射操作比静态调用慢(涉及动态解析),应避免在高频代码中使用。 - **安全性**: 反射可访问私有成员,可能破坏封装性;启用安全管理器限制反射操作。 - **兼容性**: 类结构变更可能导致反射代码失败,确保错误处理(如 `ClassNotFoundException`)。 #### 5. **总结** Java 反射机制一个强大的工具,通过运行时操作类元数据,实现了高度的动态性和灵活性。核心包括 `Class`、`Method`、`Field` 和 `Constructor` 类,使用流程涉及获取类对象、操作成员和实例化对象。结合代码示例,您可以快速上手反射的使用。但在实际应用中,需权衡性能与灵活性[^2][^4]。 ### 相关问题 1. Java 反射机制在 Spring 框架中的具体应用有哪些例子? 2. 如何通过反射实现动态代理模式? 3. 反射机制的性能优化技巧有哪些? 4. Java 反射与类加载器(ClassLoader)的关系是怎样的? 5. 在处理安全敏感操作时,如何正确使用反射以避免安全问题? [^1]: 反射相关的主要API包括Class、Method、Field等,代表类结构元素[^1]。 [^2]: 反射是Java在运行时加载和操作未知类的机制,依赖于运行期动态性[^2]。 [^3]: 通过Class获取接口信息,如Integer实现的接口[^3]。 [^4]: 使用getDeclaredConstructors可获取所有构造方法,包括私有方法[^4]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值