JVM学习整理资料

JVM是什么

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

了解JRE、JVM、JDK三者的关系

在这里插入图片描述
JDK(Java Development Kit)
JDK是Java开发工具包,是Sun Microsystems针对Java开发员的产品。JDK中包含JRE,在JDK的安装目录下有一个名为jre的目录.

Java Runtime Environment(JRE)
是运行基于Java语言编写的程序所不可缺少的运行环境。也是通过它,Java的开发者才得以将自己开发的程序发布到用户手中,让用户使用。JRE中包含了Java virtual machine(JVM),runtime class libraries和Java application launcher,这些是运行Java程序的必要组件。

JVM(java virtual machine)
就是我们常说的java虚拟机,它是整个java实现跨平台的最核心的部分,所有的java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行。

JVM、Hotspot和JRockit的关系

JVM是《JVM虚拟机规范》中提出来的规范。

JRockit JVM和Hotspot JVM就是指的是JVM的一个具体实现,由不同的实现商提供。JRockit JVM是BEA开发的,而HotSpot JVM是由Sun开发的;两者在遵循JVM抽象说明书的基础上实现时有所不同有所侧重。如今这两者都已经被Oracle收购,Oracle在开发新的JVM的时候策略是竟可能将JRockit的一些好的特征整合到HotSpot中去

JVM的运行模式

JVM有两种运行模式:Server模式与Client模式。
两种模式的区别在于:
Client模式启动速度较快,Server模式启动较慢;
但是启动进入稳定期长期运行之后Server模式的程序运行速度比Client要快很多。
因为Server模式启动的JVM采用的是重量级的虚拟机,对程序采用了更多的优化;而Client模式
启动的JVM采用的是轻量级的虚拟机。所以Server启动慢,但稳定后速度比Client远远要快。

JVM架构理解

在这里插入图片描述

类装载器(ClassLoader)

主要负责加载class文件,是否能执行主要取决于execution engine它是负责执行被加载类中包含的指令。有两种类加载器分别为启动类加载器和用户自定义类加载器,然而启动类加载器是JVM实现的一部分,用户自定义类加载器是Java程序一部分。

运行时数据区

Java虚拟机在执行Java程序的过程中会把它管理的内存分为若干个不同的数据区域。这些区域有着各自的用途,一级创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范》中规定,jvm所管理的内存大致包括以下几个运行时数据区域,如图所示:
在这里插入图片描述

方法区

方法区存储已经被虚拟机加载的类信息、常量、 静态变量、即时编译器编译后的代码等等。
在这里插入图片描述
jdk1.7之前,HotSpot虚拟机对于方法区的实现称之为“永久代”, Permanent Generation 。
jdk1.8之后,HotSpot虚拟机对于方法区的实现称之为“元空间”, Meta Space 。
方法区是Java虚拟机规范中的定义,是一种规范,而永久代和元空间是 HotSpot 虚拟机不同版本的 两种实现。

Java堆

对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。

Java虚拟机规范的描是:

所有的对象实例以及数组都要在堆上分配。

不过随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

JVM 的内存模型:
在这里插入图片描述

程序计数器

程序计数器(Program Counter Register),也叫PC寄存器,是一块较小的内存空间,它可以看作是当前线程所执行的字节码指令的行号指示器。字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支,循环,跳转,异常处理,线程回复等都需要依赖这个计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(针对多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果一个线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;

如果正在执行的是Native方法,这个计数器则为空(undefined)。

此内存区域是唯一一个在Java的虚拟机规范中没有规定任何OutOfMemoryError异常情况的区域。

Java虚拟机栈

Java虚拟机栈也是线程私有,生命周期和线程相同,虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(stack frame)。

在这里插入图片描述

  • 栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量、操作数栈、动态链接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。

一个线程中方法的调用链可能回很长,很多方法都同时处于执行状态。对于JVM执行引擎来说,在活动线程中,只有位于JVM虚拟机栈栈顶的元素才是有效的,即称为当前栈帧,与这个栈帧相关联的方法称为当前方法,定义这个方法的类叫做当前类。

执行引擎运行的所有字节码指令只针对当前栈帧进行操作。如果当前方法调用了其他方法,或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧了。

调用新的方法时,新的栈帧也会随之创建。并且随着程序控制权转移到新方法,新的栈帧称为当前栈帧。方法返回之际,原栈帧会放回方法的执行结果给之前的栈帧(返回方法的调用者),随后虚拟机将会丢弃此栈帧。

关于「栈帧」,我们在看看《Java虚拟机规范》中的描述:

栈帧是用来存储数据和部分过程结果的数据结构,同时也用来处理动态连接、方法返回值和异常分派。
栈帧随着方法调用而创建,随着方法结束而销毁——无论方法正常完成还是异常完成都算作方法结束。
栈帧的存储空间由创建它的线程分配在Java虚拟机栈之中,每一个栈帧都有自己的本地变量表 (局部变量表)、操作数栈和指向当前方法所属的类的运行时常量池的引用。

接下来,详细讲解一下栈帧中的局部变量表、操作数栈、动态连接、方法返回地址等各个部分的数据结构和作用。

  • 局部变量表

局部变量表(Local Variable Table)是一组变量存储空间,用于存放方法参数和方法内定义的局部变量。

局部变量表存放了编译期可知的各种基本类型数据(boolean、byte、char、short、int、float、long、double)、reference类型(一个对象实例的引用)、returnAddress类型(指向了一条字节码指令的地址,为jsr、jsr_w和ret指令服务的,目前已经很少使用了)

局部变量表的内容已变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用的内存空间大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。在Java程序编译为Class文件时,就在方法的Code属性max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。(最大Slot的数量)

虚拟机通过索引定位的方法找到局部变量,索引的范围从0~局部变量表最大容量。如果Slot是32位的,则遇到一个64位数据类型(eg:long型、double型)的变量时,会连续使用两个连续的Slot来存储。

局部变量表所需的内存空间在编译期完成分配,当进入一个方法时,这个方法所需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

  • 操作数栈

操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO).
当一个方法开始执行时,操作数栈是空的,随着方法的执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或者变量到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样的出栈/入栈的过程。

操作数栈的每一个元素的类型可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2个栈容量。

同局部变量一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性max_stacks数据项中。且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。

  • 动态链接

在一个class文件中,一个方法调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。
Java虚拟机中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个应用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。
这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态转化。另一部分将在每次运行期间转化为直接应用,这类转化称为动态链接。

  • 方法返回

当一个方法开始执行时,可能有两种方式退出该方法: 正常完成出口、异常完成出口。
正常完成出口是指方法正常完成并退出,没有抛出任何异常(包括Java虚拟机异常以及执行时通过throw语句显示抛出的异常)。如果当前方法正常完成,则根据当前方法返回的字节码指令,这时有可能会有返回值传递给方法调用者(调用它的方法),或者无返回值。具体是否有返回值以及返回值的数据类型将根据该方法返回的字节码指令确定。
异常完成出口是指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。无论是Java虚拟机抛出的异常还是代码中使用athrow指令产生的异常,只要在本方法的异常表中没有搜索到相应的异常处理器,就会导致方法退出。
无论方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在当前栈帧中保存一些信息,用来帮他恢复它的上层方法执行状态。方法退出过程实际上就等同于把当前栈帧出栈,因此退出可以执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者的操作数栈中,调整PC计数器的值以指向方法调用指令后的下一条指令。

  • 栈异常

在Java虚拟机规范中,对此区域规定了两种异常状况:

如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出Stack OverflowError异常;
如果虚拟机栈可以动态扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈

本地方法栈与虚拟机栈相似,区别就是虚拟机栈为虚拟机执行Java服务(字节码服务),而本地方法栈为虚拟机使用到的Native方法(如C++方法)服务。

什么是本地方法?

简单来讲,一个Native Method就是一个Java调用非Java代码的接口。

“A native method is a Java method whose implementation is provided by non- java code.”

一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。
在定义一个native method时,并不提供实现体(有些像定义一个java interface),因为其实现体是由非java语言在外面实现的。下面给了一个示例:

public class IHaveNatives { 
	native public void Native1( int x ) ; 
	native static public long Native2() ; 
	native synchronized private float Native3( Object o ) ; 
	native void Native4( int[] ary ) throws Exception ; 
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ithuman.com.cn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值