浅谈JVM

作为一名java工程师,对于JVM即Java虚拟机还是很有必要作一定的了解,无论是出于解决问题的角度,还是为了性能需要。

接下来,总结一下个人对于JVM的一些理解。

谈到JVM,首先想到的就是JDK,他们的关系到底如何呢?

JDK,java开发工具包,我们知道要运行java代码必须要先安装个jdk,这是为什么呢?因为java代码需要一个运行环境即JRE,而JRE又由JVM和一些java的核心类库组成。JDK中除了JRE还包括一些其它工具,如javac编译器、javadoc文档生成器等。

JDK的常用版本:

jdk1.5:加入自动拆/装箱、反射、foreach循环、泛型、枚举等功能。

jdk1.6:商业稳定版本,实现了XML映射、动态编译、HTTP协议的支持等。

jdk1.7:升级了swich语法兼容String类型,增强了collections集合功能等。

jdk1.8:丰富了函数功能,如stream,增加了Lambda表达式等。

java界盛传着一个口号:“Compile Once,Run Anywhere”,意思是:“一次编译,到处运行”。即.java文件被编译成字节码.class文件后,可以在任意其它虚拟机上运行,没错这个到处运行的就是这些字节码文件。并且我们常听说java是跨平台,为什么呢?

其实通过javac编译后的java字节码,并不是能直接执行的机器码,而JVM能将字节码解释为对应的机器码,最终得以执行。JVM在这封装了很多操作系统底层的细节,它作为一个中间层实现了应用与操作系统之间的解耦。因此对于java用户而言,并不需要考虑不同操作系统之间的差异性,只需要考虑装个jdk就行了!

那么问题来了,我们的java代码运行时,前前后后都经历了些什么呢?

类加载

运行时,会启动一个线程实例化一个JVM,并在内存中开辟一个内存空间。此时JVM通过类装载器(Class Loader)去加载字节码文件,即我们通常说的类加载过程。类加载包括4个步骤:

加载(定位文件位置)、

验证(验证是否符合JVM规范与安全)、

链接(根据字节码创建类合并到虚拟机内存中)、

初始化(为静态常量值赋值,以及执行静态块代码)。

解释/编译执行

类加载完毕后,将交由解释器(Interpreter)解释执行,或即时编译器JIT(Just In Time Compiler)编译执行。前者临时将字节码翻译成本地机器码,效率相对较慢,但节省磁盘和内存空间。后者直接将字节码编译成了机器码,注意是编译成而不是解释。该方式效率高,但内耗相对较高。你可能会问,那虚拟机是怎么判断什么时候用哪种编译方式呢?其实虚拟机默认的处理方式都是解释执行的,不过在处理前JVM会判断该方法是否属于"热点代码",如果是的话则进行编译执行,否则解释执行。

什么是“热点代码”?JVM又是怎么将代码标识为“热点代码”的呢?

不同的类型的JVM采取的方式可能不一样,这里主要有两种方式:

(1)基于采样的热点探测
虚拟机会周期性地检查各个线程的栈顶,如果发现某些方法经常出现在栈顶,那这个方法就是“热点方法”。

(2)基于计数器的热点探测

每个方法准备了两个计数器:方法调用计数器回边计数器方法每解释一次对应的值就会+1,当这个值达到某个阈(yu 四声)值时就会将该方法标识为"热点代码"。

内存模型

说了这么多,那么JVM的内存模型又是怎样的呢?大致可以理解为以下图中所示。

浅谈JVM

JVM开辟的内存空间分为栈(Stack)、堆(Heap)和方法区,这里的方法区/元空间通常也被称为永久区,用来存放类加载创建的类结构,如常量及方法表。

通常我们在new出一个对象时,这时候JVM会进行类加载,加载过程如果未找到目标class文件则会抛出我们常看到的异常NoClassDefFoundError,并在新生代创建对象,并将其引用一直存在java栈中。当执行某个方法时,会在java方法栈中创建对应的栈帧,把方法中声明的一些变量以及需要用到的引用压入栈中,依次按命令执行。具体细节我们可以参考:

https://blog.****.net/tellh/article/details/77370223

 

 

参考

https://blog.****.net/sunxianghuang/article/details/52094859

https://www.cnblogs.com/xuningchuanblogs/p/7688332.html