JVM直接内存

什么是直接内存(堆外内存)

 

直接内存有一种叫法,堆外内存。

直接内存(堆外内存)指的是Java应用程序通过直接方式从操作系统中申请的内存。这个的差别与之前的堆,栈,方法区不同。那些内存都是经过了虚拟化的内存。

哪些代码可以操作直接内存?

Unsafe类

使用Java的Unsafe类,做一些内地内存的操作。

Netty操作直接内存

(Direct Memory),底层会调用操作系统的 malloc 函数。

JNI和JNA程序

有过不同语言间通信经历的一般都知道,它允许Java代码和其他语言(尤其C/C++)写的代码进行交互,只要遵守调用约定即可。首先看下JNI调用C/C++的过程,注意写程序时自下而上,调用时自上而下。

 

步骤非常的多,很麻烦,使用JNI调用.dll/.so共享库都能体会到这个痛苦的过程。如果已有一个编译好的.dll/.so文件使用JNI技 术调用,首先需要使用C语言另外写一个.dll/.so共享库,使用SUN规定的数据结构替代C语言的数据结构,调用已有的 dll/so中公布的函数。然后再在Java中载入这个库dll/so,最后编写Java  native函数作为链接库中函数的代理。经过这些繁琐的步骤才能在Java中调用 本地代码。因此,很少有Java程序员愿意编写调用dll/.so库中原生函数的java程序。这也使Java语言在客户端上乏善可陈,可以说JNI是 Java的一大弱点!

那么JNA是什么呢?

JNA(Java Native Access)是一个开源的Java框架,是Sun公司推出的一种调用本地方法的技术,是建立在经典的JNI基础之上的一个框架。之所以说它是JNI的替代者,是因为JNA大大简化了调用本地方法的过程,使用很方便,基本上不需要脱离Java环境就可以完成。

 

可以看到步骤减少了很多,最重要的是我们不需要重写我们的动态链接库文件,而是有直接调用的API,大大简化了我们的工作量。

JNA只需要我们写Java代码而不用写JNI或本地代码。

代码案例

Unsafe类

Unsafe 类,-XX:MaxDirectMemorySize 参数的大小限制对这种是无效的

 

 

 

由上述案例,我们申请100M的内存,但是-XX:MaxDirectMemorySize设置为10M。并没有抛出OOM。

ByteBuffer类

ByteBuffer 的这种方式,受到 MaxDirectMemorySize 参数的大小限制

其底层是:

 

 

 

 

为什么要使用直接内存

直接内存,其实就是不受 JVM 控制的内存。相比于堆内存有几个优势:

  1. 减少垃圾回收,因为垃圾回收会STW
  2. 加快了复制速度。当我们进行IO时,会复制一份数据到堆外内存再发送。直接使用直接内存省略了这一步。
  3. 可以实现进程数据共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现。
  4. 可以扩展内存。

直接内存的另一面

说白了就是缺点:

  1. 难以排查的OOM。
  2. 不适合存储复杂对象,一般来说简单对象比较适合。

直接内存案例和场景分析

内存泄漏案例

工作中经常会使用 Java 的 Zip 函数进行压缩和解压,这种操作在一些对传输性能较高的的场景经常会用到。

程序将会申请 1kb 的随机字符串,然后不停解压。为了避免让操作系统陷入假死状态,我们每次都会判断操作系统内存使用率,在达到 60% 的时候,

我们将挂起程序(不再解压,只不断的让线程休眠)

通过访问 8888 端口,将会把内存阈值提高到 85%。

 

 

 

 

程序打包上传到 CenterOS 的服务器中(服务器内存 4G)

 

 

参数解释:

AlwaysPreTouch 这个参数,在 JVM 启动的时候,就把它所有的内存在操作系统分配了(JVM的内存是动态分配的,一开始很小,慢慢扩大),默认情况下,此选项是禁用的。我们为了减少内存动态分配的影响,把这个值设置为 True。

这个程序很快就打印一下显示,这个证明操作系统内存使用率,达到了 60%。

 

通过 top 命令查看,确实有一个进程占用了很高的内存

VIRTvirtual memory usage 虚拟内存

1、 进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据等

2、假如进程申请 100m 的内存,但实际只使用了 10m,那么它代表 100m,而不是实际的使用量

RESresident memory usage 常驻内存 达到了 1.5G

如果申请 100m 的内存,实际使用 10m,它代表 10m,与 VIRT 相反。我们一般观察这个内存更能说明问题。

 

常规排查

按照之前的排查方式,如果碰到内存占用过高,我们使用 top 命令来跟踪,然后使用 jmap –heap 来显示

 

这么一看,堆内存的使用率都很低。于是我们怀疑是不是虚拟机栈占用过高。于是使用jstack命令看下线程。

 

 

发现也就那么 10 来个左右的线程,这块占用的空间肯定也不多。

jmap -histo 3468 | head -20 显示占用内存最多的对象

 

发现这个才 20 多 M,没有达到 1.5G

发现不了,我们前面学过 MAT,我们把内存 dump 下来,放到 MAT 中进行分析。

 

 

发现没什么问题?堆空间也好,其他空间也好,这些都没有说的那么大的内存 1.5G 左右。

使用工具排查

这种情况应该是发生了直接内存泄漏。

如果要跟踪本地内存的使用情况,一般需要使用 NMT

NMT

NativeMemoryTracking,是用来追踪 Native 内存的使用情况。通过在启动参数上加入 -XX:NativeMemoryTracking=detail 就可以启用。使用 jcmd jdk 自带)命令,就可查看内存分配。

Native Memory Tracking (NMT) Hotspot VM 用来分析 VM 内部内存使用情况的一个功能。我们可以利用 jcmdjdk 自带)这个工具来访问 NMT 的数据。

NMT 必须先通过 VM 启动参数中打开,不过要注意的是,打开 NMT 会带来 5%-10%的性能损耗。

在服务器上重新运行程序:

java -cp ref-jvm3.jar

-XX:+PrintGC

-Xmx1G

-Xmn1G

-XX:+AlwaysPreTouch

-XX:MaxMetaspaceSize=10M

-XX:MaxDirectMemorySize=10M

-XX:NativeMemoryTracking=detail ex15.LeakProblem

 

jcmd $pid VM.native_memory summary

看到我们这种泄漏的场景。下面这点小小的空间,是不能和 1~2GB 的内存占用相比的。

 

问题排查到这里,很明显了,这块的问题排查超出了一般 java 程序员的范畴了(说白了就是你做到这点就 OK 了,继续排查就是在干操作系统和其他的语言相关的问题排查了)

内存泄漏问题解决

主要的是解决问题。

这个程序可以访问服务器的 8888 端口,这将会把内存使用的阈值增加到 85%,我们的程序会逐渐把这部分内存占满

访问地址:http://127.0.0.1:8888/

可以看到内存再次增长。运行时数据区的内存一般变化不大。再次帮助我们定位问题。

问题关键点

GZIPInputStream 使用 Inflater 申请堆外内存、我们没有调用 close() 方法来主动释放。如果忘记关闭,Inflater 对象的生命会延续到下一次 GC,有一点类似堆内的弱引用。在此过程中,堆外内存会一直增长。

问题修复

调用 close() 方法来主动释放,放置内存泄漏

原代码:

修改后:

此时内存不会持续正常,得到解决。

直接内存总结

直接内存主要是通过 DirectByteBuffer 申请的内存,可以使用参数“MaxDirectMemorySize”来限制它的大小

其他堆外内存(直接内存),主要是指使用了 Unsafe 或者其他 JNI 手段直接直接申请的内存。这种情况下没有任何参数能够阻挡它们,要么靠它自己去释放一些内存,要么等待操作系统对它来处理。所以如果你对操作系统底层以及内存分配使用不熟悉,最好不要使用这块,尤其是 Unsafe 或者其他 JNI 手段直接直接申请的内存。

在未来学习EhCache 这种缓存框架,提供了多种策略,可以设定将数据存储在非堆上。还有像 RocketMQ 都走了堆外分配,所以我们也需要去了解他。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大将黄猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值