系列文章目录前言一、JVM内存管理在大数据场景下的问题
1.有效数据密度低2.垃圾回收1.OOM问题影响稳定性1.缓存未命中问题 二、自主内存管理
堆上内存的问题堆外内存的不足之处
前言 Java语言的好处是不用考虑底层,JVM可以对代码进行深度优化,对内存资源进行管理,自动回收内存。但是自动内存管理的问题在于不可控,基于JVM的大数据引擎常常会面临一个问题,即在处理海量数据的时候,如何在内存中存储大量的数据
一、JVM内存管理在大数据场景下的问题 1.有效数据密度低
Java的对象在内存中的存储包含3个主要部分:对象头、实例数据、对齐填充部分。32位和64位的虚拟机中对象头分别需要占用32bit和64bit。实例数据是实际的数据存储。为了提高效率,内存中数据存储不是连续的,而是按照8byte的整数倍进行存储。例如,只有一个boolean字段的类实例占16byte:头信息占8byte,boolean占1byte,为了对齐达到8的倍数会额外占用7byte。这就导致在JVM中有效信息的存储密度很低。 2.垃圾回收
JVM的内存回收机制的优点和缺点同样明显,优点是开发者无须资源回收,提高开发效率,减少了内存泄漏的可能。但是内存回收是不可控的,在大数据计算的场景中,这个缺点被放大,TB、PB级的数据计算需要消耗大量的内存,在内存中产生海量的Java对象,一旦出现Full GC,GC会达到秒级甚至分钟级,直接影响执行效率。GC带来的中断会使集群中的心跳信息超时,导致节点被踢出集群,整个集群进入不稳定的状态。虽然通过JVM参数的调优可以提升回收效率,尽量减少Full GC,但是仍然不能避免这个问题,精确的调优也非常困难。 1.OOM问题影响稳定性
OutOfMemoryError是分布式计算框架经常会遇到的问题,当JVM中所有对象大小超过分配给JVM的内存大小时,就会发生OutOfMemoryError错误,导致JVM崩溃,分布式框架的健壮性和性能都会受到影响。 1.缓存未命中问题
CPU进行计算的时候,是从CPU缓存中获取数据,而不是直接从内存获取数据。CPU有分L1和L2/3级缓存。L1小,一般为32KB,L3大,能达到32MB。缓存的理论基础是程序局部性原理,包括时间局部性和空间局部性:最近被CPU访问的数据,短期内CPU还要访问(时间);被CPU访问的数据附近的数据,CPU短期内还要访问(空间)。Java对象在堆上存储的时候并不是连续的,所以从内存中读取Java对象时,缓存的邻近的内存区域的数据往往不是CPU下一步计算所需要的,这就是缓存未命中。此时CPU需要空转等待从内存中重新读取数据,CPU的速度和内存的速度之间差好几个数量级,导致CPU没有充分利用起来。如果数据没有在内存中,而是需要从磁盘上加载,那么执行效率就会变得惨不忍睹。
不同硬件的访问延迟如下图所示:
下面主要介绍Flink是如何解决上面的问题的,主要包括内存管理、定制的序列化工具、缓存友好的数据结构和算法、堆外内存等。
在Flink中,Java对象的有效信息被序列化为二进制数据流,在内存中连续存储,保存在预分配的内存块上,内存块叫作MemorySegment。MemorySegment是内存分配的最小单元,是一段固定长度的内存(默认大小为32KB),并且提供了非常高效的读写方法,很多运算可以直接操作二进制数据,不需要反序列化即可执行。MemorySegment可以保存在堆上,其内部存储为一个Java byte数组,也可以保存在堆外的ByteBuffer中。每条记录都会以序列化的形式存储在一个或多个Memory Segment中。Flink早期版本使用的是堆上内存,在堆内存上管理序列化之后的数据。如果需要处理的数据超出了内存限制,则会将部分数据存储到硬盘上。操作多块MemorySegment就像操作一块大的连续内存一样,Flink会使用逻辑视图(AbstractPagedInputView)以方便操作。但使用堆上内存,仍然不是完全自主的内存管理,还存在以下问题。 堆上内存的问题
超大内存(上百GB)JVM的启动需要很长时间,Full GC可以达到分钟级。使用堆外内存,可以将大量的数据保存在堆外,极大地减小堆内存,避免GC和内存溢出的问题。高效的IO操作。堆外内存在写磁盘或网络传输时是zero-copy,而堆上内存则至少需要1次内存复制。堆外内存是进程间共享的。也就是说,即使JVM进程崩溃也不会丢失数据。这可以用来做故障恢复(Flink暂时没有利用这项功能,不过未来很可能会去做)。 堆外内存的不足之处
堆外内存虽然有更好的性能和更可控的内存管理,但是也存在几个问题:
堆上内存的使用、监控、调试简单,堆外内存出现问题后的诊断则较为复杂。Flink有时需要分配短生命周期的MemorySegment,在堆外内存上分配比在堆上内存开销更高。在Flink的测试中,部分操作在堆外内存上会比堆上内存慢。
同时为了提高效率,Flink在计算中采用了DBMS的Sort和Join算法,直接操作二进制数据,避免数据反复序列化带来的开销。Flink的内部实现更像C/C++ 而非Java。