Jvm(JAVA虚拟机简述)
Jvm是java语言能够实现跨平台运行的重要机制,jvm是一种虚拟机。那么在学习jvm的时候,如果想要彻底理解虚拟机的运行机制,那么可能需要读者具备一定程度的底层硬件知识。当然,即便完全没有接触过计算机底层,也不妨碍对jvm的学习。
声明,这是一篇偏向于科普向的文章,没有足够的硬核,只是希望能够大致描绘出一个jvm的模型,而不至于学习的时候过于被动,如果其间有错误的部分,还望指出,我也会慢慢学习更加深入。
在这里,为了能够更深层次的学习jvm,在正式开始讲解jvm之前,我会补充一部分的汇编语言和底层相关知识。
本人水平有限,但是总有点喜欢追求程序以及计算机背后运行的本质是什么,所以联系了jvm、虚拟化技术、物理机的运行原理。也许其中有些部分的知识会有谬误,欢迎各位大佬随时指正。
如果有小伙伴想要对底层有更深层次的理解,推荐从王爽的汇编语言入手,在我所接触到的为数不多的基本书籍当中,这一本大概是最适合新手入门的吧。
一、cpu基础知识讲解
CPU是计算机的灵魂所在,在cpu内部有三个东西,他们分别是ALU(算数逻辑单元),寄存器,控制器。(提一嘴,本人天资有限,实在是无法单纯的从抽象角度去理解cpu的本质,所以稍稍学习了一些嵌入式的相关知识,才堪堪理解了cpu到底是怎样的存在,从研究底层开始就疯狂担心自己的发量呜呜呜,但是搞清原理好爽!)
1、ALU
算数逻辑单元,见名知意,就是计算机当中用来实际计算的部分。
2、内存
相信各位小伙伴对这个概念应该不陌生了,简单来说就是存储各类数据的结构。
3、寄存器
ALU只能实现运算,那么在运算的过程中,一定会有指令、操作数这些东西,他们被临时放在寄存器当中。那么有可爱的小伙伴会问了,不是已经有内存了吗,为什么还需要寄存器呢?我一开始也有这种问题,现在说一说我的理解。
在实际的物理机当中,cpu与内存之间通过总线连接,彼此之间有一定程度的距离,距离产生美~跑歪了,那么这一段距离可能会导致运算的速度变慢,所以就在cpu的附近增加了寄存器的存在。在现在的cpu架构当中,为了加快运算速度,还新添了高速缓存,一级缓存等物理结构。
我们只需要知道一个寄存器就好,那就是IP寄存器,又称为指令指针寄存器。代码在计算机当中是一条条存储一条条执行的,在内存中存放代码的内存区会有相对应的地址。IP寄存器就是存储下一条将要执行的代码位置。也就是说,有了IP寄存器,我们的计算机才知道下一步要去哪儿,要干啥。
4、栈
栈在汇编语言当中是一个很重要的概念,回想自己和栈相杀相爱的那些日子,真是破有一段滋味。但是在jvm当中,栈的相关操作都由虚拟机自行完成,而无需程序员自己去管理与栈相关的内存空间,所以这里不多做解说。
5、堆
堆是内存当中一段不连续的地址空间,无论是物理机中的堆,还是jvm当中都是一个重难点。特别 是在java当中,堆的垃圾回收机制。
需要提一嘴的是,尽管物理机和jvm都有堆栈相关的概念,但在细节方面,两者仍有很大的不同。在此介绍这些知识,是希望能对理解jvm有一点帮助。
6、指令集
Cpu当中存放了一堆基本指令,用来完成相对应功能。稍稍提一下,在冯诺依曼计算机体系结构中,计算机底层都是二进制代码,所以,如果cpu将一段代码识别为数据,那么他便是数据,如果cpu识别为程序,那么他便成为了程序。正是由于这一机制的存在,所有便诞生了一系列的安全问题,许多黑客手段就是基于此诞生的,比如栈溢出。
现在,我们就来思考一下为什么jvm和物理机之间会产生这些问题,而jvm又是如何运行起来的。
二、什么是虚拟机
为了搞清jvm背后的运行机制,首先就得搞清楚虚拟机到底是个啥。
在看到这个问题的时候,你的小脑瓜里面想到的是什么呢?让我猜猜,vituralbox?或者vmware?
没错,他俩确实是虚拟机,但只是虚拟机的一种。我们的虚拟化技术分为三种:
1、硬件虚拟机
是在硬件层面抽象出一个主机,可以安装各类型的操作系统,等于虚拟出一套硬件系统。典型代表:VMware,virtualbox
2、操作系统虚拟机
在操作系统层面重新构写其他操作系统的api,实现操作系统之间的兼容。典型代表:wine(使用linux操作系统的小伙伴对此可能有所了解,一会单独解释一下。)
3、代码运行环境虚拟机
提供一个程序运行时的环境,如java,python等语言的运行环境。
首先解释一下wine,众所周知(大概),liunx上的程序是无法直接在windows上运行的。为什么呢?这是因为可执行文件的格式不一样。好奇宝宝开始提问,什么是可执行文件,什么是格式?为什么格式不一样会导致无法运行呢?
这个问题,很遗憾,虽然我能解释,但不是本文的重点(话说我好像半天还没进入jvm,囧)。那么各位小伙伴只需要知道,每一种操作系统对应一种可执行文件格式,也就是二进制文件。Linux的可执行文件没办法在Windows上运行,反过来也是这样,所以就有了wine这一类虚拟机的存在(尽管wine的全称中文翻译是:wine不是一个虚拟机,但是小伙子,你跑不掉这个称号的),他的作用就是兼容不同操作系统,使得各种win的文件能够在Linux上运行。
现在,通过对wine的了解,应该对虚拟化技术有了一定的认知。当然和前期的汇编以及底层知识一样,我们暂时不需要过于深入,只需要有一定的了解能够帮助自己理解就好。
Wine的虚拟化技术实现了在不同平台上运行相同程序的功能,实际上这就是虚拟化技术带给java最强大的功能之一,可移植性高。
Jvm是一个虚拟机,而且是虚拟机当中的docker,也就是容器。容器的最大特点就是轻量,运行速度快。
Jvm通过虚拟化技术,在软件层面模拟出一个计算机系统,他甚至也有自己的一套cpu指令集,甚至有独属于自己的文件格式。只要符合jvm的文件格式,那么就可以在jvm上运行相关程序。
开发人员为了让Java语言具有良好的跨平台能力,Java独具匠心的提供了一种可以在所有平台上都能使用的一种中间代码——字节码。这种中间码使得如今的Java虚拟机甚至能够运行Kotlin、Groovy、JRuby、Jython、Scala等语言。
以上,便是jvm的入门阶段,看到这里,其实最难理解的部分已经结束了。对jvm本质的理解是困难的,本人水平也有限,讲述效果大概无法做到让人一看就懂。但好在,jvm的本质并不需要开发人员去关注,因为Java的设计师已经完全做好了,咱们只需要一边膜拜着大佬,一边努力的学习和使用Java这个工具就好。
Jvm体系结构由三部分组成,类加载、运行时数据区、执行引擎。在本文档中,我们只介绍前两个模块,首先看一看类加载。
三、Java代码执行过程
了解完jvm,下面就看一看Java代码是如何在jvm当中运行的。
Java代码从编译到执行一共分为五步(稍微提一嘴,其实一般代码文件从编写到执行也分为预处理-编译-汇编-链接四个阶段),他们分别是:
1、加载——验证——准备——解析——初始化
验证、准备、解析又统一在连接当中。
首先,Java文件通过javac变成成class文件,这个文件对应的就是字节码,然后由jvm加载字节码,运行时,解释器将字节码解释为一行行的机器码来执行。好奇宝宝提问环节,什么是解释器?虚拟机当中为什么也有机器码?好奇宝宝乖,俺们这篇文章没有那么硬核,咱能大概见名知意就好。(才不承认是因为不知道怎么用文字写出来才不写的呢!嘤嘤嘤,真希望现在还能拥有高中的文学底蕴,不然就不会出现这种“词儿穷”的局面了。)
在程序运行期间,即时编译器会针对热点代码将部分字节码编译成机器码以获得更高的执行效率。简而言之,即时编译器就是一种未卜先知。
正是解释器和即时编译器这俩好哥们的相互配合,使得java程序的运行几乎能够达到编译型语言一样的执行速度。
就和好奇宝宝提出的问题一样,上面两段话当中有相当的技术难点,比如说编译器,是不是有小宝贝不知道啥是编译器啊,没关系,不影响理解,咱们只需要知道这是大佬做的,然后交给我们仰望就可以了。
那么我们能接触到的是哪一部分呢?就是加载字节码的这一个过程,这一个过程被称为类加载。讲真的,作为科普性文章,其实俺也只能讲述到类加载机制这一环节,具体的类加载器的实现,需要深入学习以后,结合源码理解。
闲话不多说,这水字数的习惯也不知道是不是从写小说开始养成的,囧。为什么要有类加载这一个过程呢?这一个过程就是把class文件通过加载生成某种class数据结构进入内存。具体的加载过程就是前文所说的五个步骤,注意,流程当中的加载只是类加载的一步,并不能代表类加载哦。下面我们就对这五个步骤一一解释:
1、加载
加载是读取一个class文件,将其转化为某种静态数据结构存储在方法区,并在堆中生成一个java.lang.Class类的对象的过程(定义来源b站寒食君视频,感谢大佬,膜拜大佬。)这个定义当中的方法区和堆实际上都是堆的一种,在前文略有涉及,后文会再次做出解释。(写这篇文章的时候我也比较迷茫,从jvm体系结构开始讲,就不知道目的是啥,从类加载开始讲,又有些知识面没有涉及。但最终还是选择了这样一个流程,大概上来讲,我本人偏向于由面到点吧。)
2、验证
这一步骤简单点说就是验证class文件是否是安全的、可执行。验证过程会验证文件的格式、元数据以及字节码。当这两方面的验证通过以后,基本上就已经得到了jvm的认可,程序就可以愉快的跑起来啦(误)。其实在这个过程中,还有很多初期无法验证的过程,验证是分散在各个流程的。
3、准备
在基本得到jvm的认可之后,就进入了准备阶段。在这个阶段jvm会为class当中的静态变量赋0值。
4、解析
解析阶段主要做的事情就是将符号引用替换为直接引用。下面解释一下什么是符号引用和直接引用。
符号引用:假设我们此时加载了一个类A,并且在A中引用了类B,A是如何引用B的呢?就可以通过一个标号“s”来标记B的地址,这就是符号引用。当实际运行的时候,发现在B没有被记载,那么就开始加载B,B被加载的时候,拥有了实际的内存地址,那么标号“s”就被替换为B的实际地址,这便是直接引用。这类似于c语言当中的指针实现。
但在实际操作过程中,这里的B可能是一个抽象类或者接口,他有几个不同的实现类,此时并不知道具体的实现类是哪一个,那么就无法知道具体使用哪一个具体类的地址来实现替换。那么就只能等到在运行过程中,发生调用的时候,这时候虚拟机运行栈会得具体的信息,再将这个信息绑定在A中调用,这便是动态解析。
5、初始化
初始化阶段很简单,就是检查有没有需要初始化的资源。
上面的类加载过程基本上就是java代码执行过程的初期。这之后,基本就交给jvm虚拟机自行完成。
其实讲完了类加载机制之后,本应该接着讲解一下类加载器的原理,不过暂时这个模块先放一放,原因见上。
下面我们就看一看第二个模块,运行时数据区。
四、运行时数据区
运行时数据区由两部分组成,线程共享区和线程独享区。线程共享区就是在jvm当中大名鼎鼎的堆和方法区,java的调优主要针对的便是堆。而线程独享则是虚拟机栈、本地方法栈和程序计数器。
1、方法区
方法区是所有线程共享的内存区域,它用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
2、堆
java堆是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。
java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
3、 程序计数器
这个玩意简单的解释一下,其实就是在咱们的IP寄存器,告诉jvm下一步要去哪儿,执行啥。但程序计数器更大的作用是在多线程的时候,具体表现为多线程执行时不让程序出现混乱。就这两局话着实让人看的一脸懵逼,篇幅有限,见谅见谅。(其实我也不介意你们寄刀片给我,毕竟我在这篇文章里挖了太多的坑了,而且都还没填,嗯!很好的保存了写小说的习惯。所以我现在想着,要不干脆卖刀片发家算了~)
4、虚拟机栈
Java虚拟机栈本质上相当于文章开头介绍的栈,是用于函数调用时,临时开辟出来的内存空间,栈中存放局部变量,参数,返回值。栈具有先进后出的特点,先入栈的最后取出。栈是一个不太好理解的存在,建议从8086汇编开始理解栈,我也是在写汇编程序的过程中才真正理解了一点栈。好在jvm当中,并不需要我们程序员去关注栈的内容。
5、本地方法栈
Java语言在编写过程使用了c和c++代码,由c和c++代码实现的函数(java中叫做方法)就是本地方法,调用这一部分方法时,使用独特的栈,就叫做本地方法栈。
到此为止,类加载模块和运行时数据区就基本介绍完毕,此后还有一个重难点,垃圾回收机制。这一方面的内容,更加推荐选择一些视频系统的学习,包括类加载器的部分。视屏讲解会比直接的文字描述更加生动易懂。
首次编写略带一点硬核的文章,可能有点大而杂,希望自己以后能够完善这方面的能力,也希望能够有一点点帮助到陌生人对jvm的理解,感谢观看!