方法区跟堆一样是线程共享的区域,当.class字节码文件在JVM加载时会被分配到不同的数据结构,如常量池、方法、构造函数,同时也主要包括用来存储已被虚拟机加载的类相关信息(类信息又包括了类的版本、字段、方法、接口和父类等信息)都存放在方法区。
程序计数器我们知道Java程序是多线程执行的,所以即想要能满足多个线程的交叉执行,又想要确保多个线程都能完整的执行完各自的工作,那么一旦出现被中断的线程,线程执行到哪条的内存地址(指令)就必须被保存下来,这样当被中断的线程恢复时就又可以接着执行下去。
而这就是程序计数器的工作,用来记录哪个线程当前执行到哪条指令。所以分支、循环、跳转、异常处理、线程恢复等都需要依赖计数器完成。(形象的说就是程序控制流的指示器)
虚拟机栈虚拟机栈是线程私有的区域,所以不用关心数据一致性问题。当我们创建一个线程时,同样在JVM中也会创建一个与之对应的栈,称为虚拟机栈。
而虚拟机栈的内部其实是一个或多个的栈帧,每一个栈帧又都对应着一个Java方法的调用。
其运行过程是,当我们创建一个新方法同时,与之对应会在虚拟机栈中同样创建一个新的栈帧(当前栈帧)会被放在栈顶(只要是栈就会有栈顶和栈底),同时程序计数器也会指向这个当前栈地址。如下图所示:
在每个栈帧里又存储着方法的 局部变量表、操作数栈、动态链接、方法返回地址、附加信息 参与着方法的调用和返回。
所以 如果说堆解决了数据存储的问题,那么栈就是解决了程序如何运行的问题。
本地方法栈本地方法栈是为了运行JVM本地方法(也就是 Native 方法)而准备的空间。而从字面上之所以被称为本地方法栈,也是因为 Natice 方法很多也都是由C语言所实现的。
如果说 “类加载子系统是计算机结构中的输入设备,那么运行时数据区就是计算机结构中的CPU(控制器和运算器)和存储器,那么最后的输出设备就是执行引擎。”
执行引擎到了执行引擎,可以说是JVM最后的一个环节,从最先的一个Java源文件编译成.class字节码文件经过了类加载子系统,也经过了上面的JVM运行时数据区,经过了这一整系列下来,通俗的说字节码文件已经被重新打碎*成了一个可以由JVM所操控的一系列数据(再回看计算机结构,数据成流,被布好的局安排的明明白白),但是问题是代码并不能被执行。因为JVM并不是将高级语言直接转成机器指令,而是字节码,所以字节码的真正的运行得由执行引擎去将字节码翻译成机器指令后,由真正物理机去执行。
但是我们知道JVM只是对计算机的抽象,它的一切都只是建立在软件层面自行实现的。而物理计算机只认识机器码指令,这些机器码指令运行通过处理器、缓存、指令集和操作系统等构建了物理机的执行引擎。
所以JVM想要让Java程序运行起来,同样也是要在 “虚拟(通俗说是模拟)一个执行引擎”,而执行引擎的目的就是 将字节码指令解释(编译)为机器指令。
因为回到本质,真正干活做事的是物理机,所以执行引擎就是将字节码转成为物理机可执行的机器码(从用户看执行引擎就是一台翻译机)
我们使用的HotSpot VM是目前虚拟机的代表之一,它是集解释器和JIT即时编译器于一身 的架构。也就说Java虚拟机运行时,解释器和JIT(just in time 即时编译器) 互相协作。
解释器在JVM早期使用的就是解释器(大多数语言同样也是),解释器就是在运行时逐行解释字节码转化成机器码再执行程序。(这也解释了上面所说,JVM为什么不直接将Java语言直接转化为机器指码指令直接就能在物理机上执行。而是要通过加多一层字节码文件来具备通用性,所以以这种 “加一个翻译器”的方式,来避免高级语言直接转成本地机器指令的耦合,重要的事情不要忘了,JVM虚拟机是一个概念,也不要忘了目的是具备通用性)
在Java发展路程中,从最早期的,也是最古老的字节码解释器。之后到了目前普遍使用的模板解释器。一共有两套解释执行器。字节码解释器是在执行时通过纯软件代码模拟字节码的执行,所以效率也非常低。而模板解释器是将每一条字节码和模板函数相关联,而模板函数能直接产生这条字节码执行时的机器码,从而达到提高解释器性能。
但是单凭字节码解释器效率还远不够,所以 为了追求一把即时速度的推背感,虚拟机又加上了JIT也就是即时编译器。
JIT 即时编译器从上面我们知道在JVM执行引擎拥有字节码解释器之后又加入了JIT,而使用JIT对字节码转化为机器码指令时,关注的核心一点就是 程序中运行时被调用频繁的代码,被称为 “热点代码”。
而要找到这些 “热点代码”就需要使用到JIT的 热点探测。目前的Host Spot 的JVM采用的热点探测是基于 计数器热点探测。计数器热点探测很好理解,就是统计每个方法执行次数,当超过认为的热点阈值,那么就属于“热点代码”。
计数器热点探测被分为 方法调用计数器 和 回边计数器 两类。方法调用计数器用来统计代码调用次数,而回边计数器则用来统计循环执行次数。
方法调用的计数器除了递增,也同样有热度衰减,也就是当代码调用次数超过一定时间已经不足提交给JIT,那么调用计数器会递减。
而回边计数器的主要目的是为了触发 OSR (On StackReplacement)栈上编译。在一些循环周期较长代码会在循环时间内,会直接将代码替换执行缓存机器码。
本地方法接口与本地方法库在JVM中如果需要与一些底层系统实现交互,那么就会使用到本地方法接口与本地方法库,也就是 Native Method,本地方法接口与本地方法库其目很简单,就是借用到C或C 等其他语言的资源。
总结我们从冯诺依曼计算机体系理解了计算机结构思想 程序应该像数据一样可以被存储,也就是说程序就像数据(或者指令)一样,只要经过像组件构建的流就可以被牢牢操控。而接着探究操作系统本质其实就是将物理资源虚拟化,可以说是用户与物理资源之间的桥梁。最后追溯到JVM其实就是在一台虚拟(抽象)的计算机,如果类加载子系统是计算机结构中的输入设备,那么运行时数据区就是计算机结构中的CPU(控制器和运算器)和存储器,那么到最终的输出设备就是执行引擎。
到此,JVM还有类加载的双亲委派机制,以及JVM的垃圾回收机制、JVM的堆栈等异常以及JVM配置参数等内容没有聊,因为这些内容都值得再独立拎出来细说。所以这一篇文章可以当成JVM的一个开篇,也可以说是JVM的一张地图。地图作用不是告诉你应该去哪的路标,而是能纵览整个全貌后,至于你对这整个知识是想怎么理解的都取决于的是你自己的思考。
(最后,我们至今还没有非冯·诺依曼体系下的新计算机结构,但不妨大胆设想未来,也许会在距实用还相去甚远的量子计算机上看到呢。)