Java虚拟机的内存分配和运行机制(粗谈)
发布日期:2021-07-27 04:56:11 浏览次数:6 分类:技术文章

本文共 6666 字,大约阅读时间需要 22 分钟。

一、概述

1.1 虚拟机定义

定义:模拟某种计算机体系结构,执行特定指令集的软件

1.2 虚拟机分类

主要分为两大类:一种是系统虚拟机(Virtual Box、VMware),另一种是程序虚拟机(JVM、.NET CLR、P-Code)。

P-Code:由加州大学圣地亚哥分校(University of California, San Diego,UCSD)于1978年发布的高度可移植、机器无关的、运行Pascal语言的虚拟机

1.3 java虚拟机概述

Java语言虚拟机

可以执行 Java 语言的高级语言虚拟机。 Java 语言虚拟机并不一定就可以称为 JVM,譬如:Apache Harmony。

Java™ 虚拟机

  • 必须通过 Java TCK(Technology Compatibility Kit)的兼容性测试的Java语言虚拟机才能称为
  • “Java™ 虚拟机”
  • Java™ 虚拟机并非一定要执行“Java”程序
  • 业界三大商用 JVM:Oracle HotSpot 、 Oracle JRockit VM、IBM J9 VM
  • 其他虚拟机:Google Dalvik VM、Microsoft JVM

Java虚拟机是整个Java平台的基石,是Java技术用以实现硬件无关与操作系统无关的关键部分,是Java语言生成出极小体积的编译代码的运行平台,是保障用户机器免于恶意代码损害的保护屏障。

Java虚拟机可以看作是一台抽象的计算机。如同真实的计算机那样,它有自己的指令集以及各种运行时内存区域。使用虚拟机来实现一门程序设计语言有许多合理的理由,业界中流传最为久远的虚拟机可能是UCSD Pascal的P-Code虚拟机。

第一个Java虚拟机的原型机是由Sun Microsystems公司实现的,它被用在一种类似PDA(Personal Digital Assistant,俗称掌上电脑)的手持设备上仿真实现Java虚拟机指令集。时至今日,Oracle已有许多Java虚拟机实现应用于移动设备、桌面电脑、服务器等领域。Java虚拟机并不局限于特定的实现技术、主机硬件和操作系统,Java虚拟机也不局限于特定的代码执行方式,它不强求使用解释器来执行程序,也可以通过把自己的指令集编译为实际CPU的指令来实现,它可以通过微代码(Microcode)来实现,或者甚至直接实现在CPU中。

Java虚拟机与Java语言并没有必然的联系,它只与特定的二进制文件格式——Class文件格式所关联,Class文件中包含了Java虚拟机指令集(或者称为字节码、Bytecodes)和符号表,还有一些其他辅助信息。

基于安全方面的考虑,Java虚拟机要求在Class文件中使用了许多强制性的语法和结构化约束,但任一门功能性语言都可以表示为一个能被Java虚拟机接收的有效的Class文件。作为一个通用的、机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机作为他们语言的产品交付媒介。

译者注:由加州大学圣地亚哥分校(University of California, San Diego,UCSD)于1978年发布的高度可移植、机器无关的、运行Pascal语言的虚拟机。

1.4 java虚拟机架构

在这里插入图片描述

  • Class Loader 类加载器
  • Execution Engine 执行引擎
  • Native Interface本地接口
  • Runtime data area运行时数据区

1.5 java虚拟机的具体实现

Oracle HotSpot 虚拟机

最初由名为“Longview Technologies”的小公司开发,后被 Sun 公司收购。最初并非面向 Java 语言开发,而是面向 Strongtalk 语言。

HotSpot 命名来自它的“热点代码探测”技术。

从 JDK 1.2 开始加入 Sun(Oracle)JDK,在 JDK 1.3 开始成为 Sun(Oracle)JDK 的默认实现,在1.4中成为唯一的虚拟机。

在2006年底开始开源,由此建立的 OpenJDK 项目。

本系列课程中所有的虚拟机实现,所指的都是 Oracle HotSpot 虚拟机。

公有设计,私有实现

前面所讲内存区域是在《Java 虚拟机规范》(JVMS)中定义的概念模型,但JVMS也同时声明这些概念不约束虚拟机的具体实现,只要求虚拟机实现的效果在外部看起来与规范描述一致即可。

比如:堆内存在规范中规定是需要自动内存管理的,但是如何管理是具体虚拟机实现。

二、Java 虚拟机运行时数据区【重点】

2.1 概述

Java 虚拟机运行时数据区

在《Java 虚拟机规范》中定义了若干种程序运行期间会使用到的存储不同类型数据的区域。

有一些区域是全局共享的,随着虚拟机启动而创建,随着虚拟机退出而销毁。

有一些区域是线程私有的,随着线程开始和结束而创建和销毁。

是所有 Java 虚拟机共同的内存区域概念模型。

2.2 基本架构

在这里插入图片描述

运行时数据区的划分

线程私有

  • 程序计数器
  • Java 虚拟机栈
  • 本地方法栈

线程共享

  • Java 堆
  • 方法区

2.3 程序计数器

程序计数器(Program Counter Register)

一块较小的的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。

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

如果正在执行的是 Native 方法,这个计数器值则为

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

在虚拟机的概念模型里,字节码解释器工作时就是通过在改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

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

2.4 Java 虚拟机栈

Java 虚拟机栈的特征

  • 线程私有
  • 后进先出(LIFO)栈
  • 存储栈帧,支撑 Java 方法的调用、执行和退出
  • 可能出现 OutOfMemoryError 异常和 StackOverflowError 异常

2.5 本地方法栈

本地方法栈的特征

  • 线程私有
  • 后进先出(LIFO)栈
  • 作用是支撑 Native 方法的调用、执行和退出
  • 可能出现 OutOfMemoryError 异常和 StackOverflowError 异常
  • 有一些虚拟机(如 HotSpot)将 Java 虚拟机栈和本地方法栈合并实现

2.6 栈帧

2.6.1 概述

在这里插入图片描述

每一个线程的启动,在内存中创建一个对应的JVM栈,用于存储“栈帧”

执行机制

(1)一个方法对应一个栈帧结构
(2)一个方法从调用起到执行完毕的过程,就对应一个栈帧在JVM中从入栈到出栈的过程。

当main方法调用A方法,A方法中调用B方法,B方法调用C方法,则执行后,C执行完毕出栈,然后B,然后A,最后main方法。

2.6.2 栈帧结构

在这里插入图片描述

栈帧的概念和特征:

Java 虚拟机栈中存储的内容,它被用于存储数据和部分过程结果的数据结构,同时也被用来处理动态连接、方法返回地址和异常完成信息。

一个完整的栈帧包含:局部变量表、操作数栈、动态连接、方法返回地址和异常完成信息。

2.6.3 栈帧结构-局部变量表

局部变量表概念和特征

由若干个 Slot(槽) 组成,长度由编译期决定,Code属性的max_locals指定。

单个Slot可以存储一个类型为 boolean、byte、char、short、int、float、reference 和returnAddress(已过时)的数据,两个Slot可以存储一个类型为long或double的数据。

局部变量表用于方法间参数传递,以及方法执行过程中存储基础数据类型的值和对象的引用

2.6.4 栈帧结构-操作数栈

操作数栈的概念和特征

是一个后进先出栈,由若干个 Entry 组成,长度由编译期决定、由Code属性的max_stacks指定。

单个 Entry 即可以存储一个 Java 虚拟机中定义的任意数据类型的值,包括 long和 double 类型,但是存储 long 和 double 类型的 Entry 深度为2,其他类型的深度为1。

在方法执行过程中,操作数栈用于存储计算参数和计算结果;在方法调用时,操作数栈也用来准备调用方法的参数以及接收方法返回结果。

通过一个具体例子来演示栈帧的局部变量表和操作数栈的工作方式

public class add {
public int calc() {
int a = 100; int b = 200; int c = 300; return (a + b) * c; }}

在这里插入图片描述

  1. 程序计数器负责记录【偏移值】,即程序运行到了第几步
  2. 局部变量表中,当创建的对象是static属性,则对应为args,普通类则为this
  3. bipush,将100放入栈顶
  4. istore_1,将100从栈顶存入局部变量表的1位置
  5. 依次执行3、4步骤,存入200和300
  6. iload_1,将1位置的值(100)放入栈顶,然后继续将200也放入
  7. iadd,执行加操作,300放入栈顶,然后将另一个300入栈,执行乘法,最后返回结果

Java 虚拟机栈和本地方法栈可能发生如下异常情况:

如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量时,Java 虚拟机将会抛出一个 StackOverflowError 异常。

如果 Java 虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。

调整栈空间大小:-Xss1m

在jdk1.5之前栈默认大小为256k,jdk1.5之后为1m。

2.6.5 栈帧结构-动态连接

每个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。

每一次方法调用时,动态的将符号引用转成直接引用(入口地址),支持多态

2.6.6 栈帧结构-方法返回地址

方法结束有正常结束和异常结束

正常结束:当前栈帧承担着恢复调用者状态的责任,其中包括恢复调用者的局部变量表和操作数栈、正确递增程序计数器、将返回值压入调用者的操作数栈。

异常结束:如果当前方法中没有处理此种异常、当前栈帧恢复调用者状态的同时,不会返回任何值,而是通过athrow指令将当前异常抛给调用者。

2.7 堆(heap)

2.7.1 特征和参数

Java 堆的特征

  • 全局共享
  • 通常是 Java 虚拟机中最大的一块内存区域
  • 作用是做为 Java 对象或数组的主要存储区域
  • JVMS 明确要求该区域需要实现自动内存管理,即常说的 GC,但并不限制采用哪种算法和技术去实现
  • 可能出现 OutOfMemoryError 异常

相关配置和参数

  • 由JVM自动管理的线程共享区域,在JVM启动时创建。

  • 堆可以处于逻辑上连续、物理上不连续的空间当中。既可以实现固定大小的,也可以实现为可扩展的,当前主流虚拟机实现都是可扩展的,可通过相关的参数进行配置。

  • Java堆相关参数:

    -Xms:初始堆大小
    -Xmx:最大堆大小
    -XX:+PrintGC:打印查看GC日志
    -XX:+PrintGCDetails:查看日志详情
    -XX:+PrintCommandLineFlags:打印虚拟机的参数
    -XX: 对于系统级别(JVM)的配置,-X对程序的 设置参数
    非-XX基本是对应用层面的设置
    + 表示启动
    - 表示禁用

2.7.2 老年代和新生代

在这里插入图片描述

HotSpot虚拟机采用分代存储(Java内存模型JMM)的方式

分代存储: 因对象的生命周期不同,HotSpot虚拟机将堆中对象按照年龄进行划分,分别存入“新生代”和“老年代”。

相关参数:

-Xmn:新生代大小
-XX:NewRatio=?: 表示年轻代和老年代的比例, 默认1:2
-XX:SurvivorRatio=?:Eden和Survivor的比例,JDK1.8是6:1:1,之前是8:1:1
-如果设置-XX:NewRatio=4,指标是Old Generation :Young Generation= 4:1,即年轻代占堆的1/5, 老年代4/5

串行收集器: DefNew:是使用-XX:+UseSerialGC(新生代,老年代都使用串行回收收集器)。

并行收集器: ParNew:是使用-XX:+UseParNewGC(新生代使用并行收集器,老年代使用串行回收收集器)或者-XX:+UseConcMarkSweepGC(新生代使用并行收集器,老年代使用CMS)。

PSYoungGen:是使用-XX:+UseParallelOldGC(新生代,老年代都使用并行回收收集器)或者-XX:+UseParallelGC(新生代使用并行回收收集器,老年代使用串行收集器)

garbage-first heap:是使用-XX:+UseG1GC(G1收集器)

2.7.3 HotSpot对象分配策略以及栈堆的关联

HotSpot对象分配策略

优先分配Eden区:绝大多数对象都是“朝生夕死”的对象,Eden区的回收时间短、效率高,适用于频繁回收。

大对象直接进入老年代:Enden和Survivor的空间不足时,大对象直接进入老年代。

-XX:PretenureSizeThreshold=KB ,对象大小超过此值,直接进入老年代。
长期存活对象进入老年代:在Survivor区存活N岁(来回复制N次)的对象,将直接进入老年代。
-XX:MaxTenuringThreshold=15(只有单线程收集器可用)

从栈到堆的关联过程

Object obj = new Object()

在这里插入图片描述

Java 堆可能发生如下异常情况:

如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。

2.8 方法区和运行时常量池

方法区的特征

  • 全局共享
  • 作用是存储 Java 类的结构信息、常量、静态变量、即时编译器编译后的代码
  • JVMS 不要求该区域实现自动内存管理,但是商用 Java 虚拟机都能够自动管理该区域的内存
  • 可能出现 OutOfMemoryError 异常

运行时常量池的特征

  • 全局共享
  • 是方法区的一部分
  • 作用是存储 Java 类文件常量池中的符号信息
  • 可能出现 OutOfMemoryError 异常

HotSpot 方法区实现的变迁

  • 永久代与方法区
  • 在 JDK 1.2 ~ JDK 6,HotSpot 使用永久代实现方法区
  • 在 JDK 7 开始,HotSpot 开始了移除永久的代计划
    • 符号表被移到 Native Heap 中
    • 字符串常量和类的静态引用被移到 Java Heap 中
  • 在 JDK 8 开始,永久代已被元空间(Metaspace)所代替
    • 字符串常量和类的静态引用仍然在堆中

2.9 直接内存(Direct Memory)

直接内存特征

直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。

应用在某些场景中能显著提高性能,因为其避免了在Java堆和Native堆中来回复制数据。

在Jdk1.4的NIO中已经出现直接内存的使用。

可能出现 OutOfMemoryError 异常。

在HotSpot虚拟机中还有其他一些空间

  • TLAB: TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。

  • CodeCache :Code Cache用于存储JVM JIT产生的编译代码。

  • Compressed Class Space(JDK1.8以后)

转载地址:https://blog.csdn.net/qq_45337431/article/details/100710721 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:web开发之BaseServlet的使用
下一篇:常用的工具类代码

发表评论

最新留言

初次前来,多多关照!
[***.217.46.12]2024年10月04日 22时32分53秒