Code前端首页关于Code前端联系我们

JVM与Linux内存关系详解:内存分配、JVM调优、Java程序优化

terry 2年前 (2023-09-25) 阅读数 49 #后端开发
JVM 与 Linux 内存关系详解:内存分配、JVM调优、Java程序优化

一些物理内存为8g的服务器,主要运行Java服务,系统内存分配如下: JVM是堆大小Java服务设置为6g,一个监控进程大约需要600m,Linux本身大约需要800m。

乍一看物理内存应该足够;但实际中会使用大量的SWAP(说明物理内存不够),如下图所示。由于SWAP和GC同时发生,JVM会严重崩溃,所以我们要问:内存到哪里去了? JVM 与 Linux 内存关系详解:内存分配、JVM调优、Java程序优化JVM 与 Linux 内存关系详解:内存分配、JVM调优、Java程序优化

要分析这个问题,了解JVM和操作系统之间的内存关系非常重要。接下来我们主要对Linux和JVM之间的内存关系进行分析。

1。 Linux 和进程内存模型

JVM 作为进程(进程)运行在 Linux 系统上。理解Linux和进程之间的内存关系是理解JVM和Linux内存关系的基础。下图总结了硬件、系统、进程三个层面上内存之间的关系。 JVM 与 Linux 内存关系详解:内存分配、JVM调优、Java程序优化

从硬件角度来看,Linux系统的内存空间由两部分组成:物理内存和SWAP(磁盘上)。物理内存是Linux操作使用的主要内存区域;如果物理内存不够,Linux会将SWAP中一些暂时不用的内存数据放到磁盘上,以腾出更多可用内存空间;以及何时有必要。当数据在SWAP中使用时,必须首先将其交换回内存。建议对 JVM 运行时区域进行详细解释。

从Linux系统角度来看,除了引导系统的BIN区域外,整个内存空间主要分为两部分:内核内存(Kernel Space)和用户内存(User Space)。

内核内存是Linux本身使用的内存空间。主要用于程序调度、内存分配以及与硬件资源的连接等程序逻辑。

用户内存是提供给任何进程的最重要的空间。 Linux为每个进程提供相同的虚拟内存空间;这使得进程彼此独立并且不会互相干扰。实现方法是利用虚拟内存技术:每个进程都分配一定的虚拟内存空间,只有在实际使用虚拟内存时才分配物理内存。

如下图所示,对于32位Linux系统,一般分配0~3G虚拟内存空间作为用户空间,3~4G虚拟内存空间分配作为内核空间; 64位系统的分布类似。JVM 与 Linux 内存关系详解:内存分配、JVM调优、Java程序优化

从进程的角度来看,进程可以直接访问的用户内存(虚拟内存空间)分为5部分:代码区、数据区、堆区、堆栈区和未使用区。

应用程序机器码存储在代码区。代码在运行过程中不能更改,具有只读、固定大小的特点。

数据区存储应用程序中的全局数据、静态数据和一些常量字符串,其大小也是固定的。

堆是程序在运行时动态请求的空间。它是程序运行时立即请求并释放的内存资源。

堆栈区用于存储函数参数、临时变量、返回地址等数据。

未使用区域是分配新内存空间的初步区域。

2。进程与JVM内存空间

JVM本质上是一个进程,因此它的内存空间(也称为运行时数据区,注意与JMM的区别)也具有进程的一般特征。关于Java中JVM内存管理的详细解释,可以参考这篇文章。然而,

JVM 并不是一个普通的进程。它在内存方面有许多新功能。主要有两个原因:

  • JVM将很多原本属于操作系统管理范围的东西移植到了JVM中。目标是减少系统调用的次数;
  • Java NIO,目标是减少读写IO的系统调用的开销。 JVM进程与常规进程内存模型的对比如下:
JVM 与 Linux 内存关系详解:内存分配、JVM调优、Java程序优化

需要注意的是,该模型并不是JVM内存使用的准确模型。它更多地关注操作系统的角度,而忽略了 JVM 的一些内部细节(尽管这也非常重要)。下面,从用户内存和内核内存两个方面来讲解JVM进程的内存特性。

1。用户内存

上图主要强调,JVM进程模型的代码区和数据区是指JVM本身,而不是Java程序。常规进程堆栈区域通常仅用作 JVM 中的线程堆栈。下面详细解释JVM的堆区和普通进程的堆区的主要区别:

首先是永久代。永久代本质上就是Java程序的代码区和数据区。 Java程序中的类在整个区域中被加载到各种数据结构中,包括常量池、字段、方法数据、方法体、类中的构造函数和特殊方法、实例初始化、接口初始化等。对于操作系统来说,这个区域是堆的一部分;对于Java程序来说,这是容纳程序本身和静态资源的空间,允许JVM解释和执行Java程序。

接下来是新生代和老一代。新生代和老年代组成了Java程序实际使用的堆空间,主要用于存储内存对象;然而,它们的管理方法与普通流程有很大不同。

普通进程在运行时为内存对象分配空间时,例如C++执行new操作时,会触发系统调用来分配内存空间。操作系统线程根据对象的大小分配空间然后返回;同时程序被激活。当一个对象被释放时,例如C++执行删除操作时,也会触发系统调用,告诉操作系统该对象占用的空间可以回收。

JVM 使用内存的方式与常规进程不同。 JVM向操作系统申请一整个内存区域(具体大小可以在JVM参数中调整)作为Java程序的堆(分为新生代和老年代);当Java程序请求内存空间时,例如执行新操作,JVM就会这样做。段空间根据Java程序所需的大小分配给Java程序,Java程序不负责通知JVM何时有空间可用。对象可以被释放。垃圾对象的内存空间被JVM回收。

JVM的内存管理方式的好处是显而易见的,包括: 第一,减少系统调用的次数。 JVM 无需操作系统干预即可为 Java 程序分配内存空间。仅当 Java 堆大小发生变化时它才需要工作。系统请求或报告内存进行回收,而普通程序需要系统调用来参与任何内存空间的分配和回收;其次,为了减少内存泄漏,常规程序不会通知(或者没有及时通知操作系统)内存空间的释放,这是造成内存泄漏的一个主要原因。原因之一是由JVM统一管理可以防止程序员造成的内存泄漏问题。

最后一部分是未使用区域,是分配新内存空间的准备区域。对于常规进程,该空间可用于请求和释放堆和堆栈空间。每次分配堆内存时都会使用该区域,因此其大小经常变化;对于 JVM 进程,它在调整堆大小和线程堆栈时使用。尽管该区域的堆大小一般调整频率较低,但大小相对稳定。操作系统会动态调整该区域的大小,并且该区域通常不会分配任何实际的物理内存,但进程只能在该区域请求堆或堆栈空间。

2。内核内存

应用程序通常不直接处理内核内存。内核内存由操作系统管理和使用;然而,由于Linux注重性能和改进,一些新功能允许应用程序使用内核内存或映射到内核空间。 Java NIO就是在这样的背景下诞生的。它充分利用了Linux系统的新特性,提高了Java程序的IO性能。 JVM 与 Linux 内存关系详解:内存分配、JVM调优、Java程序优化

上图展示了Linux系统中Java NIO使用的内核内存分布。nio缓冲区主要包括:nio使用不同通道时使用的ByteBuffer,Java程序主动使用ByteBuffer.allocateDirector来请求分配的缓冲区。

在PageCache中,nio占用的内存主要包括:以FileChannel.map方式打开文件占用映射所需的缓存、FileChannel.transferTo和FileChannel.transferFrom(图中标记为nio文件)。

可以通过JMX检查NIO Buffer和映射的使用情况,如下图所示。但是,FileChannel 实现通过系统调用使用本机 PageCache。进程对Java来说是透明的,无法监控这部分内存的使用情况。 JVM 与 Linux 内存关系详解:内存分配、JVM调优、Java程序优化

Linux 和 Java NIO 释放内核内存中的空间以供程序使用,主要是为了减少不必要的复制并减少 IO 操作系统调用的开销。例如,使用普通方法和NIO从磁盘文件向网卡发送数据时,数据流程对比如下图所示: JVM 与 Linux 内存关系详解:内存分配、JVM调优、Java程序优化

在内核内存和用户内存之间复制数据需要资源和时间,而van As从上图中我们可以看到,NIO 将内核内存和用户内存之间的数据副本数量减少了两倍。这是Java NIO高性能的重要机制之一(另一个是异步非阻塞)。

从上面可以看出,内核内存对于Java程序的性能来说也是非常重要的。因此,在分配系统内存使用时,必须为内核留下一定的可用空间。 ?代和老年代)+线程栈+Java NIO

回到文章开头的问题:初始内存分配为:6g(java堆)+600m(监控)+800m(系统),剩余大约 600 m 的内存未分配。

现在分析一下这6亿内存的分配:

  • Linux预留了大约2亿,这是Linux正常运行所必需的。
  • Java服务的线程数为160,JVM默认的线程栈大小为1m,因此使用了160m的内存,
  • Java NIO缓冲区,通过JMX发现这占用了到200m需要。
  • Java服务使用NIO读写大量文件,需要使用PageCache。正如前面分析的那样,定量估计其规模仍然很困难。

前三项总计5.6亿,因此可以得出Linux物理内存不够的结论。

聪明的人会发现,介绍中提到的两台服务器中,一台SWAP最大占用2.16g,另一台SWAP最大占用871m;不过,我们的记忆差距似乎并没有那么大。其实这是SWAP和GC同时运行造成的。如下图所示,SWAP的使用和长GC是同时发生的。 JVM 与 Linux 内存关系详解:内存分配、JVM调优、Java程序优化JVM 与 Linux 内存关系详解:内存分配、JVM调优、Java程序优化

如果SWAP和GC同时发生,GC时间会很长,JVM会严重崩溃,极端情况下服务会崩溃。原因如下: JVM在进行GC时,必须遍历对应堆分区的已用内存; GC时,如果堆中部分内容被交换到SWAP中,则在遍历这个chunk时,必须将其交换回内存。 ,同时由于内存空间不足,需要将内存中的另一部分堆转换为SWAP;因此,在遍历堆分区时(极端情况),整个堆分区会顺序写入SWAP。 Linux对SWAP的回收是滞后的,我们会看到大量的SWAP使用。上述问题可以通过减小堆大小或增加物理内存来解决。

因此,我们得出一个结论:部署Java服务的Linux系统在内存分配时应避免使用SWAP;具体分配要综合考虑不同场景下JVM对Java持久代和Java堆(新生代和新生代)的影响。老年代)、Java NIO 使用的线程堆栈和内存要求。

2。内存泄漏问题

还有一种情况是,8g内存的服务器,Linux使用800m,监控进程使用600m,堆大小设置为4g;系统可用内存约为2.5g,但也占用大量SWAP。

这个问题的分析如下:

  • 在这个场景中,Java持久代、Java堆(新生代和老年代)、线程栈使用的内存基本是固定的。因此,内存使用过多的原因在于Java NIO。
  • 根据之前的模型,Java NIO使用的内存主要分布在Linux内核内存的System区和PageCache区。如果我们查看如下所示的监控记录,我们可以看到,在SWAP发生之前,即物理内存不够的时候,PageCache急剧收缩。因此,系统区的Java NIO缓冲区发生了内存泄漏。
JVM 与 Linux 内存关系详解:内存分配、JVM调优、Java程序优化JVM 与 Linux 内存关系详解:内存分配、JVM调优、Java程序优化
  • 由于 NIO 的 DirectByteBuffer 需要在 GC 的后期进行回收,因此不断请求 DirectByteBuffer 的程序通常需要调用 System.gc() 来避免旧区引用的 DirectByteBuffer 由于没有进行 FullGC 导致内存泄漏。很长一段时间。经过分析,我们可以得出结论,可能的原因有两个:第一,Java程序在必要的时候没有调用System.gc();第二,Java程序没有在必要的时候调用System.gc()。其次,System.gc() 被禁用。
  • 最后一步是检查JVM启动参数和Java程序的DirectByteBuffer使用情况。在此示例中,检查 JVM 启动参数显示启用了 -XX:+DisableExplicitGC,这会禁用 System.gc()。

4.总结

本文详细分析了Linux与JVM之间的内存关系,比较了一般进程与JVM进程在内存使用上的异同。了解这些特性将有助于Linux系统内存分配、JVM调优以及Java程序优化。有帮助。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门