浅谈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的内存模型又是怎样的呢?大致可以理解为以下图中所示。

  • PC寄存器

它可以看作是当前线程所执行的字节码的行数指示器,解释执行字节码时,也要依赖它来选取下一条执行的字节码,分支、循环、异常处理等都需要依赖它来完成。

由于JVM中,多线程是通过线程轮流切换并分配处理器执行的方式实现。因此,在任意时刻,一个处理器(如果是多核处理器则为一个内核)只会处理一个线程的指令。

为了保证线程在切换完后能恢复到执行的位置,每个线程都需要一个计数器,且每个线程间的计数器互不影响,独立存储。这类内存区域,称为“线程私有”内存。

 

  • Java方法栈

与PC寄存器一样,Java虚拟机栈也是线程私有的,其描述为Java方法执行时,会对应创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,就对应着一个栈帧入栈到出栈的过程。

局部变量中存储了各种基本数据类型、引用数据类型和返回类型,其中long和double类型数据会占用2个局部变量空间(单位 Slot),其它的都只占用1个Slot。局部变量空间的分配在编译器时完成,当进入一个方法时,栈帧的所需分配的局部变量空间是完全确定的,方法运行期间也不会改变其大小。

  • 本地方法栈

本地方法栈与Java虚拟机栈是很相似的,只不过后者是为Java方法(即字节码)服务,前者则为Native方法服务。

  • Java堆

Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建,其目的就是为了存放对象实例、数组。堆可以细分为新生代、老年代,再细可分为:Eden、From Survivor、To Survivor。这样方便虚拟机进行GC操作。Java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。

  • 方法区

方法区和Java堆一样,是所有线程共享的内存区域,用于存储类加载完的类信息、常量、静态变量、即时编译后的代码等数据。同时它与堆是区分开来的。在HotSpot虚拟机中,选择把GC分代收集扩展至方法区,或者说使用“永久代”来实现了方法区,这样GC可以像Java一样管理这部分内存,可以省去专门为方法区编写内存管理代码的工作。因此,方法区并不等价于永久代,至少在除了HotSpot之外的虚拟机不等价。

方法区同样也不需要物理上连续的内存,并且在这个内存区域,GC行为也相对较少(并非没有),gc的目标主要是针对常量池的回收和对类型的卸载。

 

对象内存结构

对象的结构可分为:对象头、实例数据。

对象头可分为两部分,第一份包括如哈希码(HashCode)、GC分代年龄、锁状态标识、线程锁等等。第二部分为类型指针,即对象指向它的类元数据的指针,虚拟机可通过该指针确定这个对象是哪个类的实例。并不是所有虚拟机实现都必须在对象头中保留类型指针。

实例数据则为对象存储的数据信息(可理解为成员属性)。

 

对象的访问定位

reference(引用)数据类型在虚拟机中访问对象实例有两种主流方式:1.使用句柄,2.使用直接指针。第一种会在堆中创建一个句柄池,每个reference地址会对应一个句柄地址,句柄中包含了两个指针,一个指向实例对象,一个指向对象类型数据。第二种reference地址直接指向实例对象,该对象拥有对象的类型数据指针。HotSpot使用的是后者。

参考

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值