直接内存(堆外内存)

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

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

直接内存有哪些?

 使用了 Java 的 Unsafe 类,做了一些本地内存的操作;

 Netty 的直接内存(Direct Memory),底层会调用操作系统的 malloc 函数;

 JNI 或者 JNA 程序,直接操纵了本地内存,比如一些加密库。

JNI 是 Java Native Interface 的缩写,通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植。

JNA(Java Native Access )提供一组 Java 工具类用于在运行期间动态访问系统本地库(native library:如 Window 的 dll)而不需要编写任何 Native/JNI 代码。

开发人员只要在一个 java 接口中描述目标 native library 的函数与结构,JNA 将自动实现 Java 接口到 native function 的映射。

JNA 是建立在 JNI 技术基础之上的一个 Java 类库,它使您可以方便地使用 java 直接访问动态链接库中的函数。

原来使用 JNI,你必须手工用 C 写一个动态链接库,在 C 语言中映射 Java 的数据类型。

JNA 中,它提供了一个动态的 C 语言编写的转发器,可以自动实现 Java 和 C 的数据类型映射,你不再需要编写 C 动态链接库。

也许这也意味着,使用 JNA 技术比使用 JNI 技术调用动态链接库会有些微的性能损失。但总体影响不大,因为 JNA 也避免了 JNI 的一些平台配置的开销。

代码案例

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

2、ByteBuffer 的这种方式,受到 MaxDirectMemorySize 参数的大小限制,其实底层是:

为什么要使用直接内存?

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

1、减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作。

2、加快了复制的速度。因为堆内在 flush 到远程时,会先复制到直接内存(非堆内存),然后再发送,而堆外内存相当于省略掉了这个工作。

3、可以在进程间共享,减少 JVM 间的对象复制,使得 JVM 的分割部署更容易实现。

4、可以扩展至更大的内存空间。比如超过 1TB 甚至比主存还大的空间。

直接内存的另一面

直接内存有很多好处,我们还是应该要了解它的缺点:

1、 直接内存难以控制,如果内存泄漏,那么很难排查;

2、 直接内存相对来说,不适合存储很复杂的对象。一般简单的对象比较适合。

直接内存案例和场景分析

内存泄漏案例

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

程序将会申请 1kb 的随机字符串,然后不停解压。为了避免让操作系统陷入假死状态,我们每次都会判断操作系统内存使用率,在达到 60% 的时候,我们将挂起程序(不在解压,只不断的让线程休眠)通过访问 8888 端口,将会把内存阈值提高到 85%。

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

使用以下命令把程序跑起来:

java -cp ref-jvm3.jar -XX:+PrintGC -Xmx1G -Xmn1G -XX:+AlwaysPreTouch -XX:MaxMetaspaceSize=10M -XX:MaxDirectMemorySize=10Mex15.LeakProblem 。

参数解释:

分别使用 Xmx、MaxMetaspaceSize、MaxDirectMemorySize 这三个参数限制了堆、元空间、直接内存的大小。

AlwaysPreTouch 这个参数,在 JVM 启动的时候,就把它所有的内存在操作系统分配了,默认情况下,此选项是禁用的,并且所有页面都在 JVM 堆空间填充时提交。我们为了减少内存动态分配的影响,把这个值设置为 True。

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

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

VIRT:virtual memory usage 虚拟内存。

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

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

RES:resident memory usage 常驻内存 达到了 1.5G 。

如果申请 100m 的内存,实际使用 10m,它只增长 10m,与 VIRT 相反。

常规排查方式

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

我们发现这个 3468 的 java 进程,占据的堆空间是比较小的,合计数远远小于 top 命令看到的 1.5G;

我们怀疑是不是虚拟机栈占用过高。于是使用 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 内部内存使用情况的一个功能。我们可以利用 jcmd(jdk 自带)这个工具来访问 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 了,继续排查就是在干操作系统和其他的语言相关的问题排查了),如果你有时间,有兴趣,我推荐你使用 perf 这个工具,这个工具安装很容易,但是容易遇到操作系统内核一些功能没支持,也分析不了,这里我就不多去花时间去分析:

perf 安装:yum install perf 。

内存泄漏问题解决

主要的是解决问题。

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

curl http://127.0.0.1:8888/

特意这么做也是为了方便你的观察,通过内存的增长我们可以大致知道问题的点。

问题关键点

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

问题修复

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

直接内存总结

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

其他堆外内存(直接内存),主要是指使用了 Unsafe 或者其他 JNI 手段直接直接申请的内存。这种情况下没有任何参数能够阻挡它们,要么靠它自己去释放一些内存,要么等待操作系统对它来处理。

所以如果你对操作系统底层以及内存分配使用不熟悉,最好不要使用这块,尤其是 Unsafe 或者其他 JNI 手段直接直接申请的内存. 那为什么要讲呢,EhCache 这种缓存框架,提供了多种策略,可以设定将数据存储在非堆上。

还有像 RocketMQ 都走了堆外分配,所以我们又必须要去了解他。

JVM 源码分析

使用 SourceInsight 来查看 OpenJDK 源代码了;

如何查看可以见一下文档。工具使用的 SourceInsight:https://cloud.tencent.com/developer/article/1585224 。

堆外内存默认是多大

如果我们没有通过-XX:MaxDirectMemorySize 来指定最大的堆外内存,那么默认的最大堆外内存是多少呢?

一般来说,如果没有显示的设置-XX:MaxDirectMemorySize 参数,通过 ByteBuffer 能够分配的直接内存空间大小就是堆的最大大小。

对应参数-Xmx,真的是这么样吗?

案例分析

1、VM 参数配置:-XX:MaxDirectMemorySize=100m:

2、 VM 参数配置:-XX:MaxDirectMemorySize=128m:

3、 VM 参数配置:-Xmx128m

4、 VM 参数配置:-Xmx135m -Xmn100m -XX:SurvivorRatio=8

5、 VM 参数配置:-Xmx138m -Xmn100m -XX:SurvivorRatio=8

几个案例分析,我们得出以下结论:

没有显示的设置-XX:MaxDirectMemorySize 参数,通过 ByteBuffer 能够分配的直接内存空间大小就是堆的最大大小。

这句话是有毛病的。

毛病在哪里,应该是没有显示的设置-XX:MaxDirectMemorySize 参数,通过 ByteBuffer 能够分配的直接内存空间大小就是堆的最大的可使用的大小。

堆的最大的可使用的大小= 堆的最大值减去一个 Survivor 的大小(预留的空间)。

所以案例 4 为什么会 OOM?

堆的最大的可使用的大小=135-10m=125m ,不能分配 128M 的对象;

所以案例 5 为什么不会 OOM!

堆的最大的可使用的大小=138-10m=128m ,刚好可以分配 128M 的对象。

源码分析

我们从代码出发,依次来找:

看到上面的代码之后不要误以为默认的最大值是 64M?其实不是的。

说到这个值得从 java.lang.System 这个类的初始化说起;

上 面 这 个 方 法 在 jvm 启 动 的 时 候 对 System 这 个 类 做 初 始 化 的 时 候 执 行 的 , 因 此 执 行 时 间 非 常 早 , 我 们 看 到 里 面 调 用 了sun.misc.VM.saveAndRemoveProperties(props):

这个地方是一个 native 方法,这个是一个本地方法,本地方法里面怎么实现的。

这个地方就需要看 JVM 的源码。

像这种本地方法,在 VM 的源码中一般都是会把包名加上,因为是给 java 用的所以前缀上还有一个 java。

大致推算出(其实已经知道了)是 JVM 的这个函数 :Java_java_lang_Runtime_maxMemory 。

这个容量其实就是每一个代的大小相加,比如 YGen+OldGen 之类。

在这里可以看到,新生代的最大值 = 新生代的最大值减去一个 survivor 的大小。

为什么会这样,因为在新生代采用复制回收算法,一个幸存者区域是浪费的,所以实际空间最大大小要减去一个交换器的大小。

而老年代是没有空间浪费的,所以还是全区域。

也得出我们设置的-Xmx 的值里减去一个 survivor 的大小就是默认的堆外内存的大小。

总结

读 JVM 的源码确实可以解决不少问题,但读 JVM 的源码门槛很高(C++的基础),同时 JVM 的源码体系非常大,如果想学源码的话,可以参考今天这个场景,从场景切入(找几个简单的场景),读源码要有目的,这样才能做到有的放矢,才能真正的提高自己的技术水平。

java直接内存为什么快_直接内存与 JVM 源码分析相关推荐

  1. Java的wait()、notify()学习三部曲之一:JVM源码分析

    原文链接:https://blog.csdn.net/boling_cavalry/article/details/77793224 综述 Java的wait().notify()学习三部曲由三篇文章 ...

  2. Java定时任务(一) Timer及TimerTask的案例解析及源码分析

    Java定时任务(一)  Timer及TimerTask的案例解析及源码分析 一.概述: 定时任务这个概念在Java的学习以及项目的开发中并不陌生,应用场景也是多种多样.比如我们会注意到12306网站 ...

  3. Java熔断框架有哪些_降级熔断框架 Hystrix 源码解析:滑动窗口统计

    降级熔断框架 Hystrix 源码解析:滑动窗口统计 概述 Hystrix 是一个开源的降级熔断框架,用于提高服务可靠性,适用于依赖大量外部服务的业务系统.什么是降级熔断呢? 降级 业务降级,是指牺牲 ...

  4. Java并发基础:了解无锁CAS就从源码分析

    CAS的全称为Compare And Swap,直译就是比较交换.是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,在i ...

  5. Java并发基础:了解无锁CAS就从源码分析 1

    CAS的全称为Compare And Swap,直译就是比较交换.是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,在i ...

  6. Java Review - 线程池资源一直不被释放案例源码分析

    文章目录 概述 问题复现 源码分析 小结 概述 在日常开发中为了便于线程的有效复用,经常会用到线程池,然而使用完线程池后如果不调用shutdown关闭线程池,则会导致线程池资源一直不被释放. 下面通过 ...

  7. 3 v4 中心节点固定_死磕以太坊源码分析之p2p节点发现

    死磕以太坊源码分析之p2p节点发现 在阅读节点发现源码之前必须要理解kadmilia算法,可以参考:KAD算法详解. 节点发现概述 节点发现,使本地节点得知其他节点的信息,进而加入到p2p网络中. 以 ...

  8. java usb摄像头_Android中多USB摄像头解决方案——UVCCamera源码分析(一)

    前言 前段时间捣鼓多USB摄像头的方案,一阵手忙脚乱算是勉强跑起来了.整个流程主要还是依赖于网上大神们封装好的库.之前想仔细分析一下整套底层实现,然而一直拖到现在--也没有完全看完,于是想着干脆分阶段 ...

  9. Java分布式跟踪系统Zipkin(六):Brave源码分析-Brave和SpringBoot整合

    所有博文均在个人独立博客http://blog.mozhu.org首发,欢迎访问! Zipkin是用当下最流行的SpringBoot开发的,SpringBoot将Spring项目的开发过程大大简化,一 ...

最新文章

  1. 【FFmpeg】自定义回调函数处理AVIOContext中的数据
  2. 计算机科学与编程基础,国外经典教材·计算机科学与技术:Oracle 10g编程基础
  3. Java语言类的作用,java 语言Class类的作用,怎么使用?
  4. NoSQL-MongoDB with python
  5. 计算机系统基础 数据的表示和存储
  6. 【十六】Jmeter:目录介绍
  7. 来吧,给自己提个醒,哭着复习一下当初没好好学习的内容。
  8. 标书导出html,Python爬取比比网中标标书并保存成PDF格式
  9. WINCC报表 VBS脚本链接SQL Server数据库 日报月报 导出EXCEL PDF
  10. 利用pm2 启动node项目
  11. 2022西工大网络安全知识竞赛赛后回顾资料
  12. linux系统怎么关闭屏保,Linux关闭屏保
  13. 苹果手机显示无法与服务器建立安全连接,苹果手机那个safari浏览器无法与访问器建立安全链接是什么意思...
  14. 【商业源码】生日大放送-Newlife商业源码分享
  15. HTMLCSSHTTP
  16. wifi模块微信小程序AP配网(UDP)
  17. 笔记:图解系统(小林coding)
  18. 使用 MoveIt 控制自己的真实机械臂【4】——了解 MoveIt 的轨迹规划实现机制
  19. 麻省理工计算机专业毕业就业,中国版的麻省理工大学,工科实力与清华比肩,备受业界认可...
  20. html 苹果 地图,iOS谷歌地图全景显示

热门文章

  1. mysql登陆提示ERROR 1045 (28000): Access denied for user
  2. 深入了解Oracle前滚恢复rolling forward(一)
  3. 为 protocol 中属性添加默认值
  4. mysql之 表数据存放路径非datadir目录
  5. 论政府开放数据的意义
  6. php 中文转拼音,可以只转首字母,可以设置utf8、gbk
  7. 用正则表达式判断一个二进制数是否能被3整除
  8. window.showModalDialog
  9. 网络工程师专业词汇解释(路由器)
  10. 批处理系统服务监控器[超牛B]