JVM是如何分配管理内存的?

简介: JVM是如何分配管理内存的?

本文成文参考了《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》和《Java虚拟机规范(Java SE 8版)》,这是两本难得的好书,推荐大家购买实体书籍,后续会考虑在"借书下饭"栏目下开设子专栏,如果需要电子版尝鲜可以关注后私信我。

一、JVM内存区域

Java程序在运行时,首先要读取编译后的class文件,由于我们在编写源码时会定义和使用各种结构和对象,那么在进行加载时,JVM会将分配得到的内存划分为多个区域。通常我们会粗浅地将内存划分为“栈”和"堆"两个区域,但是对于Java虚拟机来说我们应该进一步剖析

由JVM创建的不同区域,有些会随着虚拟机启动而创建,随着虚拟机退出而销毁,如:方法区(Method)、Java堆。还有一些是与线程一一对应的,会随着线程开始和结束而被创建和销毁,如:PC寄存器、Java虚拟机栈、本地方法栈。

1. PC寄存器

虽然Java虚拟机支持多线程同时执行,但是在任意时刻,一条JVM线程只会执行一个方法的代码,这个正在被线程执行的方法称为该线程的当前方法,对应的Java虚拟机栈被称为当前栈帧

PC寄存器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器,每一条JVM线程都有自己的PC寄存器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个寄存器来完成。

如果当前方法不是native的,那PC寄存器就保存Java虚拟机正在执行的字节码指令的地址,如果该方法是native的,则PC寄存器的值是undefined。(关于native的说明见:3.本地方法栈

2. Java虚拟机栈

每一条JVM线程都有自己私有的Java虚拟机栈,与线程同时创建,用于存储栈帧,其中包含局部变量和一些尚未算好的结果。另外,需要注意的是:栈(Stack)、Heap(堆)、Java虚拟机栈(Java VM Stack)、Java堆(Java Heap)的概念是不同的,Java虚拟机本身也是一个由其他语言编写运行的软件,所以本文只讨论JVM所管理的内存区域,并不探讨各区域在堆栈中的分布。

Java虚拟机规范既允许Java虚拟机栈被实现为固定大小,也允许根据计算动态来扩展和收缩。Java虚拟机栈描述的是Java方法执行的线程的内存模型:每个方法被执行的时候,Java虚拟机都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接等信息,每一个方法从被调用,到执行完毕的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

当调用新的方法时,新的栈帧会随之创建,程序的控制权也会进行移交给调用的新方法,成为新的当前栈帧。在方法执行完毕进行返回时,当前栈帧会传回此方法的执行结果给前一个栈帧(调用这个新方法的栈帧),然后虚拟机就会丢弃当前栈帧,前一个栈帧成为当前栈帧,大家可以用这篇文章来理解一下这个过程:Java方法的嵌套与递归调用

  • 局部变量表

每个栈帧内部都包含一组被称为局部变量表的变量列表,长度在编译期时被确定。一个局部变量可以保存一个类型为boolean、byte、char、short、int、float、reference或returnAddress的数据。两个局部变量可以保存一个类型为long或者double的数据。

局部变量使用索引来进行定位访问,首个局部变量的索引值为0,最大值小于局部变量表的长度。对于long和double,由于占用了两个连续的局部变量,则采用局部变量中较小的索引值来定位。

  • 操作数栈

每个栈帧内部都包含一个被称为操作数栈的后进先出栈,操作数栈的最大深度在编译器被确定,一般的操作数栈指的就是“当前栈帧的操作数栈”。

在栈帧刚刚创建时,操作数栈是空的。JVM提供一些字节码指令来从局部变量表或对象实例的字段中复制常量或变量的值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据以及把操作结果重新入栈。在调用方法时,操作数栈也用来准备调用方法的参数以及接收方法返回结果。

在任意时刻,操作数栈都会有一个确定的栈深度,一个long或者double类型的数据会占用两个单位的栈深度,其他数据类型会占用一个单位的栈深度。

  • 动态链接

每个栈帧内部都包含一个指向当前方法所在类型的运行时常量池的引用,来对当前方法的代码实现动态链接。在class文件里面,一个方法如果要调用其他方法,或者访问成员变量,需要通过符号引用来表示,动态链接的作用就是将这些符号引用所表示的方法转换为对实际方法的直接引用。

3. 本地方法栈

由于有时可能需要调用其他语言(如C语言)所编写的方法,就需要使用到传统的栈(C stack)来支持native方法的执行。native在Java语言中是一个修饰符,如果一个方法被native修饰,那么就代表这个方法是一个java调用非java代码的接口。在定义一个native method时,不需要指定方法体,与声明接口中的方法类似,具体的方法实现会在dll或其他库文件中,在运行时需要一并加载。

本地方法栈与Java虚拟机栈的作用十分相似,区别就在于Java虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机调用的本地方法服务。

4. Java堆

Java堆是JVM所管理的内存中最大的一块区域,并且是被所有线程共享的一块内存区域,在虚拟机启动时被创建。Java堆中主要存储的就是对象的实例,包括数组类型的实例。

Java堆中所存储的对象由自动内存管理系统,也就是垃圾收集器进行管理,不需要手动进行销毁和释放。另外,Java堆所对应的区域不需要连续。

5. 方法区

方法区与Java堆一样,是一块各个线程共享的内存区域,用于存储已被虚拟机加载的类的结构信息,包括运行时常量池、构造函数和普通方法、静态变量等数据。

方法区在虚拟机启动的时候被创建,虽然逻辑上方法区属于堆的一部分,但是也可以选择在这个区域不实现垃圾收集与压缩。

原文引述《Java虚拟机规范(Java SE 8版)》中的内容

这个版本的Java虚拟机规范也不限定实现方法区的内存位置和编译代码的管理策略。方法区的容量可以是固定的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。方法区在实际内存空间中可以是不连续的。

原文引述《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》中的内容

在JDK 6的时候HotSpot开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划了,到了JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了JDK 8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Metaspace)来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。

引述这两段话的原因在于不少初学者都在纠结很多类中定义的结构到底存储在什么位置的问题,笔者在这里帮助大家再次明确一下:

  • 不同版本的JVM有对方法区的管理方式并不相同
  • 有多种Java虚拟机都可以运行Java程序,对各区域的管理也有差别
  • HotSpot VM与JRockit VM已在Oracle JDK8版本完成合并

所以当我们在进行探讨时一定要明确具体的虚拟机和JDK版本,方法区本身是有JVM分配管理的区域之一,从上面的叙述中我们已经知道,对于Oracle JDK8版本,已经不再使用永久代来实现方法区,方法区中的内容全部移动存储至本地内存的元空间中

6. 运行时常量池

首先强调:运行时常量池并不等同于常量池! 运行时常量池是方法区的一部分,是class文件中每一个类或接口的常量池表的运行时表示,包括了若干种不同的常量:在编译期可知的数值字面量以及在运行期解析后才能获得的方法或字段引用。

在Java虚拟机加载类和接口后,就会创建对应的运行时常量池。Java虚拟机为每个类型都维护着一个常量池,是Java虚拟机中的运行时数据结构。运行时常量池中的所有引用最初都是符号引用,对于不同的结构(类、接口、数组等)的符号引用,也会有相应的规范和格式。

由于运行时常量池的符号引用较为复杂,同时对常量池的解释涉及到类的加载机制,所以在本文中不再赘述,后续将在其他文章中说明(包括字符串常量的规定策略)。

二、常见结构存储位置

在阅读了前面的内容后,我们可以记一波结论了,了解一些底层的东西有利于我们去解释或记忆某些用法和特点。

1. 普通成员变量

普通的成员变量由于是创建对象后才能使用的,所以基本数据类型的值或引用(与成员变量类型无关)都存放在对应的实例空间,在Java堆中。

2. 静态成员变量与静态代码块

静态成员变量与静态代码块都是直接在类下使用static声明的结构,存储在方法区中。

3. 构造方法和动态代码块

构造方法也是类似于方法的一种结构,被new调用时才会执行,而动态代码块被编译后会出现在构造函数中,它们都存储在方法区中。

  • 编译前
public class Person{
    {
        System.out.println("init");
    }
    public Person(){
        System.out.println("default");
    }
    public Person(int a){
        System.out.println("another");
    }
}
  • 编译后

4. 普通方法与静态方法

普通方法和静态方法虽然在调用和使用时有所区别,但本质上都是方法结构,对于一个类来说只要加载一次就可以了,也存放在方法区中。

5. 方法局部变量

在方法中定义的变量,由于有局部变量表的存在,基本数据类型直接存放在JVM栈中,对于引用类型的变量,在JVM栈中只存放引用(reference),而对应的实例存放在Java堆中。

目录
相关文章
|
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)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
63 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
24天前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
46 10
|
24天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
1月前
|
存储 监控 算法
JVM调优深度剖析:内存模型、垃圾收集、工具与实战
【10月更文挑战第9天】在Java开发领域,Java虚拟机(JVM)的性能调优是构建高性能、高并发系统不可或缺的一部分。作为一名资深架构师,深入理解JVM的内存模型、垃圾收集机制、调优工具及其实现原理,对于提升系统的整体性能和稳定性至关重要。本文将深入探讨这些内容,并提供针对单机几十万并发系统的JVM调优策略和Java代码示例。
49 2
|
1月前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
371 0
|
23天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
47 1
|
27天前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。