[转帖]深入理解虚拟机栈

深入,理解,虚拟机 · 浏览次数 : 0

小编点评

**1. JVM 的垃圾回收** * JVM 申请的那块逻辑堆区域,将数据标记为空闲之类的操作,不是调用 free 将内存归还给操作系统。 * JVM 提供了-XX:MinHeapFreeRatio和-XX:MaxHeapFreeRatio 两个参数,用于配置这个归还策略。 * MinHeapFreeRatio:代表当空闲区域大小下降到该值时,会进行扩容,扩容的上限为 XmxMaxHeapFreeRatio:代表当空闲区域超过该值时,会进行“缩容”,缩容的下限为Xms不过虽然有这个归还功能,不过因为这个代价比较昂贵,所以 JVM 在归还的时候,是线性递增归还的,并不是一次全部归还。 **2. 为什么要不把内存归还给操作系统?** * JVM 还是提供了这个归还内存的功能。JVM 提供了-XX:MinHeapFreeRatio和-XX:MaxHeapFreeRatio 两个参数,用于配置这个归还策略。 * MinHeapFreeRatio:代表当空闲区域大小下降到该值时,会进行扩容,扩容的上限为 XmxMaxHeapFreeRatio:代表当空闲区域超过该值时,会进行“缩容”,缩容的下限为Xms不过虽然有这个归还功能,不过因为这个代价比较昂贵,所以 JVM 在归还的时候,是线性递增归还的,并不是一次全部归还。 **3. 如何归还?** * 虽然代价高,但 JVM 还是提供了这个归还内存的功能。JVM 提供了-XX:MinHeapFreeRatio和-XX:MaxHeapFreeRatio 两个参数,用于配置这个归还策略。 * MinHeapFreeRatio:代表当空闲区域大小下降到该值时,会进行扩容,扩容的上限为 XmxMaxHeapFreeRatio:代表当空闲区域超过该值时,会进行“缩容”,缩容的下限为Xms不过虽然有这个归还功能,不过因为这个代价比较昂贵,所以 JVM 在归还的时候,是线性递增归还的,并不是一次全部归还。

正文

一、背景

最近遇到个现象,hubble-api-open组件过段时间会内容占满,从而被K8S强制重启。

让我困惑的是,已经设置了-XX:MaxRAMPercentage=75.0,我觉得留有了一定的空间,不应该会占满,所以想深究下原因。

-XX:MaxRAMPercentage是设置JVM的最大堆内存占虚机或pod(limits)的比例,默认为25.0。我设置的是75.0。

注意呦:是堆内存哦!不包含元空间,非堆这些,所以Java进程实际使用的内存可能是超过配置的limits的75%。

二、是否是配置没有起作用?如何验证?

要验证的话,其实也简单,看下JVM配置的最大堆内存是多少即可。

好在我们已经接入了prometheus,接入了Actuator,因此我们是可以看到配置的最大内存的指标的,检查如下:

  1. jvm_memory_max_bytes{area="nonheap",id="Code Cache",} 2.5165824E8
  2. jvm_memory_max_bytes{area="nonheap",id="Metaspace",} -1.0
  3. jvm_memory_max_bytes{area="nonheap",id="Compressed Class Space",} 1.073741824E9
  4. jvm_memory_max_bytes{area="heap",id="G1 Eden Space",} -1.0
  5. jvm_memory_max_bytes{area="heap",id="G1 Survivor Space",} -1.0
  6. jvm_memory_max_bytes{area="heap",id="G1 Old Gen",} 4.718592E9

上面的area="heap"的三个加起来就是配置的最大的堆内存,算一下:

4.718592E9 = 4.718592 乘以 10的9次方 = 4.718592 * 10^9 = 4718592000 byte = 4718592000 byte = 4608000 KB = 4500 MB = 4.395GB

也就是配置的JVM允许使用的最大的堆内存是4.395GB,是不是75%呢?我们需要去看下limits的配置:可见是5.86GB

5.86*0.75 = 4.395GB,和我们从指标上看到的是一致的,说明配置生效了

三、为什么会OOM?

1、是否是非堆内存导致的?

答案:不是

grafana看下当前RSS和WSS,可以看到已经快100%了:

但是我们看JVM进程实际使用的,其实并不高:

jvm_memory_used_bytes:表示JVM实际已经使用的内存

jvm_memory_committed_bytes:可供Java虚拟机使用的已提交的内存量

  • committed是当前可使用的内存大小(包括已使用的),committed >= used。committed不足时jvm向系统申请

jvm_memory_max_bytes:表示最大可以申请的内存量

  • committed <= max

 再看下其它相关内存(这个内存也是动态变化的):

jvm_memory_committed_bytes:可供Java虚拟机使用的已提交的内存量

这理论上是从操作系统层面看到的Java进程占用的内存。

如下堆和非堆总计也就4.2GB左右(所以不是非堆导致),并没有达到Limits的97.49%

 进入容器,用top命令查看,占了5.83g内存,明显还是要大于jvm_memory_committed_bytes,所以,java应该还有使用的没有通过指标列出来,这块需要找下

下面是不同内存区域最大可申请的内存量

jvm_memory_max_bytes:表示最大可以申请的内存量

本来我觉得JVM在TOP中使用的内存大小是按照JVM实际使用量计算,即按照jvm_memory_used_bytes,但是经过排查,其实是jvm_memory_committed_bytes + JVM其它内存(这块待研究)

从TOP命令看(即从操作系统层面看),确实是Java进程占用了所有的内存,所以java除了已提交的内存,一定还有其它的。

如果按照jvm_memory_committed_bytes来计算,那么差不多占了4.2GB左右,这个数值应该等于TOP中的RES,但是为什么差距那么大呢?

2、是不是因为线程占用的内存没有在prometheus指标中体现?

我觉得这个倒是有可能的,看了下线程相关的指标:

  • jvm_threads_live 906.0
  • tomcat_threads_current{name="http-nio-9507",} 200.0

可见JVM线程 + tomcat线程有差不多1000个了,假设每个1M的话,差不多占了1GB,所以这块严重怀疑。

这样算下来其实就差不多了,4.2GB + 1GB = 5.2GB

怎么验证呢?每个线程栈大小是多大呢?

通过dump命令拉取到某个时刻的线程数:879MB

而在jvm_memory_committed_bytes指标中并未看到类似大小的指标,所以我判断应该是没有在memory相关指标中有显示。

但是算上线程栈的话,假设5.2GB,和实际占用5.83还是差几百兆,因此应该还有一部分没有算上。

3、其它内存

待续……

四、JVM占用内存与操作系统层面看JVM占用内存

在JDK 8及之前,哪怕GC之后占用内存下降,得到的内存空间也仅仅是从Used转变为Committed,在JVM一侧看这些已经是空闲内存了,但在OS一侧看来,仍然是被JVM占用的状态。

从JDK 9起,加入了ShrinkHeapInSteps参数,或者说修复了以前的UseAggressiveHeapShrink参数的问题(https://bugs.openjdk.java.net/browse/JDK-8146436),让OS一侧能看到JVM主动释放的内存。

但这个行为必须是与具体收集器相关的,其他几款收集器的改进并不活跃,G1在这个时期有改动,让它在FullGC时顺带Decommit空闲内存回OS(https://bugs.openjdk.java.net/browse/JDK-8038423)。

不过,以上改进仍然是效果很差,因为G1是并发增量收集,FullGC是很罕见的,所以通常加了这个参数也不容易见到效果。

到了JDK 12,发布了JEP 346(https://openjdk.java.net/jeps/346),这个JEP的改进就是让G1能在正常的不产生FullGC的收集循环中,也能自动Decommit空闲内存回OS(通过G1PeriodicGCInterval参数),后来的Shenandoah收集器也共用了这部分代码。

到了JDK 13,ZGC默认就有Decommit行为,不需要额外参数了。

五、Java程序的占用内存构成

虚拟机内存与本地内存的区别
 Java虚拟机在执行的时候会把管理的内存分配成不同的区域,这些区域被称为虚拟机内存,同时,对于虚拟机没有直接管理的物理内存,也有一定的利用,这些被利用却不在虚拟机内存数据区的内存,我们称它为本地内存,这两种内存有一定的区别:

JVM内存

  • 受虚拟机内存大小的参数控制,当大小超过参数设置的大小时就会报OOM

本地内存

  • 本地内存不受虚拟机内存参数的限制,只受物理内存容量的限制,虽然不受参数的限制,但是如果内存的占用超出物理内存的大小,同样也会报OOM

六、各个主要内存区域存放的数据

1、堆

java堆是JVM内存中最大的一块,由所有线程共享,是由垃圾收集器管理的内存区域。

2、虚拟机栈

虚拟机栈是线程私有的,随线程生灭。

由于一个线程默认分配1M空间,一个Java进程一般都有几百个线程,升至上千个线程,因此虚拟机栈一般是除了堆之外,最占用内存的区域

另外,程序计数器是线程私有的,每个线程都已自己的程序计数器,所以,程序计数器的内容不用另外算,直接计算线程的内存即可。

3、元空间(Meta Space)

元空间类似于老版本的方法区。非堆区。

元空间一般占内存100MB左右,是堆,栈之外,最占用内存的区域

元空间主要包含两部分数据:

  • 类信息:或者说类的元信息。
    • 类的元信息里面放置了类的基本信息,包括类的版本、字段、方法、接口以及常量池表
    • 常量池表:存储了类在编译期间生成的字面量、符号引用
  • 运行时常量池:主要存放在类加载后被解析的字面量与符号引用

方法区是所有线程共享的内存。

在java8以前是放在JVM内存中的,由永久代实现,受JVM内存大小参数的限制。

在java8中移除了永久代的内容,方法区由元空间(Meta Space)实现,并直接放到了本地内存中,不受JVM参数的限制(当然,如果物理内存被占满了,方法区也会报OOM),并且将原来放在方法区的字符串常量池和静态变量都转移到了Java堆中。

CCS(Compressed Class Space)

在Java8以前,有一个选项是UseCompressedOops。所谓OOPS是指“ordinary object pointers“,就是原始指针。Java Runtime可以用这个指针直接访问指针对应的内存,做相应的操作(比如发起GC时做copy and sweep)。

那么Compressed是啥意思?64bit的JVM出现后,OOPS的尺寸也变成了64bit,比之前的大了一倍。这会引入性能损耗——占的内存double了,并且同尺寸的CPU Cache要少存一倍的OOPS。

于是就有了UseCompressedOops这个选项。打开后,OOPS变成了32bit。但32bit的base是8,所以能引用的空间是32GB——这远大于目前经常给jvm进程内存分配的空间。

Compressed Class Space 是 Metaspace 的一部分,默认值是 1G。所以其实 Compressed Class Space 这个名字取得很误导,压缩的并不是 Klass,而是 Klass*。

compressed class space 空间的大小,是通过 -XX:CompressedClassSpaceSize 指定的。

如下占用了13MB左右空间,比较小。非堆区。

jvm_memory_committed_bytes{application="hubble-biz-host",area="nonheap",id="Compressed Class Space",} 1.3549568E7

4、代码缓存区

代码缓存区是一块存储编译后代码的区域。非堆区。

如下大致占了80MB左右的空间

jvm_memory_committed_bytes{application="hubble-biz-host",area="nonheap",id="Code Cache",} 7.9757312E7

5、本地方法栈(Native Method Stacks)

 本地方法栈与虚拟机栈的作用是相似的,都会抛出OutOfMemoryError和StackOverFlowError,都是线程私有的,主要的区别在于:

  • 虚拟机栈执行的是java方法
  • 本地方法栈执行的是native方法

直接内存

执行native方法会产生直接内存。

直接内存位于本地内存,不属于JVM内存,但是也会在物理内存耗尽的时候报OOM。非堆区。

直接内存(DirectMemory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致

这块占用的内存经过观察一般不高,如下可知只有大约4MB,如下:

jvm_buffer_memory_used_bytes{application="hubble-biz-host",id="direct",} 4169692.0

在jdk1.4中加入了NIO(New Input/Putput)类,引入了一种基于通道(channel)与缓冲区(buffer)的新IO方式,它可以使用native函数直接分配堆外内存,然后通过存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样可以在一些场景下大大提高IO性能,避免了在java堆和native堆来回复制数据。
 

七、其它可能产生内存的地方

八、内存归还

1、介绍

JVM 的垃圾回收,只是一个逻辑上的回收,回收的只是 JVM 申请的那一块逻辑堆区域,将数据标记为空闲之类的操作,不是调用 free 将内存归还给操作系统。

JVM 的自动内存管理,其实只是先向操作系统申请了一大块内存,然后自己在这块已申请的内存区域中进行“自动内存管理”。JAVA 中的对象在创建前,会先从这块申请的一大块内存中划分出一部分来给这个对象使用,在 GC 时也只是这个对象所处的内存区域数据清空,标记为空闲而已

2、为什么不把内存归还给操作系统?

JVM 还是会归还内存给操作系统的,只是因为这个代价比较大,所以不会轻易进行。而且不同垃圾回收器 的内存分配算法不同,归还内存的代价也不同。

比如在清除算法(sweep)中,是通过空闲链表(free-list)算法来分配内存的。简单的说就是将已申请的大块内存区域分为 N 个小区域,将这些区域同链表的结构组织起来,就像这样:

 

每个 data 区域可以容纳 N 个对象,那么当一次 GC 后,某些对象会被回收,可是此时这个 data 区域中还有其他存活的对象,如果想将整个 data 区域释放那是肯定不行的。

所以这个归还内存给操作系统的操作并没有那么简单,执行起来代价过高,JVM 自然不会在每次 GC 后都进行内存的归还。

3、怎么归还?

虽然代价高,但 JVM 还是提供了这个归还内存的功能。JVM 提供了-XX:MinHeapFreeRatio-XX:MaxHeapFreeRatio 两个参数,用于配置这个归还策略。

  • MinHeapFreeRatio:代表当空闲区域大小下降到该值时,会进行扩容,扩容的上限为 Xmx
  • MaxHeapFreeRatio:代表当空闲区域超过该值时,会进行“缩容”,缩容的下限为Xms

不过虽然有这个归还的功能,不过因为这个代价比较昂贵,所以 JVM 在归还的时候,是线性递增归还的,并不是一次全部归还。

经过实测,这个归还内存的机制,在不同的垃圾回收器,甚至不同的 JDK 版本中还不一样!

参考:

终于搞懂了java8的内存结构,再也不纠结方法区和常量池了!_lei6393的博客-CSDN博客_java8 内存结构

运维:你们 JAVA 服务内存占用太高,还只增不减!告警了,快来接锅 - SegmentFault 思否

文章知识点与官方知识档案匹配,可进一步学习相关知识

与[转帖]深入理解虚拟机栈相似的内容:

[转帖]深入理解虚拟机栈

一、背景 最近遇到个现象,hubble-api-open组件过段时间会内容占满,从而被K8S强制重启。 让我困惑的是,已经设置了-XX:MaxRAMPercentage=75.0,我觉得留有了一定的空间,不应该会占满,所以想深究下原因。 -XX:MaxRAMPercentage是设置JVM的最大堆内

[转帖]JVM(3)之垃圾回收(GC垃圾收集器+垃圾回收算法+安全点+记忆集与卡表+并发可达性分析......)

《深入理解java虚拟机》+宋红康老师+阳哥大厂面试题2总结整理 一、堆的结构组成 堆位于运行时数据区中是线程共享的。一个进程对应一个jvm实例。一个jvm实例对应一个运行时数据区。一个运行时数据区有一个堆空间。 java堆区在jvm启动的时候就被创建了,其空间大小也就被确定了(堆是jvm管理的最大

[转帖]JVM系列之:你知道Java有多少种内存溢出吗

本文为《深入学习 JVM 系列》第二十五篇文章 Java内存区域 关于这部分内容大多来源于《深入理解Java虚拟机》一书。 Java 运行时数据区域(JDK8)如下图所示: 关于上述提到的线程共享和线程隔离区域,下图做详细讲解: 程序计数器 程序计数器是一块较小的内存空间,可以看作是当前线程所执行的

[转帖]Java实战之OutOfMemoryError异常问题及解决方法

https://www.jb51.net/article/244872.htm + 目录 在Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError (下文称OOM)异常的可能。本篇主要结合着【深入理解Java虚拟机】一书当中整理了本篇博客

[转帖]深入理解同步机制---内核自旋锁

https://switch-router.gitee.io/blog/spinlock/ 进程(线程)间的同步机制是面试时的常见问题,所以准备用一个系列来好好整理下用户态与内核态的各种同步机制。本文就以内核空间的一种基础同步机制—自旋锁开始好了 自旋锁是什么 自旋锁就是一个二状态的原子(atomi

[转帖]深入理解SQL的四种连接-左外连接、右外连接、内连接、全连接

https://www.cnblogs.com/jiangjunli/p/10617034.html 1、内联接(典型的联接运算,使用像 = 或 <> 之类的比较运算符)。包括相等联接和自然联接。 内联接使用比较运算符根据每个表共有的列的值匹配两个表中的行。例如,检索 students和course

[转帖]深入理解Redis的持久化

https://www.cnblogs.com/ivictor/p/9749465.html RDB RDB是将当前数据生成快照保存到硬盘上。 RDB的工作流程: 1. 执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进程,如RDB/AOF子进程,如果存在bgsave命令直接返回。

[转帖]深入理解 netfilter 和 iptables

Netfilter (配合 iptables)使得用户空间应用程序可以注册内核网络栈在处理数据包时应用的处理规则,实现高效的网络转发和过滤。很多常见的主机防火墙程序以及 Kubernetes 的 Service 转发都是通过 iptables 来实现的。 关于 netfilter 的介绍文章大部分只

[转帖]深入理解以太网网线原理

https://zhuanlan.zhihu.com/p/568057983?utm_id=0 译者按:大部分人都知道,百兆以太网只用了 RJ45 端口中的 2 对 4 根线,分别为 TX、RX 的差分信号。 千兆以太网用了 RJ45 端口中的全部 4 对 8 根线,但是这 4 对 8 根线是怎么定

[转帖]深入理解Redis的scan命令

熟悉Redis的人都知道,它是单线程的。因此在使用一些时间复杂度为O(N)的命令时要非常谨慎。可能一不小心就会阻塞进程,导致Redis出现卡顿。 有时,我们需要针对符合条件的一部分命令进行操作,比如删除以test_开头的key。那么怎么获取到这些key呢?在Redis2.8版本之前,我们可以使用ke