2-1-2、Java体系结构和运行时数据区

9 篇文章 0 订阅


JDK体系结构

在这里插入图片描述

JDK

我们可以把Java程序设计语言、Java虚拟机、Java类库这三部分统称为JDK(Java Development Kit),JDK是用于支持Java开发的最小环境

JRE

我们可以把Java类库API中的Java SE API子集和Java虚拟机这两部分统称为JRE(Java Runtime Environment),JRE是支持Java程序运行的标准环境

Java语言的无关性

JVM支持的语言和平台

在这里插入图片描述

语言无关性(跨语言)

jvm支持Java、Scala、kotlin、groovy、JRuby等语言,无论是哪一种语言,在编译过后都会转换成class字节码文件,因此JVM具有语言无关的特性

在这里插入图片描述

平台无关性(跨平台)

JVM具有跨平台特性,那是因为在JVM执行class字节码文件时,会将class字节码文件“翻译”成不同的机器可执行的语言,其实也就是JVM本身不支持跨平台,但是由于每个不同的操作系统都会安装不同版本的JVM,JVM再将字节码文件翻译成当前机器可执行的机器语言,因此JVM具有平台无关的特性

在这里插入图片描述

JVM可跨越的平台有:Windows、Linux、Unix、Android、Mac等

JVM运行时数据区

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,具体结构如下图所示:

在这里插入图片描述


![在这里插入图片描述](https://img-blog.csdnimg.cn/27f5c965d9094cfc8a5fb090c72c41cc.png)

线程共享

线程共享的区域主要有方法区和堆,方法区又包含常量池、运行时常量池,类元信息等

方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来

实现

在jdk1.8之前,方法区的实现是永久代的方式,在jdk1.8(包括1.8)之后,方法区的实现是元空间,元空间直接使用了本地内存,不再虚拟机的管理内存中

作用

用于存储已经被虚拟机加载的类信息、常量、静态常量和即时编译器编译后的代码等数据
注:《Java虚拟机规范》对方法区的约束是非常宽松的,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,甚至还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域的确是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法

注意

方法区的大小不必是固定的,JVM可以根据应用的需要动态调整

JDK7及以前
  1. 通过-XX:PermSize 来设置永久代初始分配空间。默认值是20.75M
  2. 通过 -XX:MaxPermSize 来设定永久代最大可分配空间。32位机器默认是 64M,64位机器模式是 82M
  3. 当 JVM 加载的类信息容量超过了这个值,会报异常 OutOfMemoryError:PermGen space
JDK8 以后
  1. 元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 来指定
  2. 默认值依赖于平台。Windows下:-XX:MetaspaceSize 是 21M,-XX:MaxMetaspaceSize的值是 -1,即没有限制
  3. 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常 OutOfMemoryError:Metaspace
  4. -XX:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端 JVM 来说,其默认的 -XX:MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,FullGC 将会被触发并卸载没用的类(即这些类对应的类加载器不再存活)然后这个高水位线将会重置。新的高水位线的值取决于 GC 后释放了多少元空间。如果释放的空间不足,那么在不超过 MaxMetaspaceSize 时,适当提高该值。如果释放空间过多,则适当降低该值
  5. 如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到 FullGC 多次调用。为了避免频繁地 GC,建议将 -XX:MetaspaceSize 设置为一个相对较高的值

对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。在《Java虚拟机规范》中对Java堆的描述是:“所有的对象实例以及数组都应当在堆上分配”,而这里写的“几乎”是指从实现角度来看,随着Java语言的发展,现在已经能看到些许迹象表明日后可能出现值类型的支持,即使只考虑现在,由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了

作用

用来存储对象信息,GC回收主要回收的也是堆空间的“死亡”对象

实现

由于一部分对象的存活周期很短(朝生夕死),而另一部分对象经历过一定次数的回收之后便不容易“死亡”,因此将堆空间氛围年轻代和老年代

  1. 年轻代:新创建的对象大多都会在年轻代,年轻代分为Eden区、From Survivor和To Survivor,他们之间的大小比例一般为8:1:1
  2. 老年代:经历一定次数的年轻代回收(一般为15次),对象会存入老年代

线程私有

线程私有的区域主要是栈空间,主要包含虚拟机栈、本地方法栈和程序计数器

虚拟机栈

在这里插入图片描述


虚拟机栈(Java Virtual Machine Stack)是一个线程私有的,他的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
  • 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常
栈帧

在这里插入图片描述


栈帧包含局部变量表、操作数栈、动态连接和完成出口,线程在运行过程中,只有一个栈帧处于执行中,它被称为当前活跃栈帧,当前活跃栈帧永远是虚拟机栈的顶部元素。每个方法都会产生一个栈帧,而每个栈帧的大小都依赖于方法区中记载的类信息决定

局部变量表

局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)

  • 局部变量表中的存储空间以存储变量槽来表示,其中long和double类型的数据会占用两个变量槽,其余的都占用一个
  • 局部变量表所需的内存空间在编译期间完成分配,在执行方法时,栈帧中的局部变量表大小(代指变量槽的数量)是确定的,方法执行期间不会去更改其大小
  • 具体一个变量槽代指多大的内存空间(比如64bit或32bit),由不同的虚拟机各自实现
操作数栈

操作数栈也是一个后入先出的栈,class字节码文件的指令就是对操作数栈上的数据进行操作,具体执行涉及到jvm的执行引擎,操作数栈只是用来提供和存储执行引擎需要执行和返回的数据。理论上,两个栈帧作为虚拟机的元素,他们之间是相互独立的,目前大多数虚拟机都会对实现做一些优化处理,也就是令两个栈帧出现一部分重叠,让下栈帧的部分操作数栈与上栈帧的部分局部变量表重叠在一起,这样在进行方法调用返回时,就可以共用一部分数据,而无需进行额外的参数复制传递

在这里插入图片描述

动态连接

每个栈帧都包含一个对运行时常量池中方法属性的引用持有这个引用是为了支持方法调用过程中的动态连接。在Class文件中的常量池中存在大量的符号引用,class文件字节码中的方法调用指令就是以常量池中的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化成为静态解析。另一部分在每次运行期间直接转化为直接引用,这部分称为动态连接

完成出口(返回地址)

当一个方法开始执行后,只有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者或者主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为“正常调用完成”(Normal Method Invocation Completion)
另外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为“异常调用完成(Abrupt Method Invocation Completion)”。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者提供任何返回值的

本地方法栈

这里是引用本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常

程序计数器

与虚拟机栈一样,程序计数器也是线程私有的,每个线程都会有一个程序计数器
《深入理解Java虚拟机》一书中提出:程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(它代表了所有虚拟机的统一外观,但各款具体的Java虚拟机并不 一定要完全照着概念模型的定义来进行设计,可能会通过一些更高效率的等价方式去实现它),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
特点:

  • 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址
  • 如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)
  • 此内存区域是唯 一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域
与分支、循环、跳转、异常处理有关

class字节码文件中的分支、循环、跳转和异常等指令在离开当前栈帧进行跳转等操作或离开当前指令行进行循环等操作的时候,会记录相应的行号偏移量在程序计数器中

与CPU时间分片有关

由于Java虚拟机的多线程是通过线程轮流切换,分配处理器(CPU)执行时间的方式来实现的,在任何一个确定的时刻,每个处理器都会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的程序计数器互不影响

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值