IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 【JVM】第一章 JVM体系结构 -> 正文阅读

[Java知识库]【JVM】第一章 JVM体系结构

第一章 JVM体系结构


初学 JVM,已经尽力在网上寻找最正确的资料,如有纰漏望指正!

一、JVM 体系结构

1.体系结构

下图是 JVM 的体系结构,基于 HotSpot VM
在这里插入图片描述
下图是 JDK 1.8 的内存模型
在这里插入图片描述
我们看到,第一张图中的方法区在第二张图里并没有找到。这里我们需要注意,方法区只是一种抽象的概念,在不同的 JDK 版本,它的实现方式不同

版本变化
JDK 1.6 及之前有永久代,类信息、静态变量、和常量池存放在永久代
JDK 1.7字符串常量池、静态变量移出永久代,被放在堆中
JDK 1.8 及之后去除了永久代,由本地内存的元数据区(Metaspace)取代

在这里插入图片描述
在这里插入图片描述

  • JDK 1.7 中字符串常量池 (StringTable) 为什么从永久代移到堆中?
    永久代的回收效率很低,只有 Full GC 才会触发,(老年代或永久代空间不足会触发Full GC)导致字符串常量池回收效率不高,开发中会有大量字符串被创建,放到堆里能够及时回收内存
  • JDK 1.8 为什么去除永久代?
    第一点,永久代如果存放在 JVM 中,那么很难确定合适的大小。元数据区被分配在本地内存,则无需考虑大小
    第二点,原来在运行时生成大量的类,造成经常需要 Full GC,如使用反射和代理
    第二点,对永久代调优很困难

需要注意,元数据区从虚拟机转移到了本地内存意味着什么?默认情况下,元数据区的大小仅受本地内存的限制,这意味着以后不会因为永久代空间不够而抛出 OOM 异常了。JDK 1.8 以前版本的 class 和 jar 包数据存储在永久代,永久代的大小是固定的,而且项目之间无法共用公有的 class,所以很容易碰到 OOM 异常。改成元数据区后,各个项目会共享同样的 class 内存空间,比如多个项目都引用了 apache-common 包,在元数据区只会存储一份 apache-common 的 class,提高了内存的利用率,垃圾回收更有效率

2.程序计数器(Program Counter Register)

程序计数器是一个记录着当前线程所执行的字节码的行号指示器。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。内存空间小、线程私有

Java 代码编译后的字节码在未经过 JIT (实时编译器)编译前,其执行方式是通过 “字节码解释器” 进行解释执行。简单的工作原理为:解释器读取装载入内存的字节码,按照顺序读取字节码指令。在读取一个指令后,将该指令 “翻译” 成固定的操作,并根据这些操作进行分支、循环、跳转、异常处理、线程恢复等基础功能

从上面的描述中,可能会产生程序计数器是否多余的疑问,因为沿着指令的顺序执行下去,即使是分支跳转这样的流程没跳转到指定的指令处按顺序继续执行是完全能够保证程序的执行顺序的。假设程序永远只有一个线程,程序计数器确实多余,但是实际上程序是通过多个线程协同合作执行的,这就有说法了

首先我们要搞清楚 JVM 的多线程实现方式。JVM 的多线程是通过 CPU 时间片轮转(即线程轮流切换并分配处理器执行时间)来实现的。因此,某个线程在执行过程中可能会因为时间片耗尽而被挂起,另一个线程会获取到时间片并开始执行。当被挂起的线程重新获取到时间片的时候,它想要从被挂起的地方继续执行,就必须直到它上次执行到哪个位置,在 JVM 中,就是通过程序计数器来记录某个线程的字节码执行位置的。所以,程序计数器具备线程隔离的特性,每个线程工作时都有属于自己的独立计数器

如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是 Native 方法,这个计数器的值则为(Undefined)。因为 Native 方法是 Java 通过 JNI(本地方法接口)直接调用本地 C/C++ 库,可以近似地认为 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口到 C/C++ 方法。由于该方法是通过 C/C++ 而不是 Java 进行实现。那么自然无法产生相应的字节码,并且 C/C++ 执行时的内存分配是由自己语言决定的,而不是由 JVM 决定

在这里插入图片描述

2.虚拟机栈(JVM Stacks)

线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程

  • 局部变量表:存放了编译器可知的八大基本类型、引用类型和 returnAddress 类型(指向了一条字节码指令的地址)

虚拟机栈也会发生 OOM 异常

  • StackOverflowErroe:线程请求的栈深度大于虚拟机所允许的深度
  • OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存

虚拟机栈是用于描述 Java 方法执行的内存模型。方法调用时,创建栈帧,并压入虚拟机栈。方法执行完毕,栈帧出栈并被销毁
在这里插入图片描述

3.本地方法栈(Native Method Stacks)

区别于虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常

如何去服务 Native 方法、Native 方法使用什么语言实现、怎么组织像栈帧这种为了服务方法的数据结构等问题,虚拟机规范并未给出强制规定,因此不同的虚拟机可以进行自由实现,我们常用的 HotSpot 虚拟机选择合并了虚拟机栈和本地方法栈,HotSpot 虚拟机不区分本地方法栈和虚拟机栈

4.堆区(Heap)

线程共享,主要是存放对象和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。发生 OOM 的原因是堆中没有内存完成实例分配,并且堆也无法再扩展。一般来说,堆无法分配对象时会进行一次 GC,如果 GC 后仍然无法分配对象,才会报错

堆是用于存放对象的内存区域。因此,它是垃圾收集器(GC)管理的主要目标,其具有以下特点:

  • 堆在逻辑上划分为 “新生代” 和 “老年代”。由于 Java 中的对象大部分是朝生夕灭,还有一部分能够长期地驻留在内存中,为了对这两种对象进行有效的回收,将堆划分为新生代和老年代,并且执行不同的回收策略。不同的垃圾收集器对这两个逻辑区域的回收机制不尽相同
  • 堆占用的内存并不要求物理连续,只需要逻辑连续即可
  • 堆一般实现成可扩展内存大小,使用 “-Xms” 与 “-Xmx” 控制堆的最小与最大内存,扩展动作交由虚拟机执行。但由于该行为比较消耗性能,因此一般将堆的最大最小内存设为相等
  • 堆是所有线程共享的内存区域,因此每个线程都可以拿到堆上的同一个对象
  • 堆的生命周期是随着虚拟机的启动而创建的

5.方法区(Method Area)

方法区,也称为非堆(Non-Heap),其中主要存储加载的字节码、class/mehod/field 等元数据对象、static-final 常量、static 变量、JIT 编译器编译后的代码等数据。此外方法区包含了一个特殊的区域 “运行时常量池”。方法区有一个最大值上限,因此,若方法区占用内存到达最大值,且无法再申请到内存时,便会抛出 OOM 异常。比如,我们使用 cglib 字节码生成框架不断生成新的类,最终使方法区内存占满,除此之外,动态语言、JSP、基于 OSGI(面向Java的动态模型系统) 的应用都会在方法区额外产生大量的类信息

  • 加载的类字节码
    要使用一个类,首先需要将其字节码加载到 JVM 的内存中。至于类的字节码来源,可以多种多样,如 .class 文件、网络传输或者由 cglib 字节码框架直接生成
  • class/method/field 等元数据对象
    字节码加载之后,JVM 会根据其中的内容,为这个类生成 Class/Method/Field 等对象,它们用于描述一个类,通常再反射中用的比较多。不同于存储在堆中的 Java 实例对象,这两种对象存储在方法区中
  • static-final 常量、static 常量
    对于这两种类型的类成员,JVM 会在方法区为它们创建一份数据,因此同一个类的 static 修饰的类成员只有一份
  • JIT 编译器编译后的代码:以 HotSpot 虚拟机为例,其在运行时会使用 JIT 即使编译器对热点代码进行优化,优化方式为将字节码编译成机器码。通常情况下,JVM 使用 “解释执行” 的方式执行字节码,即 JVM 在读取到一个字节码指令时,会将其按照预先定好的规则执行栈操作,而栈操作会进一步映射为底层的机器操作;通过 JIT 编译后,执行的机器码会直接和底层机器打交道
    在这里插入图片描述

运行时常量池

类的字节码在加载时会被解析并生成不同的东西存入方法区。类的字节码中不仅包含了类的版本、字段、方法、接口等描述信息,还包含了一个常量池。常量池用于存放在字节码中使用到的所有字面量和符号引用(如字符串字面量),在类加载时,它们进入方法区的运行时常量池存放

运行时常量池是方法区中一个比较特殊的部分,具备动态性,也就是说,除了类加载时将常量池写入其中,Java 程序运行期间也可以向其中写入常量

//使用 StringBuilder 在堆上创建字符串 abc,再使用 intern 将其放入运行时常量池
String str = new StringBuilder("abc");
str.intern();
//直接使用字符串字面量 xyz,其被放入运行时常量池
String str2 = "xyz";

开头我们提到 JDK 1.8 去除了永久代,在了解了方法区的细节之后,我们再来思考一下,为什么使用永久代并将 GC 分代收集扩展至方法区不好?首先要明白方法区的回收目标是什么,方法区存储了类的元数据信息和各种常量,它的内存回收目标理应是对这些类型的卸载和常量的回收。但由于这些数据被类的实例引用,卸载条件变得复杂且严格,回收不当会导致堆中的类实例失去元数据信息和常量信息。因此,回收方法区内存不是一件简单高效的事情,往往 GC 在做无用功。另外随着应用规模的变大,各种框架的引入,尤其是使用了字节码生成技术的框架,会导致方法区内存占用越来越大,最终 OOM

6.每个区域存储的内容

在这里插入图片描述

7.直接内存(Direct Memory)

非虚拟机运行时数据区的部分。直接内存会受到本机内存的限制,如果内存区域总和大于物理内存限制会导致动态扩展时抛出 OOM 异常

在 JVM 的内存模型里并不包含直接内存,也就是说这块内存区域并不是 JVM 运行时数据区的一部分,但它却会被频繁的使用,原因是 NIO 这个包。在 JDK 1.4 中系加入 NIO(New Input/Output)类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配对外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作
在这里插入图片描述
可以看出,直接内存的大小并不受到 Java 堆大小的限制,甚至不受到 JVM 进程内存大小的限制。它只受限于本机总内存(RAM 及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制(最常见的就是 32 位/64 位 CPU 的最大寻址空间限制不同)

直接内存出现 OOM 的原因是对该区域进行内存分配时,其内存与其他内存加起来超过最大物理内存限制(包括物理的和操作系统级别的限制)。另外,若我们通过参数 “XX:MaxDirectMemorySize” 指定了直接内存的最大值,其超过指定的最大值时,也会抛出 OOM 异常


参考链接

建议结合以下链接理解

先看视频入门
【狂神说Java】JVM快速入门篇
对JVM还有什么不懂的?资深架构师马士兵一节课带你深入浅出JVM虚拟机丨JVM从入门到实战

jdk1.8中jvm的变化
面试题系列第5篇:JDK的运行时常量池、字符串常量池、静态常量池,还傻傻分不清?
jdk1.8——jvm分析与调优

注意:以下文章基于 JDK 1.7
Java虚拟机(JVM)你只要看这一篇就够了!
01-JVM内存模型:程序计数器
02-JVM内存模型:虚拟机栈与本地方法栈
03-JVM内存模型:堆与方法区
04-JVM内存模型:直接内存

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-08-09 10:06:43  更:2021-08-09 10:07:28 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年2日历 -2025/2/3 7:27:44-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码