JVM内存区域划分-JVM内存布局-《Java笔记》

admin 2025-10-19 03:41:14 编程 来源:ZONE.CI 全球网 0 阅读模式

JVM 内存布局

概念

内存是非常重要的系统资源,是硬盘和 CPU 的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM 内存布局规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行。上图描述了当前比较经典的%20JVM%20内存布局。(堆区画小了%202333,按理来说应该是最大的区域)如果按照线程是否共享来分类的话,如下图所示:PS:线程是否共享这点,实际上理解了每块区域的实际用处之后,就很自然而然的就记住了。不需要死记硬背。下面让来了解下各个区域。


Heap%20(堆区)

1.%20堆区的介绍

我们先来说堆。堆是%20OOM%20故障最主要的发生区域。它是内存区域中最大的一块区域,被所有线程共享,存储着几乎所有的实例对象、数组。所有的对象实例以及数组都要在堆上分配,但是随着%20JIT%20编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。Java%20堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC%20堆”。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以%20Java%20堆中还可以细分为:新生代和老年代。再细致一点的有%20Eden%20空间、From%20Survivor%20空间、To%20Survivor%20空间等。从内存分配的角度来看,线程共享的%20Java%20堆中可能划分出多个线程私有的分配缓冲区(Thread%20Local%20Allocation%20Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。

2.%20堆区的调整

根据%20Java%20虚拟机规范的规定,Java%20堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以在运行时动态地调整。如何调整呢?通过设置如下参数,可以设定堆区的初始值和最大值,比如%20-Xms256M%20-Xmx%201024M,其中%20-X%20这个字母代表它是%20JVM%20运行时参数,ms%20是%20memory%20start%20的简称,中文意思就是内存初始值,mx%20是%20memory%20max%20的简称,意思就是最大内存。值得注意的是,在通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,会形成不必要的系统压力所以在线上生产环境中%20JVM%20的%20Xms%20和%20Xmx%20会设置成同样大小,避免在%20GC%20后调整堆大小时带来的额外压力。

3.%20堆的默认空间分配

另外,再强调一下堆空间内存分配的大体情况。先来看虚拟机的默认配置。命令行上执行如下命令,就可以查看当前%20JDK%20版本所有默认的%20JVM%20参数。

java%20-XX:+PrintFlagsFinal%20-version

输出

对应的输出应该有几百行,这里去看和堆内存分配相关的两个参数

>java%20-XX:+PrintFlagsFinal%20-version[Global%20flags]%20%20%20%20...%20%20%20%20uintx%20InitialSurvivorRatio%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20=%208%20%20%20%20uintx%20NewRatio%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20=%202%20%20%20%20...java%20version%20"1.8.0_131"Java(TM)%20SE%20Runtime%20Environment%20(build%201.8.0_131-b11)Java%20HotSpot(TM)%2064-Bit%20Server%20VM%20(build%2025.131-b11,%20mixed%20mode)

参数解释

因为新生代是由%20Eden%20+%20S0%20+%20S1%20组成的,所以按照上述默认比例,如果%20eden%20区内存大小是%2040M,那么两个%20survivor%20区就是%205M,整个%20young%20区就是%2050M,然后可以算出%20Old%20区内存大小是%20100M,堆区总大小就是%20150M。

4.%20堆溢出演示/**%20*%20VM%20Args:-Xms10m%20-Xmx10m%20-XX:+HeapDumpOnOutOfMemoryError%20*/public%20class%20HeapOOMTest%20{%20%20%20%20public%20static%20final%20int%20_1MB%20=%201024%20*%201024;%20%20%20%20public%20static%20void%20main(String[]%20args)%20{%20%20%20%20%20%20%20%20List<byte[]>%20byteList%20=%20new%20ArrayList<>(10);%20%20%20%20%20%20%20%20for%20(int%20i%20=%200;%20i%20<%2010;%20i++)%20{%20%20%20%20%20%20%20%20%20%20%20%20byte[]%20bytes%20=%20new%20byte[2%20*%20_1MB];%20%20%20%20%20%20%20%20%20%20%20%20byteList.add(bytes);%20%20%20%20%20%20%20%20}%20%20%20%20}}

输出-XX:+HeapDumpOnOutOfMemoryError%20可以让%20JVM%20在遇到%20OOM%20异常时,输出堆内信息,特别是对相隔数月才出现的%20OOM%20异常尤为重要。java.lang.OutOfMemoryError:%20Java%20heap%20spaceDumping%20heap%20to%20java_pid32372.hprof%20...Heap%20dump%20file%20created%20[7774077%20bytes%20in%200.009%20secs]Exception%20in%20thread%20"main"%20java.lang.OutOfMemoryError:%20Java%20heap%20space%20%20%20%20at%20jvm.HeapOOMTest.main(HeapOOMTest.java:18)

创建一个新对象内存分配流程

看完上面对堆的介绍,我们趁热打铁再学习一下%20JVM%20创建一个新对象的内存分配流程。绝大部分对象在%20Eden%20区生成,当%20Eden%20区装填满的时候,会触发%20Young%20Garbage%20Collection,即%20YGC。垃圾回收的时候,在%20Eden%20区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到%20Survivor%20区。Survivor%20区分为%20so%20和%20s1%20两块内存空间。每次%20YGC%20的时候,它们将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。如果%20YGC%20要移送的对象大于%20Survivor%20区容量的上限,则直接移交给老年代。一个对象也不可能永远呆在新生代,就像人到了%2018%20岁就会成年一样,在%20JVM%20中%20-XX:MaxTenuringThreshold%20参数就是来配置一个对象从新生代晋升到老年代的阈值。默认值是%2015,可以在%20Survivor%20区交换%2014%20次之后,晋升至老年代。上述涉及到一部分垃圾回收的名词,不熟悉的读者可以查阅资料或者看下本系列的垃圾回收章节。


Metaspace%20元空间在%20HotSpot%20JVM%20中,永久代(%20≈%20方法区)中用于存放类和方法的元数据以及常量池,比如%20Class%20和%20Method。每当一个类初次被加载的时候,它的元数据都会放到永久代中。永久代是有大小限制的,因此如果加载的类太多,很有可能导致永久代内存溢出,即万恶的%20java.lang.OutOfMemoryError:%20PermGen,为此不得不对虚拟机做调优。那么,Java%208%20中%20PermGen%20为什么被移出%20HotSpot%20JVM%20了?(详见:JEP%20122:%20Remove%20the%20Permanent%20Generation):

  1. 由于%20PermGen%20内存经常会溢出,引发恼人的%20java.lang.OutOfMemoryError:%20PermGen,因此%20JVM%20的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的%20OOM
  2. 移除%20PermGen%20可以促进%20HotSpot%20JVM%20与%20JRockit%20VM%20的融合,因为%20JRockit%20没有永久代。

根据上面的各种原因,PermGen%20最终被移除,方法区移至%20Metaspace,字符串常量池移至堆区。准确来说,Perm%20区中的字符串常量池被移到了堆内存中是在%20Java7%20之后,Java%208%20时,PermGen%20被元空间代替,其他内容比如类元信息、字段、静态属性、方法、常量等都移动到元空间区。比如%20java/lang/Object%20类元信息、静态属性%20System.out、整形常量%20100000%20等。元空间的本质和永久代类似,都是对%20JVM%20规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。(和后面提到的直接内存一样,都是使用本地内存)In%20JDK%208,%20classes%20metadata%20is%20now%20stored%20in%20the%20native%20heap%20and%20this%20space%20is%20called%20Metaspace.对应的%20JVM%20调参:


Java%20虚拟机栈

对于每一个线程,JVM%20都会在线程被创建的时候,创建一个单独的栈。也就是说虚拟机栈的生命周期和线程是一致,并且是线程私有的。除了%20Native%20方法以外,Java%20方法都是通过%20Java%20虚拟机栈来实现调用和执行过程的(需要程序技术器、堆、元空间内数据的配合)。所以%20Java%20虚拟机栈是虚拟机执行引擎的核心之一。而%20Java%20虚拟机栈中出栈入栈的元素就称为「栈帧」。栈帧(Stack%20Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。栈对应线程,栈帧对应方法在活动线程中,%20只有位于栈顶的帧才是有效的,%20称为当前栈帧。正在执行的方法称为当前方法。在执行引擎运行时,%20所有指令都只能针对当前栈帧进行操作。而%20StackOverflowError%20表示请求的栈溢出,%20导致内存耗尽,%20通常出现在递归方法中。虚拟机栈通过%20pop%20和%20push%20的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上。在执行的过程中,如果出现了异常,会进行异常回溯,返回地址通过异常处理表确定。可以看出栈帧在整个%20JVM%20体系中的地位颇高。下面也具体介绍一下栈帧中的存储信息。

1. 局部变量表

局部变量表就是存放方法参数和方法内部定义的局部变量的区域。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。这里直接上代码,更好理解。

  1. public int test(int a, int b) {
  2. Object obj = new Object();
  3. return a + b;
  4. }

如果局部变量是 Java 的 8 种基本基本数据类型,则存在局部变量表中,如果是引用类型。如 new 出来的 String,局部变量表中存的是引用,而实例在堆中。

2.%20操作栈

操作数栈(Operand%20Stack)看名字可以知道是一个栈结构。Java%20虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。当%20JVM%20为方法创建栈帧的时候,在栈帧中为方法创建一个操作数栈,保证方法内指令可以完成工作。还是用实操理解一下。

public%20class%20OperandStackTest%20{%20%20%20%20public%20int%20sum(int%20a,%20int%20b)%20{%20%20%20%20%20%20%20%20return%20a%20+%20b;%20%20%20%20}}

编译生成%20.class%20文件之后,再反汇编查看汇编指令

>%20javac%20OperandStackTest.java>%20javap%20-v%20OperandStackTest.class%20>%201.txt%20public%20int%20sum(int,%20int);%20%20%20%20descriptor:%20(II)I%20%20%20%20flags:%20ACC_PUBLIC%20%20%20%20Code:%20%20%20%20%20%20stack=2,%20locals=3,%20args_size=3%20//%20最大栈深度为2%20局部变量个数为3%20%20%20%20%20%20%20%20%200:%20iload_1%20//%20局部变量1%20压栈%20%20%20%20%20%20%20%20%201:%20iload_2%20//%20局部变量2%20压栈%20%20%20%20%20%20%20%20%202:%20iadd%20%20%20%20//%20栈顶两个元素相加,计算结果压栈%20%20%20%20%20%20%20%20%203:%20ireturn%20%20%20%20%20%20LineNumberTable:%20%20%20%20%20%20%20%20line%2010:%200

3.%20动态连接

每个栈帧中包含一个在常量池中对当前方法的引用,%20目的是支持方法调用过程的动态连接。

4.%20方法返回地址

方法执行时有两种退出情况:

  • 正常退出,即正常执行到任何方法的返回字节码指令,如%20RETURNIRETURNARETURN%20等
  • 异常退出

无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:

  • 返回值压入上层调用栈帧
  • 异常信息抛给能够处理的栈帧
  • PC%20计数器指向方法调用后的下一条指令

本地方法栈

本地方法栈(Native%20Method%20Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行%20Java%20方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的%20Native%20方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如%20Sun%20HotSpot%20虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出%20StackOverflowError%20和%20OutOfMemoryError%20异常。


程序计数器

程序计数器(Program%20Counter%20Register)是一块较小的内存空间。是线程私有的。它可以看作是当前线程所执行的字节码的行号指示器。什么意思呢?白话版本:因为代码是在线程中运行的,线程有可能被挂起。即%20CPU%20一会执行线程%20A,线程%20A%20还没有执行完被挂起了,接着执行线程%20B,最后又来执行线程%20A%20了,CPU%20得知道执行线程A的哪一部分指令,线程计数器会告诉%20CPU。由于%20Java%20虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,CPU%20只有把数据装载到寄存器才能够运行。寄存器存储指令相关的现场信息,由于%20CPU%20时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等,线程执行或恢复都要依赖程序计数器。此区域也不会发生内存溢出异常。


直接内存

直接内存(Direct%20Memory)并不是虚拟机运行时数据区的一部分,也不是%20Java%20虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致%20OutOfMemoryError%20异常出现,所以我们放到这里一起讲解。在%20JDK%201.4%20中新加入了%20NIO(New%20Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的%20I/O%20方式,它可以使用%20Native%20函数库直接分配堆外内存,然后通过一个存储在%20Java%20堆中的%20DirectByteBuffer%20对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在%20Java%20堆和%20Native%20堆中来回复制数据。显然,本机直接内存的分配不会受到%20Java%20堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括%20RAM%20以及%20SWAP%20区或者分页文件)大小以及处理器寻址空间的限制。如果内存区域总和大于物理内存的限制,也会出现%20OOM。


Code%20Cache

简而言之,%20JVM%20代码缓存是%20JVM%20将其字节码存储为本机代码的区域%20。我们将可执行本机代码的每个块称为%20nmethod。该%20nmethod%20可能是一个完整的或内联%20Java%20方法。实时(JIT)编译器是代码缓存区域的最大消费者。这就是为什么一些开发人员将此内存称为%20JIT%20代码缓存的原因。这部分代码所占用的内存空间成为%20CodeCache%20区域。一般情况下我们是不会关心这部分区域的且大部分开发人员对这块区域也不熟悉。如果这块区域%20OOM%20了,在日志里面就会看到:java.lang.OutOfMemoryError%20code%20cache。

诊断选项

以太坊cppgolang区别 编程

以太坊cppgolang区别

以太坊是一种去中心化的开源平台,它采用智能合约技术,旨在构建和运行不受干扰的分布式应用程序。作为目前最受欢迎的区块链平台之一,以太坊提供了多种编程语言的支持,其
progolang 编程

progolang

Go语言(Golang)是由Google开发的一门静态类型编程语言。作为一名专业的Golang开发者,我深知这门语言的优势和特点。在本文中,我将介绍Golang
golangn个发送者 编程

golangn个发送者

Golang是一种开源的编程语言,由Google团队开发,旨在提高程序的并发性和简化软件开发过程。在Go语言中,有时需要向多个接收者发送信息。本文将介绍如何在G
golang技能图谱 编程

golang技能图谱

从互联网行业的快速发展到人工智能技术的日益成熟,各种编程语言也应运而生。而在这众多的编程语言中,Golang(即Go)作为一门强大且高效的开发语言备受关注。Go
评论:0   参与:  0