深度解析JVM世界:JVM内存结构

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 深度解析JVM世界:JVM内存结构

JVM:全称 Java Virtual Machine,即 Java 虚拟机,一种规范,本身是一个虚拟计算机,直接和操作系统进行交互,与硬件不直接交互,而操作系统可以帮我们完成和硬件进行交互的工作

特点:

  • Java 虚拟机基于二进制字节码执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成
  • JVM 屏蔽了与操作系统平台相关的信息,从而能够让 Java 程序只需要生成能够在 JVM 上运行的字节码文件,通过该机制实现的跨平台性

Java 代码执行流程:Java 程序 --(编译)--> 字节码文件 --(解释执行)--> 操作系统(Win,Linux)

下面介绍JVM的内存结构:

1.内存概述

内存结构是 JVM 中非常重要的一部分,是非常重要的系统资源,是硬盘和 CPU 的桥梁,承载着操作系统和应用程序的实时运行,又叫运行时数据区

JVM 内存结构规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行

Java1.8 以前的内存结构图:

(图片来源:https://github.com/Seazean/JavaNote)

Java1.8 之后的内存结果图:

(图片来源:https://github.com/Seazean/JavaNote)

2.JVM内存

2.1 *虚拟机栈*

2.1.1 Java 栈

Java 虚拟机栈:Java Virtual Machine Stacks,每个线程运行时所需要的内存

(图片来源:https://www.bilibili.com/video/BV1yE411Z7AP)

虚拟机栈的运行过程:

  • 每个方法被执行时,都会在虚拟机栈中创建一个栈帧 stack frame(一个方法一个栈帧
  • Java 虚拟机规范允许 Java 栈的大小是动态的或者是固定不变的
  • 虚拟机栈是每个线程私有的,每个线程只能有一个活动栈帧,对应方法调用到执行完成的整个过程
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存,每个栈帧中存储着:
  • 局部变量表:存储方法里的 Java 基本数据类型以及对象的引用
  • 动态链接:也叫指向运行时常量池的方法引用
  • 方法返回地址:方法正常退出或者异常退出的定义
  • 操作数栈或表达式栈和其他一些附加信息

虚拟机栈特点:

  • 栈内存不需要进行GC,方法开始执行的时候会进栈,方法调用后自动弹栈,相当于清空了数据
  • 栈内存分配越大越大,可用的线程数越少(内存越大,每个线程拥有的内存越大)
  • 方法内的局部变量是否线程安全
  • 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的(逃逸分析)
  • 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

设置栈内存大小:-Xss size -Xss 1024k

2.1.2 局部变量

局部变量表也被称之为局部变量数组或本地变量表,本质上定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量

  • 表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题
  • 表的容量大小是在编译期确定的,保存在方法的 Code 属性的 maximum local variables 数据项中
  • 表中的变量只在当前方法调用中有效,方法结束栈帧销毁,局部变量表也会随之销毁
  • 表中的变量也是重要的垃圾回收根节点,只要被表中数据直接或间接引用的对象都不会被回收

局部变量表最基本的存储单元是 slot(变量槽)

  • 参数值的存放总是在局部变量数组的 index0 开始,到数组长度 -1 的索引结束,JVM 为每一个 slot 都分配一个访问索引,通过索引即可访问到槽中的数据
  • 存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量
  • 32 位以内的类型只占一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占两个 slot
  • 局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么之后申明的新的局部变量就可能会复用过期局部变量的槽位,从而达到节省资源的目的
2.1.3 操作数栈

操作数栈:在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop)

  • 保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间,是执行引擎的一个工作区
  • Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈
  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中

栈顶缓存技术 ToS(Top-of-Stack Cashing):将栈顶元素全部缓存在 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行的效率

基于栈式架构的虚拟机使用的零地址指令更加紧凑,完成一项操作需要使用很多入栈和出栈指令,所以需要更多的指令分派(instruction dispatch)次数和内存读/写次数,由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,所以需要栈顶缓存技术

2.1.4 动态链接

动态链接是指向运行时常量池的方法引用,涉及到栈操作已经是类加载完成,这个阶段的解析是动态绑定

  • 为了支持当前方法的代码能够实现动态链接,每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用

  • 在 Java 源文件被编译成的字节码文件中,所有的变量和方法引用都作为符号引用保存在 class 的常量池中,常量池的作用:提供一些符号和常量,便于指令的识别

(图片来源:https://www.bilibili.com/video/BV1yE411Z7AP)

2.1.5 返回地址

Return Address:存放调用该方法的 PC 寄存器的值

方法的结束有两种方式:正常执行完成、出现未处理的异常,在方法退出后都返回到该方法被调用的位置

  • 正常:调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址
  • 异常:返回地址是要通过异常表来确定

正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者

异常完成出口:方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,本方法的异常表中没有搜素到匹配的异常处理器,导致方法退出

两者区别:通过异常完成出口退出的不会给上层调用者产生任何的返回值

2.1.6 附加信息

栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息,例如对程序调试提供支持的信息

2.2 本地方法栈

本地方法栈是为虚拟机执行本地方法时提供服务的

JNI:Java Native Interface,通过使用 Java 本地接口程序,可以确保代码在不同的平台上方便移植

  • 不需要进行 GC,与虚拟机栈类似,也是线程私有的,有 StackOverFlowError 和 OutOfMemoryError 异常
  • 虚拟机栈执行的是 Java 方法,在 HotSpot JVM 中,直接将本地方法栈和虚拟机栈合二为一
  • 本地方法一般是由其他语言编写,并且被编译为基于本机硬件和操作系统的程序
  • 当某个线程调用一个本地方法时,就进入了不再受虚拟机限制的世界,和虚拟机拥有同样的权限
  • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
  • 直接从本地内存的堆中分配任意数量的内存
  • 可以直接使用本地处理器中的寄存器

原理:将本地的 C 函数(如 foo)编译到一个共享库(foo.so)中,当正在运行的 Java 程序调用 foo 时,Java 解释器利用 dlopen 接口动态链接和加载 foo.so 后再调用该函数

  • dlopen 函数:Linux 系统加载和链接共享库
  • dlclose 函数:卸载共享库

**

**

2.3 程序计数器

Program Counter Register 程序计数器(使用寄存器来存储地址)

作用:内部保存字节码的行号,用于记录正在执行的字节码指令地址(如果正在执行的是本地方法则为空)

简单来讲就是记住下一条jvm指令的执行地址

原理:

  • JVM 对于多线程是通过线程轮流切换并且分配线程执行时间,一个处理器只会处理执行一个线程
  • 切换线程需要从程序计数器中来回去到当前的线程上一次执行的行号

特点:

  • 是线程私有的(每个线程都有自己的程序计数器)
  • 不会存在内存溢出,是 JVM 规范中唯一一个不出现 OOM 的区域,所以这个空间不会进行 GC

Java 反编译指令:javap -v Test.class

#20:代表去 Constant pool 查看该地址的指令

0: getstatic #20     // PrintStream out = System.out;3: astore_1       // --4: aload_1         // out.println(1);5: iconst_1       // --6: invokevirtual #26   // --9: aload_1         // out.println(2);10: iconst_2       // --11: invokevirtual #26   // --

2.4 堆

Heap 堆:是 JVM 内存中最大的一块,由所有线程共享,由垃圾回收器管理的主要区域,堆中对象大部分都需要考虑线程安全的问题

存放哪些资源:

  • 对象实例:类初始化生成的对象,基本数据类型的数组也是对象实例,new 创建对象都使用堆内存
  • 字符串常量池:
  • 字符串常量池原本存放于方法区,JDK7 开始放置于堆中
  • 字符串常量池存储的是 String 对象的直接引用或者对象,是一张 string table
  • 静态变量:静态变量是有 static 修饰的变量,JDK8 时从方法区迁移至堆中
  • 线程分配缓冲区 Thread Local Allocation Buffer:线程私有但不影响堆的共性,可以提升对象分配的效率

设置堆内存指令:-Xmx Size

内存溢出:new 出对象,循环添加字符数据,当堆中没有内存空间可分配给实例,也无法再扩展时,就会抛出 OutOfMemoryError 异常

堆内存诊断工具:(控制台命令)

  1. jps:查看当前系统中有哪些 Java 进程
  2. jmap:查看堆内存占用情况 jhsdb jmap --heap --pid 进程id
  3. jconsole:图形界面的,多功能的监测工具,可以连续监测

在 Java7 中堆内会存在年轻代、老年代和方法区(永久代)

  • Young 区被划分为三部分,Eden 区和两个大小严格相同的 Survivor 区。Survivor 区某一时刻只有其中一个是被使用的,另外一个留做垃圾回收时复制对象。在 Eden 区变满的时候,GC 就会将存活的对象移到空闲的 Survivor 区间中,根据 JVM 的策略,在经过几次垃圾回收后,仍然存活于 Survivor 的对象将被移动到 Tenured 区间
  • Tenured 区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在 Young 复制转移一定的次数以后,对象就会被转移到 Tenured 区
  • Perm 代主要保存 Class、ClassLoader、静态变量、常量、编译后的代码,在 Java7 中堆内方法区会受到 GC 的管理

分代原因:不同对象的生命周期不同,70%-99% 的对象都是临时对象,优化 GC 性能

public static void main(String[] args) {    // 返回Java虚拟机中的堆内存总量    long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;    // 返回Java虚拟机使用的最大堆内存量    long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
    System.out.println("-Xms : " + initialMemory + "M");//-Xms : 245M    System.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641M}

2.5 方法区

方法区:是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是也叫 Non-Heap(非堆)

方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式

方法区的大小不必是固定的,可以动态扩展,加载的类太多,可能导致永久代内存溢出 (OutOfMemoryError)

方法区的 GC:针对常量池的回收及对类型的卸载,比较难实现

为了避免方法区出现 OOM,在 JDK8 中将堆内的方法区(永久代)移动到了本地内存上,重新开辟了一块空间,叫做元空间,元空间存储类的元信息,静态变量和字符串常量池等放入堆中

类元信息:在类编译期间放入方法区,存放了类的基本信息,包括类的方法、参数、接口以及常量池表

常量池表(Constant Pool Table)是 Class 文件的一部分,存储了类在编译期间生成的字面量、符号引用,JVM 为每个已加载的类维护一个常量池

运行时常量池是方法区的一部分

  • 常量池(编译器生成的字面量和符号引用)中的数据会在类加载的加载阶段放入运行时常量池
  • 类在解析阶段将这些符号引用替换成直接引用
  • 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()

注:jdk1.8之后方法区(元空间)使用的内存是物理内存

(图片来源:https://www.bilibili.com/video/BV1yE411Z7AP)

本篇文章到这里就结束了,最后送大家一句话 白驹过隙,沧海桑田


相关文章
|
14天前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
7天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
5天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
8 1
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
62 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
存储 缓存 算法
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
|
24天前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
46 10
|
23天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
30天前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理方式,特别是垃圾回收机制。我们将了解Java的自动内存管理是如何工作的,它如何帮助开发者避免常见的内存泄漏问题。通过分析不同垃圾回收算法(如标记-清除、复制和标记-整理)以及JVM如何选择合适的垃圾回收策略,本文旨在帮助Java开发者更好地理解和优化应用程序的性能。
|
1月前
|
存储 监控 算法
JVM调优深度剖析:内存模型、垃圾收集、工具与实战
【10月更文挑战第9天】在Java开发领域,Java虚拟机(JVM)的性能调优是构建高性能、高并发系统不可或缺的一部分。作为一名资深架构师,深入理解JVM的内存模型、垃圾收集机制、调优工具及其实现原理,对于提升系统的整体性能和稳定性至关重要。本文将深入探讨这些内容,并提供针对单机几十万并发系统的JVM调优策略和Java代码示例。
49 2

推荐镜像

更多