java虚拟机JVM--java虚拟机的结构

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq475703980/article/details/79954438

学Java的朋友, 相信都听过一句话:java语言是跨平台的。那java是怎么跨平台的呢, 靠的就是JVM(Java Virtual Machine)java虚拟机。java编译以后会生成class字节码文件, 然后字节码文件运行在JVM上, 然后JVM就把class字节码文件转成机器指令, 可以在不同的平台上运行了。

这里要注意的是, 跨平台的是java语言, 而不是JVM,不同平台上JVM的实现是不同的, 但是他们的整体结构是相同的。了解一下JVM的结构, 有利于我们更深入的理解java。

JVM 结构图

首先来看一下java虚拟机的结构图:

可以看出, JVM主要组成有:类加载器子系统、运行时数据区、执行引擎以及本地方法接口等。

一、ClassLoader类加载器

负责把存储设备上的class文件加载到JVM的运行时数据区,使JVM可以实例化或以其它方式使用加载后的类。JVM的类加载子系统支持在运行时的动态加载

二、Runtime Data Area–运行时数据区

我们通常所说的堆栈堆栈就在这里,运行时数据区由方法区、堆、Java栈、PC寄存器、本地方法栈w五部分组成, 结构图如下:

图中橙色部分是线程私有的,其他线程补课访问, 而蓝色部分(堆区和方法区)是整个进程共享的, 所有线程均可以访问。其中JDK1.7以前(不含1.7),常量池在方法区中, JDK1.7及以后常量池移到了堆区。下面对这五部分一一介绍

2.1 java虚拟机栈(Java Virtual Mathion Stack)

jvm栈, 有的也称为java栈,主要任务是存储方法参数、局部变量、中间运算结果, 并提供部分其他模块工作需要的数据。

JVM栈是私有的, 生命周期和线程一致,每当创建线程就创建JVM栈。每个JVM栈中会包含多个栈帧(Stack Frame), 每当调用一个方法,就创建一个一个栈帧, 每个栈帧回包含调用的这个方法的局部变量、操作栈、方法返回值等信息。在jvm栈栈顶的栈帧就是当前正在执行的活动栈帧, PC寄存器会指向该内存地址。如果在这个方法内调用了另一个方法, 又会创建一个新的栈帧,那这个新栈帧会被放到jvm栈 栈顶, 变成当前的活动栈帧, PC寄存器也重新会指向这里。当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧,然后继续执行下一个栈帧。

在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常

2.2 本地方法栈(Native Method Stack)

本地方法栈类似于Java栈,主要存储了本地方法调用的状态。区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务。本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。有一些虚拟机(如HotSpot, Sun JDK中的)将Java虚拟机栈和本地方法栈合并,所以其本地方法栈和Java栈是同一个。

2.3 PC寄存器(Program Counter Register)

严格来说是一个数据结构,用于保存当前正在执行的程序的内存地址,由于Java是支持多线程执行的,所以程序执行的轨迹不可能一直都是线性执行。当有多个线程交叉执行时,被中断的线程的程序当前执行到哪条内存地址必然要保存下来,以便用于被中断的线程恢复执行时再按照被中断时的指令地址继续执行下去。为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存,这在某种程度上有点类似于“ThreadLocal”,是线程安全的。

2.4 方法区(Method Area)

类型信息和类的静态变量都存储在方法区中。方法区中对于每个类存储了以下数据:

  • 类及其父类的全限定名(java.lang.Object没有父类)
  • 类的类型(Class or Interface)
  • 访问修饰符(public, static, abstract, final)
  • 实现的接口的全限定名的列表
  • 常量池
  • 字段信息
  • 方法信息
  • 静态变量
  • ClassLoader引用
  • Class引用

    可见类的所有信息都存储在方法区中。由于方法区是所有线程共享的,所以必须保证线程安全,举例来说,如果两个类同时要加载一个尚未被加载的类,那么一个类会请求它的ClassLoader去加载需要的类,另一个类只能等待而不会重复加载。

2.5 堆区(Heap)

堆是JVM所管理的内存中最大的一块,是被所有Java线程锁共享的,不是线程安全的,在JVM启动时创建。我们通常说的GC回收内存也是回收这一块的内存。

Java虚拟机规范中这样描述:所有的对象实例以及数组都要在堆上分配。堆中有指向类数据的指针,该指针指向了方法区中对应的类型信息。堆中还可能存放了指向方法表的指针。堆是所有线程共享的,所以在进行实例化对象等操作时,需要解决同步问题。此外,堆中的实例数据中还包含了对象锁,并且针对不同的垃圾收集策略,可能存放了引用计数或清扫标记等数据。

三、执行引擎

执行引擎是JVM执行Java字节码的核心,执行方式主要分为解释执行、编译执行、自适应优化执行、硬件芯片执行方式。

JVM的指令集是基于栈而非寄存器的,这样做的好处在于可以使指令尽可能紧凑,便于快速地在网络上传输(别忘了Java最初就是为网络设计的),同时也很容易适应通用寄存器较少的平台,并且有利于代码优化,由于Java栈和PC寄存器是线程私有的,线程之间无法互相干涉彼此的栈。每个线程拥有独立的JVM执行引擎实例。

JVM指令由单字节操作码和若干操作数组成。对于需要操作数的指令,通常是先把操作数压入操作数栈,即使是对局部变量赋值,也会先入栈再赋值。注意这里是“通常”情况,之后会讲到由于优化导致的例外。

4.1 解释执行

和一些动态语言类似,JVM可以解释执行字节码。Sun JDK采用了token-threading的方式,感兴趣的同学可以深入了解一下。解释执行中有几种优化方式:

栈顶缓存:将位于操作数栈顶的值直接缓存在寄存器上,对于大部分只需要一个操作数的指令而言,就无需再入栈,可以直接在寄存器上进行计算,结果压入操作数站。这样便减少了寄存器和内存的交换开销。

部分栈帧共享:被调用方法可将调用方法栈帧中的操作数栈作为自己的局部变量区,这样在获取方法参数时减少了复制参数的开销。
执行机器指令:在一些特殊情况下,JVM会执行机器指令以提高速度。

4.2 编译执行

为了提升执行速度,Sun JDK提供了将字节码编译为机器指令的支持,主要利用了JIT(Just-In-Time)编译器在运行时进行编译,它会在第一次执行时编译字节码为机器码并缓存,之后就可以重复利用。Oracle JRockit采用的是完全的编译执行。

4.3 自适应优化执行

自适应优化执行的思想是程序中10%~20%的代码占据了80%~90%的执行时间,所以通过将那少部分代码编译为优化过的机器码就可以大大提升执行效率。自适应优化的典型代表是Sun的Hotspot VM,正如其名,JVM会监测代码的执行情况,当判断特定方法是瓶颈或热点时,将会启动一个后台线程,把该方法的字节码编译为极度优化的、静态链接的C++代码。当方法不再是热区时,则会取消编译过的代码,重新进行解释执行。

自适应优化不仅通过利用小部分的编译时间获得大部分的效率提升,而且由于在执行过程中时刻监测,对内联代码等优化也起到了很大的作用。由于面向对象的多态性,一个方法可能对应了很多种不同实现,自适应优化就可以通过监测只内联那些用到的代码,大大减少了内联函数的大小。

Sun JDK在编译上采用了两种模式:Client和Server模式。前者较为轻量级,占用内存较少。后者的优化程序更高,占用内存更多。

在Server模式中会进行对象的逃逸分析,即方法中的对象是否会在方法外使用,如果被其它方法使用了,则该对象是逃逸的。对于非逃逸对象,JVM会在栈上直接分配对象(所以对象不一定是在堆上分配的),线程获取对象会更加快速,同时当方法返回时,由于栈帧被抛弃,也有利于对象的垃圾收集。Server模式还会通过分析去除一些不必要的同步,感兴趣的同学可以研究一下Sun JDK 6引入的Biased Locking机制。

此外,执行引擎也必须保证线程安全性,因而JMM(Java Memory Model)也是由执行引擎确保的。

java虚拟机的结构就讲完了, 后面将继续分析java虚拟机的内存管理和垃圾回收、垃圾回收机制、类加载器等。

喜欢的朋友, 麻烦点个赞吧~~

阅读更多

没有更多推荐了,返回首页