图解带你掌握`JVM`运行时核心内存区
摘要:堆空间差不多是最大的内存空间,也是运行时数据区最重要的内存空间。堆可以处于物理上不连续的内存空间,但在逻辑上它应该被视为连续的。
本文分享自华为云社区《醒酒菜:动画图解核心内存区--堆》,作者: 阿Q说代码。
堆的概述
一般来说:
- 一个Java程序的运行对应一个进程;
- 一个进程对应着一个JVM实例(JVM的启动由引导类加载器加载启动),同时也对应着多个线程;
- 一个JVM实例拥有一个运行时数据区(Runtime类,为饿汉式单例类);
- 一个运行时数据区中的堆和方法区是多线程共享的,而本地方法栈、虚拟机栈、程序计数器是线程私有的。
堆空间差不多是最大的内存空间,也是运行时数据区最重要的内存空间。堆可以处于物理上不连续的内存空间,但在逻辑上它应该被视为连续的。
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
堆内存大小设置
堆一旦被创建,它的大小也就确定了,初始内存默认为电脑物理内存大小的1/64,最大内存默认为电脑物理内存的1/4,但是堆空间的大小是可以调节,接下来我们来演示一下。
准备工具
JDK自带内存分析的工具:在已安装JDK的bin目录下找到jvisualvm.exe。打开该软件,下载插件Visual GC,一定要点击检查最新版本,否则会导致安装失败。

安装完重启jvisualvm

代码样例
public class HeapDemo {
public static void main(String[] args) {
System.out.println("start...");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end...");
}
}
IDEA设置


- -Xms10m用于表示堆区的起始内存为10m,等价于-XX:InitialHeapSize;
- -Xmx10m用于表示堆区的最大内存为10m,等价于-XX:MaxHeapSize;
- 其中-X是JVM的运行参数,ms是memory start
通常会将-Xms和-Xmx两个参数配置相同的值,其目的就是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
启动程序
启动程序之后去jvisualvm查看

一旦堆区中的内存大小超过-Xmx所指定的最大内存时,将会抛出OOM(Out Of MemoryError)异常。
堆的分代
存储在JVM中的java对象可以被划分为两类:
- 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速;
- 另一类是生命周期非常长,在某些情况下还能与JVM的生命周期保持一致;
堆区分代
经研究表明70%-99%的对象属于临时对象,为了提高GC的性能,Hotspot虚拟机又将堆区进行了进一步划分。

如图所示,堆区又分为年轻代(YoungGen)和老年代(OldGen);其中年轻代又分为伊甸园区(Eden)和幸存者区(Survivor);幸存者区分为幸存者0区(Survivor0,S0)和幸存者1区(Survivor1,S1),有时也叫from区和to区。
分代完成之后,GC时主要检测新生代Eden区。
统一概念:
新生区<=>新生代<=>年轻代
养老区<=>老年区<=>老年代
几乎所有的Java对象都是在Eden区被new出来的,有的大对象在该区存不下可直接进入老年代。绝大部分的Java对象都销毁在新生代了(IBM公司的专门研究表明,新生代80%的对象都是“朝生夕死”的)。
新生代与老年代在堆结构的占比
- 默认参数-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3;
- 可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5;
该参数在开发中一般不会调整,如果生命周期长的对象偏多时可以选择调整。
Eden与Survivor在堆结构的占比
在HotSpot中,Eden空间和另外两个Survivor空间所占的比例是8:1:1(测试的时候是6:1:1),开发人员可以通过选项-XX:SurvivorRatio调整空间比例,如-XX:SurvivorRatio=8
可以在cmd中通过jps 查询进程号-> jinfo -flag NewRatio(SurvivorRatio) + 进程号 查询配置信息
-Xmn设置新生代最大内存大小(默认就好),如果既设置了该参数,又设置了NewRatio的值,则以该参数设置为准。
查看设置的参数
以上边的代码为例:设置启动参数-XX:+PrintGCDetails;可在cmd窗口中输入jps查询进程号,然后通过jstat -gc 进程id指令查看进程的内存使用情况。

图解对象分配过程
对象分配过程

- new的对象先放伊甸园区,此区有大小限制;
- 当伊甸园的空间填满时,程序继续创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC,也叫YGC):将伊甸园区中的不再被其他对象所引用的对象进行销毁,将未被销毁的对象移动到幸存者0区并分配age;
- 然后再加载新的对象放到伊甸园区;
- 如果再次触发垃圾回收,将此次未被销毁的对象和上一次放在幸存者0区且此次也未被销毁的对象一齐移动到幸存者一区,此时新对象的age为1,上次的对象的age加1变为2;
- 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区,age也随之增加;
- 默认当age为15时,未被回收的对象将移动到老年区。可以通过设置参数来更改默认配置:-XX:MaxTenuringThreshold=<N>;该过程称为晋升(promotion);
- 在养老区,相对悠闲,当老年区内存不足时,再次触发GC(Major GC),进行养老区的内存清理;
- 若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常。
S0,S1满时不会触发YGC,但是YGC会回收S0,S1的对象。
总结
- 针对幸存者s0,s1区:复制之后有交换,谁空谁是to;
- 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不再永久区/元空间收集。
对象特殊情况分配过程

- 新对象申请内存,如果Eden放的下,则直接存入Eden;如果存不下则进行YGC;
- YGC之后如果能存下则放入Eden,如果还存不下(为超大对象),则尝试存入Old区;
- 如果Old区可以存放,则存入;如果不能存入,则进行Full GC;
- Full GC之后如果可以存入Old区,则存入;如果内存空间还不够,则OOM;
- 图右侧为YGC的流程图:当YGC之后未销毁的对象放入幸存者区,此时如果幸存者区的空间可以装下该对象,则存入幸存者区,否则,直接存入老年代;
- 当在幸存者区的对象超过阈值时,可以晋升为老年代,未达到阈值的依旧在幸存者区复制交换。
内存分配策略
针对不同年龄段的对象分配原则如下:
- 优先分配到Eden;
- 大对象直接分配到老年代:尽量避免程序中出现过多的大对象;
- 长期存活的对象分配到老年代;
- 动态对象年龄判断:如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入到老年代。无需等到MaxTenuringThreshold中要求的年龄;
数值变小原理
代码样例,设置参数:-Xms600m,-Xmx600m
public class HeapSpaceInitial {
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");
System.out.println("-Xmx : " + maxMemory + "M");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//执行结果
-Xms : 575M
-Xmx : 575M
明明设置的600M,怎么变成575M了呢?这是因为在堆内存存取数据时,新生代里边只有伊甸园和幸存者1区或者是幸存者2区存储对象,所以会少一个幸存者区的内存空间。
GC
JVM进行GC时,并非每次都对新生代、老年代、方法区(永久代、元空间)这三个区域一起回收,大部分回收是指新生代。
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)
Partial GC
部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集;
- 老年代收集(Major GC/Old GC):只是老年代的垃圾收集;
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集,只有G1 GC (按照region划分新生代和老年代的数据)会有这种行为。
目前,只有CMS GC会有单独收集老年代的行为;很多时候Major GC会和Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收。
Full GC
整堆收集(Full GC):整个java堆和方法区的垃圾收集。
触发机制
年轻代GC(Minor GC)触发机制
- 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次Minor GC会清理年轻代的内存,Survivor是被动GC,不会主动GC)
- 因为Java对象大多都具备“朝生夕灭”的特性,所以Minor GC非常频繁,一般回收速度也比较快。
- Minor GC会引发STW(Stop The World),暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行。
老年代GC(Major GC/Full GC)触发机制
- 指发生在老年代的GC,对象从老年代消失时,Major GC或者Full GC发生了;
- 出现了Major GC,经常会伴随至少一次的Minor GC(不是绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程),也就是老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发Major GC;
- Major GC速度一般会比Minor GC慢10倍以上,STW时间更长;
- 如果Major GC后,内存还不足,就报OOM了。
Full GC触发机制
触发Full GC执行的情况有以下五种:
- 调用System.gc()时,系统建议执行Full GC,但是不必然执行;
- 老年代空间不足;
- 方法区空间不足;
- 通过Minor GC后进入老年代的平均大小小于老年代的可用内存;
- 由Eden区,Survivor S0(from)区向S1(to)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
Full GC是开发或调优中尽量要避免的,这样暂停时间会短一些。
图解带你掌握`JVM`运行时核心内存区的更多相关文章
- 查看JVM运行时堆内存
利用jmap和MAT等工具查看JVM运行时堆内存 https://www.cnblogs.com/cjsblog/p/9561375.html jmap JDK自带了一些工具可以帮助我们查看JVM运行 ...
- 了解JVM运行时的内存分配
了解JVM运行时的内存分配 前言 上文中,在介绍运行时数据区域中的 JAVA 堆时,提到了 JVM 中的堆,一般分为三大部分:新生代.老年代.永久代,本文将进一步了解运行时的内存分配情况. 正文 1. ...
- 利用jmap和MAT等工具查看JVM运行时堆内存
jmap JDK自带了一些工具可以帮助我们查看JVM运行的堆内存情况,常用的是jmap命令 jmap -heap <pid> 打印堆的使用情况 那么,从这个输出中我们也可以大致看出堆的结构 ...
- JVM运行时的内存划分--JDK1.8
对比JDK1.7,JDK1.8在运行时的内存分配上进行了调整.本篇对JDK1.8版本进行简要介绍. 先以一张图片描述运行时内存: 程序计数器 记录当前线程执行的字节码行号.如果执行的是native方法 ...
- 图文并茂,带你认识 JVM 运行时数据区
跨平台的本质 关于 JVM, Java 程序员的最熟悉的一句话就是:一处编码,到处执行,指的就是 Java 语言可以通过 JVM 实现跨平台.而跨平台到底跨越了什么这个问题相信很少有人知道,接下来就跟 ...
- JVM 运行时的内存分配
首先我们必须要知道的是 Java 是跨平台的.而它之所以跨平台就是因为 JVM 不是跨平台的.JVM 建立了 Java 程序和操作系统之间的桥梁,JVM 是用 C 语言编写,而 C 语言不具备跨平台的 ...
- [二]Java虚拟机 jvm内存结构 运行时数据内存 class文件与jvm内存结构的映射 jvm数据类型 虚拟机栈 方法区 堆 含义
前言简介 class文件是源代码经过编译后的一种平台中立的格式 里面包含了虚拟机运行所需要的所有信息,相当于 JVM的机器语言 JVM全称是Java Virtual Machine ,既然是虚拟机, ...
- JVM总结(一):概述--JVM运行时数据区
大三下,趁着寒假重温一遍JVM,准备在一个系列来总价一下学习JVM的整个过程.争取在接下来的一个星期内更新完这一个系列,然后回家过年. JVM运行时数据区 线程私有的数据区 程序计数器 虚拟机栈 本地 ...
- 基础篇:JVM运行时内存布局
目录 1 JVM的内存区域布局 2 JVM五大数据区域介绍 3 JVM运行时内存布局和JMM内存模型区别 4 JMM内存模型交互操作 欢迎指正文中错误 关注公众号,一起交流 参考文章 1 JVM的内存 ...
- Java运行时环境---内存划分
背景:听说Java运行时环境的内存划分是挺进BAT的必经之路. 内存划分: Java程序内存的划分是交由JVM执行的,而不像C语言那样需要程序员自己买单(C语言需要程序员为每一个new操作去配对del ...
随机推荐
- shell脚本之规范与变量
shell编程规范与变量 名词简述 面向过程语言 按照顺序执行程序 第一件事干什么->第二件事干什么......(C,shell...) 面向对象语言 把程序看成一个整体(java,python ...
- 机器学习从入门到放弃:硬train一发手写数字识别
一.前言 前面我们了解了关于机器学习使用到的数学基础和内部原理,这一次就来动手使用 pytorch 来实现一个简单的神经网络工程,用来识别手写数字的项目.自己动手后会发现,框架里已经帮你实现了大部分的 ...
- JAVA类的加载(5)——总结
总结一下:1.类加载(初始化) 包括加载(类的class文件读入内存,并为之创建一个java.lang.Class对象,由类加载器完成).连接(把类的二进制数据合并到JRE中).初始化(对静态属性进行 ...
- lora训练之偷师
自stable diffusion开源之后AIGC绘画方向定制化百花齐放百家争鸣.而c站 https://civitai.com/ 也聚集了全球爱好者的各种微调训练模型分享. 其中以lora为首,应用 ...
- STM8 STM32 GPIO 细节配置问题
在MCU的GPIO配置中我们经常需要预置某一 IO 上电后为某一固定电平, 如果恰好我们需要上电后的某IO为高电平, 那么在配置GPIO的流程上面需要特别注意. 配置如下: (以下问题仅在STM8 / ...
- 题解 CF1739B
题目大意: 有一个非负整数序列 \(A\),定义序列 \(D\) 是序列 \(A\) 的绝对值差分序列,问给定序列 \(D\),能否求出唯一的序列 \(A\),若不能,输出 \(-1\),否则输出序列 ...
- AndroGenshin
是一个apk文件 jadx打开以后发现主要操作 找到mainactivity,研究代码可以知道主要操作就是对两个字符串进行RC4加密再进行一次换表的base64操作,最后再将加密后的结果与flag做对 ...
- 金蝶对接电商ERP库存数据,实现监听库存变化
金蝶云星空实时库存专题 通过向金蝶库存单据注册Python脚本,用于实时监听库存单据审核/反审核,并且将数据发送到轻易云系统集成平台 .通过集成平台将数据分发到对应的目标系统. 向金蝶的库存单据注册脚 ...
- SpringBoot项目中常见组件的配置属性
本文本的属性摘录自官方Properties配置清单,并附加了国内开发常用的框架配置属性.以国内WEB开发中,所涉及的常见组件为顺序组织配置清单 1. 配置属性清单 1.1 日志配置 序号 属性名 类型 ...
- ASP.NET Core 8 在 Windows 上各种部署模型的性能测试
ASP.NET Core 8 在 Windows 上各种部署模型的性能测试 我们知道 Asp.net Core 在 windows 服务器上部署的方案有 4 种之多.这些部署方案对性能的影响一直以来都 ...