1 内存布局总体结构

根据 JVM 规范,JVM 内存共分为虚拟机栈(Virtual Machine Stacks)、堆(Heap)、方法区(Method Area)、程序计数器(Program Counter Registers)、本地方法栈(Native Method Stacks)五个部分。

  • Java 8 之前在堆(Heap)中除了年轻代(YongGen)、老年代(OldGen)之外还存在一个永久代(PremGen)

    • 永久代存放:类的元数据、静态变量和常量

    • 方法区(Method Area)存在于永久代之中

    • 运行时常量池(Runtime Constant Pool)存在于方法区(Method Area)中

  • Java 8 及之后的版本,彻底移除了持久代(PermGen),而使用 元空间(Metaspace) 来进行替代

    • 永久代中的 类元信息(class metadata) 转移到了 本地内存(Native Memory) 而不是虚拟机

    • 永久代中的 字符串常量池(interned Strings)类静态变量(class static variables) 转移到了堆( Heap)中

    • 永久代参数(PermSize 与 MaxPermSize)失效,替换为元空间参数(MetaspaceSize 与 MaxMetaspaceSize)

  • Java 8 为什么要将永久代替换成Metaspace?

    • 字符串存在永久代中,容易出现性能问题和内存溢出。

    • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

    • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

    • Oracle 可能会将 HotSpot 与 JRockit 合二为一,JRockit 没有所谓的永久代。

  • 废除永久代的好处

    • 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间

    • 将运行时常量池从 PermGen 分离出来,与类的元数据分开,提升类元数据的独立性。

    • 将元数据从 PermGen 剥离出来到 Metaspace,可以提升对元数据的管理同时提升GC效率。

2 程序计数器(Program Counter Register)

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

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

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

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

  • 程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个

3 Java 虚拟机栈(Java Virtual MachineStacks)

Java虚拟机栈(Java Virtual Machine Stacks)是线程私有的,生命周期和线程相同。

Java虚拟机栈和线程同时创建,用于存储栈帧(Stack Frame)。每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

栈里的每条数据,就是栈帧。在每个 Java 方法被调用的时候,都会创建一个栈帧,并入栈。一旦完成相应的调用,则出栈。所有的栈帧都出栈后,线程也就结束了。每个栈帧,都包含四个区域:

  • 局部变量表(Local Variable Table):用于存放方法参数和方法内定义的局部变量。 包括8种基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。

  • 操作数栈(Operand Stack):是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。

  • 动态连接(Dynamic Linking):将符号引用转换成直接引用。

  • 返回地址(Return Address):方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

在Java虚拟机规范中,对这个区域规定了两种异常状况:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;

  • 如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

4 本地方法栈(Native Method Stack)

基本功能与虚拟机栈非常相似,服务的对象是 native 方法。本地方法栈也是线程私有的,它的生命周期与线程相同,每个线程都有一个。

在 HotSpot 虚拟机中直接就把本地方法栈和虚拟机栈合二为一。

会和 Java 虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

5 堆(Heap)与 元空间(Metaspace)

堆是什么:

  • 在虚拟机启动的时候创建。

  • 堆中的数据是线程所共享的,目的就是存放对象实例。

  • 堆是 垃圾收集器管理 的主要区域。

  • 堆是虚拟机所管理的内存中最大的一块,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代(JDK 1.7以及之前还存在永久代);新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间,默认占比 8:1:1。

  • 堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(可以通过 -Xms-Xmx 控制)。

  • 方法结束后,堆中对象不会马上移出仅仅在垃圾回收的时候时候才移除。

  • 如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

另外:从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local AllocationBuffer,TLAB)。

  • Yong Gen:1个Eden Space和2个Suvivor Space(from 和to)。主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。

  • Old Gen(Tenured Gen): 主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。

  • 默认 -XX:NewRatio=2 , 标识新生代占1 , 老年代占2 ,新生代占整个堆的1/3

  • 默认 -XX:SurvivorRatio=6,标识Eden 空间:From Survivor空间:To Survivor空间 = 8:1:1

对象分配内存的工作流程图

GC相关概念

  • form survivor 又称 s0

  • to survivor 又称 s1

  • 部分收集:Partial GC

    • 新生代收集:Minor GC / Young GC

      • 年轻代空间不足触发, 这里年轻代指的是Eden满。Survivor满不会引发GC
    • 老年代收集:Major GC / Old GC

      • 老年代空间不足时,会尝试触发MinorGC. 如果空间还是不足,则触发Major GC,如果Major GC , 内存仍然不足,则报错OOM
  • 混合收集:Mixed GC

    • G1垃圾回收器会混合回收, region 区域回收
  • 整堆收集:Full GC

    • 用System.gc() , 系统会执行Full GC ,不是立即执行

    • 老年代空间不足时触发

    • 方法区空间不足时触发

关于元空间(Metaspace)的单独说明

  • 在 JDK1.7 之前,HotSpot 虚拟机把方法区当成永久代来进行垃圾回收

  • 从 JDK1.8 开始,HotSpot 虚拟机移除永久代,并把方法区移至元空间

  • 永久代与元空间的区别

    • 永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存

    • 在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。现在类的元信息存储在元空间中,静态变量和常量池等并入堆中。

6 方法区(Method Area)与 运行时常量池(Runtime Constant Pool)

在 HotSpot 虚拟机上 GC 分代收集扩展至方法区,使用了永久代来实现方法区(JDK1.7及以前,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间)。

元空间、永久代是方法区具体的落地实现。方法区看作是一块独立于Java堆的内存空间,它主要是用来存储所加载的类信息的。方法区是一个规范,只不过取代永久代的是元空间(Metaspace)。

  • 与堆类似,方法区是被各个线程共享的内存区域

  • 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

类加载器将Class文件加载到内存之后,将类的信息存储到方法区中

  • 类信息:类全名、直接父类的全名、修饰符、实现的接口列表

  • 类的属性信息:名称、类型、修饰符

  • 类的方法信息:返回类型、参数数量和类型、修饰符、字节码bytecodes、操作数栈、局部变量表及大小(abstract和native方法除外)、异常表

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

  • 静态常量池:存放编译期间生成的各种字面量与符号引用。在字节码文件中即 .class 文件。

  • 运行时常量池:常量池表在运行时的表现形式。在方法区。

  • 编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过ClassLoader将字节码文件的常量池中的信息加载到内存中,存储在了方法区的运行时常量池中。

7 直接内存(Direct Memory)

直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分。

  • 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显

  • 直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显

在JDK 1.4中新加入了NIO(New Input/Output) 类, 引入了一种基于通道(Channel) 与缓冲区 (Buffer)的 I/O 方法,它可以使用Native函数库直接分配堆外内存, 然后通过一个存储在Java堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。避免了 在Java堆和Native堆中来回复制数据。

直接内存的大小并不受到 JVM 堆大小的限制,甚至不受到 JVM 进程内存大小的限制。它只受限于本机总内存(RAM 及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。

DirectBuffer并没有真正向OS申请分配内存,其最终还是通过调用 Unsafe 的 allocateMemory() 来进行内存分配。不过 JVM 对 Direct Memory 可申请的大小也有限制,可用 -XX:MaxDirectMemorySize=1M 设置,这部分内存不受JVM垃圾回收管理。

内容为之前学习笔记整理,如果有问题请指正!

JVM简明笔记2:运行时数据区的更多相关文章

  1. 【JVM第三篇--运行时数据区】程序计数器、虚拟机栈、本地方法栈

    写在前面的话:本文是在观看尚硅谷JVM教程后,整理的学习笔记.其观看地址如下:尚硅谷2020最新版宋红康JVM教程 一.运行时数据区 我们在编写Java程序时,使用JVM的流程主要如下所示: 虚拟机在 ...

  2. JVM 专题十:运行时数据区(五)堆

    1. 核心概述 1.1 堆概述 一个进程对应一个jvm实例,一个运行时数据区,又包含多个线程,这些线程共享了方法区和堆,每个线程包含了程序计数器.本地方法栈和虚拟机栈. 一个jvm实例只存在一个堆内存 ...

  3. Jvm基础(1)-Java运行时数据区

    最近在看<深入理解Java虚拟机>,里面讲到了Java运行时数据区,这是Jvm基本知识,把读书笔记记录在此.这些知识属于常识,都能查到的,如果我有理解不对的地方,还请指出. 首先把图贴上来 ...

  4. 【JVM第四篇--运行时数据区】堆

    写在前面的话:本文是在观看尚硅谷JVM教程后,整理的学习笔记.其观看地址如下:尚硅谷2020最新版宋红康JVM教程 一.堆的概述 JVM的运行时数据区如下: 一个Java程序运行起来对应着一个进程(操 ...

  5. JVM内存区域(运行时数据区)划分

    前言: 我们每天都在编写Java代码,编译,执行.很多人已经知道Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文 ...

  6. JVM系列之四:运行时数据区

    1. JVM架构图 Java虚拟机主要分为五大模块:类装载器子系统.运行时数据区.执行引擎.本地方法接口和垃圾收集模块. 2. JDK1.7内存模型-运行时数据区域 根据<Java 虚拟机规范( ...

  7. JVM 专题十三:运行时数据区(八)直接内存

    1. 直接内存 不是虚拟机运行时数据区的一部分,也不是<Java虚拟机规范>中定义的内存区域. 直接内存是Java堆外的.直接向系统申请的内存区间. 来源于NIO,通过存在堆中的Direc ...

  8. JVM 专题十一:运行时数据区(六)方法区

    1. 栈.堆.方法区关系交互 运行时数据区结构图: 从线程共享与否的角度来看: 2. 方法区的理解 2.1 方法区在哪里? <Java虚拟机规范>中明确说明:“尽管所有的方法区在逻辑上属于 ...

  9. JVM 专题九:运行时数据区(四)本地方法栈

    1. 本地方法栈 2. 什么是本地方法栈? Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用   本地方法栈,也是线程私有的. 允许被实现成固定或者是可动态拓展的内存大小 ...

  10. JVM 专题八:运行时数据区(三)虚拟机栈

    2.虚拟机栈 1. 概述 1.1 虚拟机栈出现背景 由于跨平台性的设计,java的指令都是根据栈来设计的.不同平台CPU架构不同,所以不能设计为基于寄存器的. 优点是跨平台,指令集小,编译器容易实现, ...

随机推荐

  1. select 对当前选项显示文本的获取 m.options[m.selectedIndex].text | selectz

    select 对当前选项显示文本的获取 m.options[m.selectedIndex].text | selectz <html> <head> <title> ...

  2. 单词本z develop vel = 到上面 从下面到上面的一种过程 抽象是相对从无到有

    单词本z develop vel = 到上面 从下面到上面的一种过程 抽象是相对从无到有 develop 发展 开发 de = down 下面 velop 这里 vel 就是 lev的反写 op = ...

  3. C#实现一个简单的日志类

    目录 自定义日志类 NLog版本的日志类 Serilog版本的日志类 上个月换工作,新项目又要重新搭建基础框架,把日志实现部分单独记录下来方便以后参考. 自定义日志类 代码大部分使用ChatGPT生成 ...

  4. python的软连接的操作方法

    详细:切换python的版本 cd /usr/bin/ ls -l python* sudo rm -rf python sudo ln -s /usr/bin/python3.7 /usr/bin/ ...

  5. 反转链表——java

    给定一个链表,请你将链表反转过来. 举例:原链表:1→2→3→4→5→null 反转链表:5→4→3→2→1→null 代码: package algorithm_niuke; public clas ...

  6. 【2311. 小于等于 K 的最长二进制子序列】贪心

    class Solution { public static void main(String[] args) { Solution solution = new Solution(); System ...

  7. 虚幻引擎UE4如何实现打包后播放片头?其实超简单!

    虚幻引擎作为一款全球性的3D实时开发工具,不仅在游戏行业,其在建筑.影视.医疗等行业也被广泛使用.作为开发人员,有时开发的UE虚幻引擎项目比较大,开始运行项目时需要等待较长的时间,还有些公司要求添加片 ...

  8. 首届实时渲染3D动画创作大赛最佳人气奖?你说了算!

    根据评选标准,经过评委组层层选拔,首届实时渲染3D动画创作大赛「最佳人气奖」投票开始啦!!! 本次赛事报名人数达212人,入围作品共40份,其中Omniverse组11份,专业组15份,学生组14份. ...

  9. Python实践:基于Matplotlib实现某产品全年销量数据可视化

    本文分享自华为云社区<画图实战-Python实现某产品全年销量数据多种样式可视化>,作者:虫无涯. 学习心得 有时候我们需要对某些数据进行分析,得到一些可视化效果图,而这些效果图可以直观展 ...

  10. Flutter Utils 全网最齐全的工具类

    FlutterUtils 目录介绍 01.事件通知bus工具类 02.颜色Color工具类 03.日期转化工具类 04.File文件工具类 05.Sql数据库工具类 06.Json转化工具类 07.L ...