最近经常有人问我在Java中使用堆外(off heap)内存的好处与用途何在。我想其他面临几样选择的人应该也会对这个答案感兴趣吧。

堆外内存其实并无特别之处。线程栈,应用程序代码,NIO缓存用的都是堆外内存。事实上在C或者C++中,你只能使用未托管内存,因为它们默认是没有托管堆(managed heap)的。在Java中使用托管内存或者“堆”内存是这门语言的一个特性。注意:Java并非唯一这么做的语言。

new Object() vs 对象池 vs 堆外内存

1、new Object()

在Java 5.0以前,对象池一度非常流行。那个时候创建对象的开销是非常昂贵的。然而,从Java 5.0以后,对象创建及垃圾回收已经变得非常廉价了,开发人员发现性能得到了提升后,便简化了代码,废弃了对象池,需要的时候就去创建新的对象就好了。在Java 5.0以前,几乎所有对象,包括对象池本身,都通过对象池来提升性能,而在5.0以后,只有那些特别昂贵的对象才有必要池化了,比方说线程,Socket,以及数据库连接。

2、对象池

在低时延领域它仍是有一定的用武之处的,由于可变对象的循环使用减轻了CPU缓存的压力,进而使得性能得到了提升。这些对象的生命周期和结构都必须尽可能简单,但这么做之后你会发现系统性能及抖动都会得到大幅度的改善。

还有一个领域也比较适合使用对象池,譬如需要加载海量数据且其中包含许多冗余对象时。使用对象池能显著减少内存的使用量以及需要GC的对象数,进而换来更短的GC时间以及更高的吞吐量。

这类对象池通常都会设计得比较轻量级,而非简单地使用一个同步的HashMap,因此它们仍是有存在的价值的。

StringInterner类来作一个例子。你可以将一个包含你想要的文本的可重复使用的可变StringBuilder作为参数传给它,它会返回你一个匹配的字符串。直接传递String对象的效率会很低,因为你已经把这个对象创建出来了。StringBuilder则是可以重复使用的。

注意:这个结构有一个很有意思的特性就是它不需要额外的线程安全的机制,比方说volatile或者synchronized,仅需Java所保障的最低限度的线程安全就足够了。你能正确地访问到String内部的final字段,顶多就是读到了不一致的引用而已。

public class StringInterner {
privatefinal String[] interner;
privatefinal int mask;
publicStringInterner(intcapacity) {
intn = Maths.nextPower2(capacity, 128);
interner = newString[n];
mask = n - 1;
} private static boolean isEqual(@NullableCharSequence s, @NotNullCharSequence cs) {
if(s == null)returnfalse;
if(s.length() != cs.length()) returnfalse;
for(inti = 0; i < cs.length(); i++)
if(s.charAt(i) != cs.charAt(i))
returnfalse;
returntrue;
} @NotNull
public String intern(@NotNullCharSequence cs) {
longhash = 0;
for(inti = 0; i < cs.length(); i++)
hash = 57* hash + cs.charAt(i);
inth = (int) Maths.hash(hash) & mask;
String s = interner[h];
if(isEqual(s, cs))
returns;
String s2 = cs.toString();
returninterner[h] = s2;
}
}

3、堆外内存的使用

使用堆外内存与对象池都能减少GC的暂停时间,这是它们唯一的共同点。生命周期短的可变对象,创建开销大,或者生命周期虽长但存在冗余的可变对象都比较适合使用对象池。生命周期适中,或者复杂的对象则比较适合由GC来进行处理。然而,中长生命周期的可变对象就比较棘手了,堆外内存则正是它们的菜。

3.1、ehcache、memcache中都有堆外内存的使用。

分别见《ehcache基本原理》中的“

ehcache存储方式

1、堆内存储:速度快,但是容量有限。

2、堆外(OffHeapStore)存储:被称为BigMemory,只在企业版本的Ehcache中提供,原理是利用nio的DirectByteBuffers实现,比存储到磁盘上快,而且完全不受GC的影响,可以保证响应时间的稳定性;但是direct buffer的在分配上的开销要比heap buffer大,而且要求必须以字节数组方式存储,因此对象必须在存储过程中进行序列化,读取则进行反序列化操作,它的速度大约比堆内存储慢一个数量级。

(注:direct buffer不受GC影响,但是direct buffer归属的的JAVA对象是在堆上且能够被GC回收的,一旦它被回收,JVM将释放direct buffer的堆外空间。)”

3.2、spark

见《Spark Tungsten in-heap / off-heap 内存管理机制

4、堆外内存的优点和缺点

堆外内存,其实就是不受JVM控制的内存。相比于堆内内存有几个优势: 
  1 、减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作(可能使用多线程或者时间片的方式,根本感觉不到) 。
  2 、加快了复制的速度。因为堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作。 
  3、可以在进程间共享,减少JVM间的对象复制,使得JVM的分割部署更容易实现。
  4、可以扩展至更大的内存空间。比如超过1TB甚至比主存还大的空间。

 而福之祸所依,自然也有不好的一面: 
  1 堆外内存难以控制,如果内存泄漏,那么很难排查 
  2 堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合。

站在系统设计的角度来看,使用堆外内存可以为你的设计提供更多可能。最重要的提升并不在于性能,而是决定性的。

堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送的说明:

HeapByteBuffer与DirectByteBuffer,在原理上,前者可以看出分配的buffer是在heap区域的,其实真正flush到远程的时候会先拷贝得到直接内存,再做下一步操作(考虑细节还会到OS级别的内核区直接内存),其实发送静态文件最快速的方法是通过OS级别的send_file,只会经过OS一个内核拷贝,而不会来回拷贝;在NIO的框架下,很多框架会采用DirectByteBuffer来操作,这样分配的内存不再是在java heap上,而是在C heap上,经过性能测试,可以得到非常快速的网络交互,在大量的网络交互下,一般速度会比HeapByteBuffer要快速好几倍。

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。 
在JDK 1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java 堆和Native 堆中来回复制数据。

import sun.nio.ch.DirectBuffer;

import java.nio.ByteBuffer;

public class Main {

    public static void main(String[] args) throws InterruptedException {
System.out.println("Hello World!");
ByteBuffer bb = ByteBuffer.allocateDirect(1024 * 1024 * 128);
Thread.sleep(10000);
((DirectBuffer)bb).cleaner().clean();
Thread.sleep(10000);
}
}

堆外内存及测试

高性能计算领域最大的一个难点在于重现那些隐蔽的BUG,并证实问题已经得到修复。通过将输入事件及数据以持久化的形式存储到堆外内存中,你可以将你的关键系统变成一系列的复杂状态机。(简单的情况下只有一个状态机)。这样的话在测试环境便能够复现出生产环境出现的行为及性能问题了。

许多投行都通过这项技术来可靠地重现当天系统对某个事件的响应,并分析出该事件之所以这么处理的原因。更为重要的是,你能够立即证明线上的故障已经得到了解决,而不是发现一个问题后,寄希望于它就是引发线上故障的根源。确定性的行为还伴随着确定性的性能。

你可以在测试环境中按照真实的时间来回放事件,由此得到的时延分布也必定是生产环境中所出现的。由于硬件的不同,一些系统的抖动可能难以复现,不过这在数据分析的角度而言已经相当接近真实的情况了。为了避免出现花一整天的时间来回话前一天的数据的情况,你还可以增加一个阈值,比方说,如果两个事件的间隔超过10ms的话你可以就只等待10ms。这样你能够在一个小时内根据实际的时间来回放出一天的事件,来检查下你的改动是否对时延分布有所改善。

这样做是否就损失了“一次编译,处处执行”的好处了?

一定程度上来讲是这样的,但其实的影响比你想像的要小得多。越接近处理器,你就更依赖于处理器或者操作系统的行为。所幸的是,绝大多数系统使用的都是AMD/Intel的CPU,甚至是ARM处理器在底层上也越来越与这两家兼容了。操作系统之间也存在差别,因此相对于Windows而言,这项技术更适合在Linux系统上使用。如果你是在Mac OS X或者Windows上开发,然后生产环境是部署在Linux上的话,就一点问题都没有了。我们在Higher Frequency Trading中也是这么做的。

使用堆外内存会引入什么新的问题

天下没有免费的午餐,堆外内存也不例外。最大的问题在于你的数据结构变得有些别扭。要么就是需要一个简单的数据结构以便于直接映射到堆外内存,要么就使用复杂的数据结构并序列化及反序列化到内存中。很明显使用序列化的话会比较头疼且存在性能瓶颈。使用序列化比使用堆对象的性能还差。

在金融领域,许多高频率的数据都是扁平的简单结构,全部由基础类型组成,非常适合映射到堆外内存。然而,并非所有的应用程序都是这样的,可能会有一些嵌套得很深的数据结构,比如说图,你还不得不将这些对象缓存在堆上。

另外一个问题就是JVM会制约到你对操作系统的使用。你不用再担心JVM会给系统造成过重的负载。使用堆外内存后,某些限制已经不复存在了,你可以使用比主存还大的数据结构,不过如果你这么做的话又得考虑一下使用的是什么磁盘子系统了。比如说,你肯定不会希望分页到一块只有80 IOPS(Input/Ouput Operations per Second,每秒的IO操作)的HDD硬盘上,最好是IOPS能到80,000的SSD硬盘,当然了,1000x的话更好。

OpenHFT能做些什么?

OpenHFT包含许多类库,它们向你屏蔽了使用本地内存来存储数据的细节。这些数据结构都是持久化的,使用它们不会产生垃圾或者只有很少。使用了它的应用程序可以运行一整天也没有一次Minor GC.

Chronicle Queue——持久化的事件队列。支持同一台机器上多个JVM的并发写,以及多台机器间的并发读。微秒级的延迟,并能持续保持每秒上百万消息的吞吐量。

Chronicle Map——kv表的本地或持久化存储。它能在同一台机器的不同JVM间共享,数据是通过UDP或者TCP来复制的,并通过TCP来进行远程访问。微秒级的延迟,单台机器能保持每秒百万级的读写操作。

Thread Affinity ——将关键线程绑定到独立的CPU核或者逻辑CPU上,以减少系统抖动。抖动可以减小到原来的千分之一。

Java堆外内存之一:堆外内存场景介绍(对象池VS堆外内存)的更多相关文章

  1. Android内存泄漏第二课--------(集合中对象没清理造成的内存泄漏 )

    一.我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大.如果这个集合是static的话,那情况就更严重 ...

  2. 抓到 Netty 一个隐藏很深的内存泄露 Bug | 详解 Recycler 对象池的精妙设计与实现

    欢迎关注公众号:bin的技术小屋,如果大家在看文章的时候发现图片加载不了,可以到公众号查看原文 本系列Netty源码解析文章基于 4.1.56.Final版本 最近在 Review Netty 代码的 ...

  3. iOS App内存优化之 解决UIImagePickerController的图片对象占用RAM过高问题

    这个坑会在特定的情况下特别明显: 类似朋友圈的添加多张本地选择\拍照 的图片 并在界面上做一个预览功能 由于没有特别的相机\相册需求,则直接使用系统自带的UIImagePickerController ...

  4. [转]Java中继承、多态、重载和重写介绍

    什么是多态?它的实现机制是什么呢?重载和重写的区别在那里?这就是这一次我们要回顾的四个十分重要的概念:继承.多态.重载和重写. 继承(inheritance) 简单的说,继承就是在一个现有类型的基础上 ...

  5. Java 中的对象池实现

    点赞再看,动力无限.Hello world : ) 微信搜「程序猿阿朗 」. 本文 Github.com/niumoo/JavaNotes 和 未读代码博客 已经收录,有很多知识点和系列文章. 最近在 ...

  6. 【Spark篇】---Spark调优之代码调优,数据本地化调优,内存调优,SparkShuffle调优,Executor的堆外内存调优

    一.前述 Spark中调优大致分为以下几种 ,代码调优,数据本地化,内存调优,SparkShuffle调优,调节Executor的堆外内存. 二.具体    1.代码调优 1.避免创建重复的RDD,尽 ...

  7. [二]Java虚拟机 jvm内存结构 运行时数据内存 class文件与jvm内存结构的映射 jvm数据类型 虚拟机栈 方法区 堆 含义

    前言简介 class文件是源代码经过编译后的一种平台中立的格式 里面包含了虚拟机运行所需要的所有信息,相当于 JVM的机器语言 JVM全称是Java Virtual Machine  ,既然是虚拟机, ...

  8. C++——内存对象 禁止产生堆对象 禁止产生栈对象

    用C或C++写程序,需要更多地关注内存,这不仅仅是因为内存的分配是否合理直接影响着程序的效率和性能,更为主要的是,当我们操作内存的时候一不小心就会出现问题,而且很多时候,这些问题都是不易发觉的,比如内 ...

  9. NET的堆和栈04,对托管和非托管资源的垃圾回收以及内存分配

    在" .NET的堆和栈01,基本概念.值类型内存分配"中,了解了"堆"和"栈"的基本概念,以及值类型的内存分配.我们知道:当执行一个方法的时 ...

随机推荐

  1. mac下webstorm添加scss watcher

    一.前提条件: 1.安装ruby,如果我没记错的话,mac自带ruby,终端输入 ruby -v ,回车,如果显示ruby的版本号,则说明ruby环境已经安装好了.如果没有,自行安装ruby.例如我的 ...

  2. C++面向对象高级编程(八)模板

    技术在于交流.沟通,转载请注明出处并保持作品的完整性. 这节课主要讲模板的使用,之前我们谈到过函数模板与类模板 (C++面向对象高级编程(四)基础篇)这里不再说明 1.成员模板 成员模板:参数为tem ...

  3. [置顶] Deep Learning 学习笔记

    一.文章来由 好久没写原创博客了,一直处于学习新知识的阶段.来新加坡也有一个星期,搞定签证.入学等杂事之后,今天上午与导师确定了接下来的研究任务,我平时基本也是把博客当作联机版的云笔记~~如果有写的不 ...

  4. postgresql与Oracle:空字符串与null

    空字符串:两个单引号,中间无空格等任何内容 在postgresql中,空字符串与null是不同的:而oracle中,空字符串与null等同.测试如下: postgresql中: postgres=# ...

  5. python中继承和多态

    继承和多态 继承 引入继承 我们有这样一个需求 模仿英雄联盟定义两个英雄类 1.英雄要有昵称.攻击力.生命值属性 2.实例化出两个英雄对象 3.英雄之间可以互殴,被殴打的一方掉血,血量小于0则判断为死 ...

  6. caffe学习记录2——blobs

    参考:caffe官网  2016-01-23 10:08:22 1 blobs,layers,nets是caffe模型的骨架 2 blobs是作者写好的数据存储的“容器”,可以有效实现CPU和GPU之 ...

  7. css工具类封装

    温馨提示:一下css封装,建议按需使用,否则会造成很大的代码冗余,且很多样式会造成不符合预期的效果,建议合理使用 <a href="https://meyerweb.com/eric/ ...

  8. BD09坐标(百度坐标) WGS84(GPS坐标) GCJ02(国测局坐标) 的相互转换

    BD09坐标(百度坐标) WGS84(GPS坐标) GCJ02(国测局坐标) 的相互转换 http://www.cnphp6.com/archives/24822 by root ⋅ Leave a ...

  9. 优化器Optimizer

    目前最流行的5种优化器:Momentum(动量优化).NAG(Nesterov梯度加速).AdaGrad.RMSProp.Adam,所有的优化算法都是在原始梯度下降算法的基础上增加惯性和环境感知因素进 ...

  10. a链接嵌套无效,嵌套链接最优解决办法

    <a>不支持嵌套.例如: <a href="#1">11111111111<a href="#2">22222222222& ...