本文已参与「新人创作礼」活动,一起开启掘金创作之路。
一. JVM的提出(为什么需要JVM)?
首先,我们看下下面这段简短的代码?
/**
* @Auther: limingwu
* @Date: 2021/2/23 11:14
* @Description:
*/
public class App {
public int add() {
int a = 1;
int b = 2;
int c = (a + b) * 100;
return c;
}
public static void main(String[] args) {
App app = new App();
int result = app.add();
System.out.println(result);
}
}
这里创建了一个简单的App类,App类里面有两个方法,主要是通过一个main方法去创建对象调用add方法,这个add方法计算完成之后去输出一个300。就这个简单的运算也是有着一系列复杂过编译、解释、执行过程的,而这一切主要是通过JVM来完成的,下面我们可以对这个过程进行一个总结。
首先我们编写了一个类名为App的java文件通过javac 的命令转化成了App.class文件,通过JVM对这个这个字节码文件解释、执行我们可以在windows上运行,也可以在linux上面运行。大家也都知道JVM是可以跨平台的,那它是怎么样进行跨平台的,这个时候我要强调一个东西,比如我们在windows上面打开一个文件可能生成的文件句柄字节码为1010,而linux可能是110(随便打个比方),这些指令是不一样的,那我们jvm为了区分这一块,它肯定是要去屏蔽这些底层这些系统给它带来这些差异性的指令,我们要意识到一点就像我们去下载jdk的时候我们针对不同系统要下载不同版本的jdk,我们针对不同的系统下载不同的jdk就是jvm去屏蔽不同指令的手段。所以JVM的存在是为了更好的帮助Java在不同的平台运行。
二. 什么是JVM?
Java语言里负责解释执行字节码文件的是Java虚拟机,即JVM(Java Virtual Machine)。 JVM是可运行Java字节码文件的虚拟计算机。所有平台上的JVM向编译器提供相同的编程接口,而编译器只需要面向虚拟机,生成虚拟机能理解的代码,然后由虚拟机来解释执行。在一些虚拟机的实现中,还会将虚拟机代码转换成特定系统的机器码执行,从而提高执行效率。
当使用Java编译器编译Java程序时,生成的是与平台无关的字节码,这些字节码不面向任何具体平台,只面向JVM。不同平台上的JVM都是不同的,但它们都提供了相同的接口。JVM是Java程序跨平台的关键部分,只要为不同平台实现了相应的虚拟机,编译后的Java字节码就可以在该平台上运行。显然,相同的字节码程序需要在不同的平台上运行,这几乎是“不可能的”,只有通过中间的转换器才可以实现,JVM就是这个转换器。
JVM是一个抽象的计算机,和实际的计算机一样,它具有指令集并使用不同的存储区域。它负责执行指令,还要管理数据、内存和寄存器。
三. JVM的组成部分
JVM主要由以下三个部分组成,他们分别是类加载子系统、执行引擎、JVM运行时数据区。
也就是说我们上面的这个App.class主要是通过类加载子系统去加载,类加载之后给到运行时数据区,那我们的运行时数据区具体是怎么样去工作的呢?那我们再次运行App.java这个实例,我们通过debug模式去运行这个程序的时候肯定是有一个线程去运行这个代码,我们通过debug的快照可以看到线程的状态以及名字。
"main@1" prio=5 tid=0x1 nid=NA runnable
java.lang.Thread.State: RUNNABLE
at com.awu.jvm.demo.App.main(App.java:17)
下图就是debug的内容,这里面可以看到一个main的主线程,当这个main线程去运行的时候,他就会包含虚拟机栈和本地方法栈以及程序计数器(就是图上黄色的这三块。它们属于线程私有,绿色的代表线程共享),线程一被创建他就有这三块空间,这个线程如何去执行呢,这里就要用到执行引擎去执行加载的字节码指令,包括去分配相应的一些空间。这个线程去运行的代码无非就是一个main方法和一个add方法,这时执行引擎会先在虚拟机栈中(关于栈这个数据结构的东西,不懂的同学可有去看下数据结构,这里就不多赘述了)去装载mian方法和add方法,也就是要分配俩块空间给到这个虚拟机栈,划分的结果是会形成栈祯,因为我们是有俩个方法所以会形成一个main栈祯和一个add栈祯,程序运行时先执行main方法,所以main栈祯先被压到虚拟机栈中,之后执行到add方法的时候再把add栈祯压到虚拟机栈中,这时我还要提到一点就是每个栈祯中都包含这些数据结构(看图说话)。
四. JVM如何解析Class文件
我们前面了解了执行引擎的知识之后,再思考一下,我们线程执行代码其实就是运行的class文件,那这个class打开之后究竟是怎样的呢,打开之后一脸蒙蔽。
这个很正常,因为我们是java程序员并不是class程序员,这个时候我们可以通过jdk的javap -c App.class > App.txt去汇编这个class文件,同学们可以动手试一下,以下是我反汇编出来的App.class文件内容。
那我们现在就来分析这个汇编出来的class文件究竟和我们的源java文件有什么样的联系呢?在这之前可以先简单了解下JVM指令集,更多指令集详情请自行百度。
对JVM指令集有过了解以后,我们再次去看我们代码中的add方法就比较容易理解了。
我们来和我们的java文件对比分析,我们首先从第一行 0: iconst_1说起, iconst_1的意思就是将int类型常量1压入栈中,那我们去Java文件中找对应的常量1就是就是对应的add方法中的“1”(也就是a=1 的1),这个时候1先会进入栈祯中的操作数栈中,之后第二行的1: istore_1就是将int类型的值存入局部变量1也就是“a”,这个的意思就是将a存入栈祯中的局部变量表,之前第一条提到的1会出栈。出到1所对应的局部变量表里这样一个操作的结果就是a=1;就这样一个运算。那之后的指令我们也可以得出b=2的结果。以上就是一个局部变量表和操作数栈的过程。好我们继续接着上面。当这些变量运算完成之后return的结果300会通过栈祯中的方法出口找到main方法。(看图说话)
讲到现在线程私有数据结构我们还有程序计数器与本地方法栈没有讲,程序计数器它有这样一句话,官方讲的是:指向当前线程所执行的字节码指令的(地址)行号,这个的意思就是告诉执行引擎去执行行号对应的执行指令! 而本地方法栈是JVM用来专门处理用native修饰的方法,它的底层与虚拟机栈是一模一样的。
那我们现在是不是还有一个堆以及一个方法区(元空间) 这两个还没了解,我们现在将注意力定位到main方法,这个方法里面有两个变量,除了一个result变量,是不是还有一个app变量,这个app变量和result变量相比较还是有点差异的,因为这个app变量指向的是一个对象(App app = new App();)。大家都知道JVM里面的对象是在堆里面创建的,那也就是说我们现在有一个变量,在java里面叫做“引用”,而在C++里面叫做指针,这个变量app对堆里的对象进行了引用。那这个时候我们可以讲到一个点,之前很多人堆和栈都分不清,到底是我们的堆指向栈还是栈指向堆,那么现在我们可以知道了,是我们的虚拟机栈指向堆,也就是栈指向堆。(看图说话)
现在我们知道了对象是在堆里创建的,那么方法区(元空间) 这块是怎么做的呢?我们都知道一个类一般都用publick修饰,通常这个类里面有属性,有方法,这些信息都存储在这个字节码文件里面,而这个字节码又存储在方法区(元空间) ,也就是说这个类对象创建的时候会有一个类地址指向方法区(元空间)里面的类信息。(看图说话)
五. JVM里堆的结构
前面花了大量篇幅讲解栈,其实JVM里面得堆也很重要,我们经常说的性能调优,基本就是对JVM里面堆配置的一个调优。前面花了大量篇幅讲解栈,其实JVM里面得堆也很重要,我们经常说的性能调优,基本就是对JVM里面堆配置的一个调优。平常工作中我们会创建大量的对象,这些对象都会存储在堆里面。那么问题来了,这些对象如何在堆里创建的呢?接下来我们重点讲解下堆,讲完你就明白对象如何在堆里创建了。堆在JVM里面主要分为两块区域,一块是新生代,一块是老年代,而新生代又分为Eden、from、to区。 我们经常说的JVM调优和GC调优其实都是发生在JVM上面的。
六. JVM的垃圾回收(GC)
说到这里,引入阿里的一个面试题,Java为什么要垃圾回收?打个比方,我们知道代码都是运行电脑上的,而电脑的组成是CPU和硬盘等组成的,而软件是运行在硬件上的,是需要分配物理内存的。假设我们现在有一个网站的代码,运行在物理内存总共8G的机器上,当然这台机器上的JVM也分配了一定的内存。随着软件的网站的用户增多,用户相关信息也随着增多,JVM里面堆里面的对象也随着增多,那么此时的JVM就会增大。如果这时任由JVM继续增大而不管的话,那么JVM就会随着时间所占内存慢慢达到8G,最后撑爆内存,系统崩溃。所以为了避免这种情况,JVM会在内存达到临界点的时候,触发垃圾回收(GC) 主动回收部分内存,当再次内存达到临界点的时候,又会触发垃圾回收(GC) 回收部分内存。(看图说话)
到此,深入剖析JVM内存模型已讲完,觉得有帮助的给个赞吧,谢谢😄。