发表时间:2022-03-25来源:网络
本篇是《深入理解JVM》系列博客的第一篇,旨在全局把控,先对整体流程有个认识,然后再分阶段详解。程序从编写到执行整体可以划分为以下几个步骤:编辑源码->编译生成class文件->(加载class文件、运行class字节码文件、垃圾回收),其中后两个步骤都是在jvm虚拟机上执行的,整体的执行流程如下:
编辑源代码是经历的第一个环节,编辑源代码,就是我们在任何一个工具上编写源代码,可以是记事本,也可以是IDE,这部分相当于我们在IDEA上 新建一个.java的Class 然后写内容,这里我们创建几个类和接口:
//父类Person public class Person { //成员变量 private String name; private int age; //构造方法 public Person(int age, String name){ this.age = age; this.name = name; } //成员方法 public void run(){ } } //接口IStudy public interface IStudy { int study(int a, int b); }真正的Strudent类,实现接口IStudy 和继承父类Person:
public class Student extends Person implements IStudy { //私有静态成员变量 private static int cnt=5; //静态方法块 static{ cnt++; } //私有成员变量 private String sid; //构造方法 public Student(int age, String name, String sid){ //继承父类构造方法 super(age,name); this.sid = sid; } //父类方法重写 public void run(){ System.out.println("run()..."); } //实现接口方法 public int study(int a, int b){ int c = 10; int d = 20; return a+b*c-d; } //成员方法 public static int getCnt(){ return cnt; } //方法加载入口 public static void main(String[] args){ Student s = new Student(28,"tml","20210201"); //接口方法调用 s.study(5,6); //成员方法调用 Student.getCnt(); //父类重写方法调用 s.run(); } }可以从文件路径看到已经有三个Java文件生成了:
编译的过程就是生成.class字节码文件,输入命令javac Student.java将该源码文件.java编译生成.class字节码文件。由于在源码文件中定义了两个类,一个接口,所以生成了3个.clsss文件。
字节码文件是真正实现Java语言跨平台的基石,JVM运行的是class字节码文件,只要是这种格式的文件就行
所以其实可以看的出它实现了跨平台和跨语言,用一张图可以描述清楚
在类文件夹目录下可以执行类问题,用命令javap -c Student 执行类class文件结构如下 :
"C:\Program Files\Java\jdk1.8.0_251\bin\javap.exe" -c Student.class Compiled from "Student.java" public class com.company.Student extends com.company.Person implements com.company.IStudy { public com.company.Student(int, java.lang.String, java.lang.String); Code: 0: aload_0 1: iload_1 2: aload_2 3: invokespecial #1 // Method com/company/Person."":(ILjava/lang/String;)V 6: aload_0 7: aload_3 8: putfield #2 // Field sid:Ljava/lang/String; 11: return public void run(); Code: 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #4 // String run()... 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return public int study(int, int); Code: 0: bipush 10 2: istore_3 3: bipush 20 5: istore 4 7: iload_1 8: iload_2 9: iload_3 10: imul 11: iadd 12: iload 4 14: isub 15: ireturn public static int getCnt(); Code: 0: getstatic #6 // Field cnt:I 3: ireturn public static void main(java.lang.String[]); Code: 0: new #7 // class com/company/Student 3: dup 4: bipush 28 6: ldc #8 // String tml 8: ldc #9 // String 20210201 10: invokespecial #10 // Method "":(ILjava/lang/String;Ljava/lang/String;)V 13: astore_1 14: aload_1 15: iconst_5 16: bipush 6 18: invokevirtual #11 // Method study:(II)I 21: pop 22: invokestatic #12 // Method getCnt:()I 25: pop 26: aload_1 27: invokevirtual #13 // Method run:()V 30: return static {}; Code: 0: iconst_5 1: putstatic #6 // Field cnt:I 4: getstatic #6 // Field cnt:I 7: iconst_1 8: iadd 9: putstatic #6 // Field cnt:I 12: return } Process finished with exit code 0可以看到字节码文件存放了这个类的各种信息:字段、方法、父类、实现的接口等各种信息
在IDEA的Settings里进行相关配置即可,配置截图如下:
填写的相关属性信息如下:
配置好以上信息在对应的类右键查看字节码即可:
File -> setting -> plugins路径下直接安装jclasslib:
重启后直接打开view 查看:
可以直观的看到每个方法调用时局部变量表的内容以及操作数栈的操作,如果用Jclasslib来查看字节码比IDEA集成的看起来更清晰一些,内容如下,
在命令行中输入java Student这个命令,就启动了一个java虚拟机,然后加载Student.class字节码文件到内存,然后运行内存中的字节码指令了。这部分的操作就相当于我们在IDEA这样的ide上 点击运行按钮。整个字节码class文件执行的整体宏观如图所示:
虚拟机JVM负责核心的加载.class文件、将.class文件转为机器码,最终执行机器码。JVM的功能模块主要包括类加载器、执行引擎和垃圾回收系统。同时JVM有自己的内存模型。
JVM中把内存分为方法区、Java栈、Java堆、本地方法栈、PC寄存器 5部分数据区域:
方法区:用于存放类、接口、常量以及静态变量等元数据信息,加载进来的字节码数据都存储在方法区虚拟机栈 :执行引擎运行字节码时的运行时内存区,采用栈帧的形式保存每个方法的调用运行数据本地方法栈:执行引擎调用本地方法时的运行时内存区Java堆:运行时数据区,各种对象一般都存储在堆上PC寄存器(程序计数器):功能如同CPU中的PC寄存器,指示要执行的字节码指令。这部分内容在Jvm运行时内存分析这篇blog详细解析
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化五个阶段,其中验证、准备、解析三个部分统称链接
各个阶段需要对代码操作内容如下:
加载阶段类加载过程中主要是将class文件(准确地讲,应该是类的二进制字节流)加载到虚拟机内存中,真正执行字节码的操作,在加载完成后才真正开始
执行引擎找到main这个入口方法,执行其中的字节码指令,这里就依赖到了Java栈,也就是虚拟机栈,其结构如下:
每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。执行引 擎运行的所有字节码指令都只针对当前栈帧进行操作
注意当存在方法调用关系时遵循以上两个原则
main方法对应的下面是我们执行的代码:
//方法加载入口 public static void main(String[] args){ Student s = new Student(28,"tml","20210201"); //接口方法调用 s.study(5,6); //成员方法调用 Student.getCnt(); //父类重写方法调用 s.run(); }其对应的字节码指令如下:
public static void main(java.lang.String[]); Code: 0: new #7 // class com/company/Student 3: dup 4: bipush 28 6: ldc #8 // String tml 8: ldc #9 // String 20210201 10: invokespecial #10 // Method "":(ILjava/lang/String;Ljava/lang/String;)V 13: astore_1 14: aload_1 15: iconst_5 16: bipush 6 18: invokevirtual #11 // Method study:(II)I 21: pop 22: invokestatic #12 // Method getCnt:()I 25: pop 26: aload_1 27: invokevirtual #13 // Method run:()V 30: return首先会在虚拟机栈中为main方法创建栈帧,局部变量表长度为2,slot0存放参数args,slot1存放局部变量Student s,操作数栈最大深度为5:
然后执行new#7指令,在java堆中创建一个Student对象,并将其引用值放入main栈帧中
最后初始化一个对象所需的参数入操作数栈(通过实例构造的方式):
入栈后的整体效果如下

invokespecial #10:调用#10这个常量所代表的方法,即Student.()这个方法,这步是为了初始化对象s的各项值,()方法,是编译器将调用父类的()的语句、构造代码块、实例字段赋值语句,以及自己编写的构造方法中的语句整合在一起生成的一个方法。保证调用父类的()方法在最开头,自己编写的构造方法语句在最后,而构造代码块及实例字段赋值语句按出现的顺序按序整合到()方法中。此时需注意:上边从dup到ldc #9这四条指令向栈中添加了4个数据,而Student.()方法刚好也需要4个参数:
//构造方法 public Student(int age, String name, String sid){ //继承父类构造方法 super(age,name); this.sid = sid; }其对应的字节码文件为:
public com.company.Student(int, java.lang.String, java.lang.String); Code: 0: aload_0 1: iload_1 2: aload_2 3: invokespecial #1 // Method com/company/Person."":(ILjava/lang/String;)V 6: aload_0 7: aload_3 8: putfield #2 // Field sid:Ljava/lang/String; 11: return虽然定义中只显式地定义了传入3个参数,而实际上会隐含传入一个当前对象的引用作为第一个参数,所以四个参数依次为this,age,name,sid。上面的4条指令刚好把这四个参数的值依次入栈,进行参数传递,然后调用了Student.()方法,会创建该方法的栈帧,并入栈。栈帧中的局部变量表的第0到4个slot分别保存着入栈的那四个参数值。创建Studet.()方法的栈帧:
Student.()方法中的字节码指令如下:
aload_0:将局部变量表slot0处的引用值入操作数栈
aload_1:将局部变量表slot1处的int值入操作数栈
aload_2:将局部变量表slot2处的引用值入操作数栈
invokespecial #1:调用Person.()方法,同调用Student.过程类似,创建栈帧,将三个参数的值存放到局部变量表等,并且给堆上的对象赋值
从Person.()返回之后,用于传参的操作数栈的3个值被回收了, Person栈帧的使命完成。
aload_0:将slot0处的引用值入栈,也就是父类构造好的引用。
aload_3:将slot3处的引用值入栈,也就是子类独有的参数sid。
putfield #2:将当前栈顶的值”20210201”赋值给0x2222所引用对象的sid字段,然后两个参数出栈。
return:返回调用方即main()方法,当前方法栈帧出栈,main栈帧上操作数栈上使用的几个slot销毁,只保留了最底部的引用0x222
重新回到main()方法中,继续执行下面的字节码指令:
public static void main(java.lang.String[]); Code: 0: new #7 // class com/company/Student 3: dup 4: bipush 28 6: ldc #8 // String tml 8: ldc #9 // String 20210201 10: invokespecial #10 // Method "":(ILjava/lang/String;Ljava/lang/String;)V 13: astore_1 14: aload_1 15: iconst_5 16: bipush 6 18: invokevirtual #11 // Method study:(II)I 21: pop 22: invokestatic #12 // Method getCnt:()I 25: pop 26: aload_1 27: invokevirtual #13 // Method run:()V 30: returnastore_1:将当前栈顶引用类型的值赋值给slot1处的局部变量,然后出栈。
接下来继续执行main方法的相关指令:
aload_1:slot1处的引用类型的值入栈,也就是要开始使用s对象的方法,所以先将引用入栈iconst_5:将常数5入栈,int型常数只有0-5有对应的iconst_x指令bipush 6:将变量6入栈invokevirtual #11:调用虚方法study(),这个方法是重写的接口中的方法,需要动态分派,所以使用了invokevirtual指令。
构造方法执行完成后,即顺序执行study方法的调用,代码如下:
//实现接口方法 public int study(int a, int b){ int c = 10; int d = 20; return a+b*c-d; }字节码如下:
public int study(int, int); Code: 0: bipush 10 2: istore_3 3: bipush 20 5: istore 4 7: iload_1 8: iload_2 9: iload_3 10: imul 11: iadd 12: iload 4 14: isub 15: ireturn最大栈深度3,局部变量表5
字节码指令执行如下:
bipush 10:将10入栈istore_3:将栈顶的10赋值给slot3处的int局部变量,即c,出栈。bipush 20:将20入栈istore 4:将栈顶的20赋值给slot4处的int局部变量,即d,出栈。上面4条指令,完成对c和d的赋值工作。iload_1、iload_2、iload_3这三条指令将slot1、slot2、slot3这三个局部变量入栈:
imul:将栈顶的两个值出栈,相乘的结果入栈,也就是计算b*c:
iadd:将当前栈顶的两个值出栈,相加的结果入栈,也就是计算a+b*ciload 4:将slot4处的int型的局部变量入栈,也就是将d入栈isub:将栈顶两个值出栈,相减结果入栈,也就是计算a+b*c-d:
ireturn:将当前栈顶的值返回到调用方,到这里为止study方法执行完毕,study栈帧出虚拟机栈,main栈帧上的操作数栈清空,方法继续向下执行。以上study的方法就执行完成了。
重新回到main()方法中,继续执行下面的字节码指令:
public static void main(java.lang.String[]); Code: 0: new #7 // class com/company/Student 3: dup 4: bipush 28 6: ldc #8 // String tml 8: ldc #9 // String 20210201 10: invokespecial #10 // Method "":(ILjava/lang/String;Ljava/lang/String;)V 13: astore_1 14: aload_1 15: iconst_5 16: bipush 6 18: invokevirtual #11 // Method study:(II)I 21: pop 22: invokestatic #12 // Method getCnt:()I 25: pop 26: aload_1 27: invokevirtual #13 // Method run:()V 30: return下面的字节码指令就不再重复画图示意了:
invokestatic #12 调用静态方法getCnt()不需要传任何参数pop:getCnt()方法有返回值,将其出栈aload_1:将slot1处的引用值入栈invokevirtual #13:调用0x2222对象的run()方法,重写自父类的方法,需要动态分派,所以使用invokevirtual指令return:main()返回,程序运行结束。这样整体的main方法也执行完毕,整个程序调用完毕。
总结而言,从一个Java程序被编写,最后一直到创建的对象被垃圾回收,全流程包括以下几步,加粗部分为本系列接下来的blog重点讲解内容:
编辑生成源代码.java文件编译(javac编译和jit编译)生成字节码文件类文件被加载到虚拟机(类Class文件结构,虚拟机运行时内存分析,类加载机制)虚拟机执行二进制字节码(虚拟机字节码执行系统)垃圾回收(JVM垃圾回收机制)虚拟机发挥作用的部分从第3步到第5步之间:
类加载阶段:一个类文件首先加载到方法区,一些符号引用被解析(静态解析)为直接引用或者等到运行时分派(动态绑定),经过一系列的加载过程(class文件的常量池被加载到方法区的运行时常量池,各种其它的静态存储结构被加载为方法区运行时数据解构等等),程序通过Class对象来访问方法区里的各种类型数据字节码执行阶段:当加载完之后,程序发现了main方法,也就是程序入口,那么程序就在栈里创建了一个栈帧,逐行读取方法里的代码所转换为的指令,而这些指令大多已经被解析为直接引用了,那么程序通过持有这些直接引用使用指令去方法区中寻找变量对应的字面量来进行方法操作。JVM垃圾回收阶段:操作完成后方法返回给调用方,该栈帧出栈。内存空间被GC回收,堆里被new的那些也就被垃圾回收机制GC了。以上就是整个Java程序的生命周期。
上一篇:Java后端工程师常见面试题
下一篇:Java程序员面试笔试宝典笔记
皓盘云建最新版下载v9.0 安卓版
53.38MB |商务办公
ris云客移动销售系统最新版下载v1.1.25 安卓手机版
42.71M |商务办公
粤语翻译帮app下载v1.1.1 安卓版
60.01MB |生活服务
人生笔记app官方版下载v1.19.4 安卓版
125.88MB |系统工具
萝卜笔记app下载v1.1.6 安卓版
46.29MB |生活服务
贯联商户端app下载v6.1.8 安卓版
12.54MB |商务办公
jotmo笔记app下载v2.30.0 安卓版
50.06MB |系统工具
鑫钜出行共享汽车app下载v1.5.2
44.7M |生活服务
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-02-15
2022-02-14