yeskery

ByteBuffer 与 DirectByteBuffer 在堆外内存的应用

前置知识

堆外内存

堆外内存是相对于堆内内存的一个概念。堆内内存是由 JVM 所管控的 Java 进程内存,我们平时在 Java 中创建的对象都处于堆内内存中,并且它们遵循 JVM 的内存管理机制,JVM 会采用垃圾回收机制统一管理它们的内存。那么堆外内存就是存在于 JVM 管控之外的一块内存区域,因此它是不受 JVM 的管控。

在讲解 DirectByteBuffer 之前,需要先简单了解两个知识点。java 引用类型,因为 DirectByteBuffer 是通过虚引用(Phantom Reference)来实现堆外内存的释放的。PhantomReference 是所有“弱引用”中最弱的引用类型。不同于软引用和弱引用,虚引用无法通过 get() 方法来取得目标对象的强引用从而使用目标对象,观察源码可以发现 get() 被重写为永远返回 null。

那虚引用到底有什么作用?其实虚引用主要被用来 跟踪对象被垃圾回收的状态,通过查看引用队列中是否包含对象所对应的虚引用来判断它是否 即将被垃圾回收,从而采取行动。它并不被期待用来取得目标对象的引用,而目标对象被回收前,它的引用会被放入一个 ReferenceQueue 对象中,从而达到跟踪对象垃圾回收的作用。

关于linux的内核态和用户态

  • 内核态:控制计算机的硬件资源,并提供上层应用程序运行的环境。比如socket I/0操作或者文件的读写操作等
  • 用户态:上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源
  • 系统调用:为了使上层应用能够访问到这些资源,内核为上层应用提供访问的接口

因此我们可以得知当我们通过JNI调用的native方法实际上就是从用户态切换到了内核态的一种方式。并且通过该系统调用使用操作系统所提供的功能。

Q:为什么需要用户进程(位于用户态中)要通过系统调用(Java中即使JNI)来调用内核态中的资源,或者说调用操作系统的服务了?
A:Intel X86 cpu 提供Ring0-Ring3四种级别的运行模式,Ring0级别最高,Ring3最低。Linux使用了Ring3级别运行用户态,Ring0作为内核态。Ring3状态不能访问Ring0的地址空间,包括代码和数据。普通的应用程序只能运行在Ring3,并且不能访问Ring0的地址空间。操作系统运行在Ring0,并提供系统调用供用户态的程序使用。如果用户态的程序的某一个操作需要内核态来协助完成(例如读取磁盘上的某一段数据),那么用户态的程序就会通过系统调用来调用内核态的接口,请求操作系统来完成某种操作。因此用户态是没有权限去操作内核态的资源的,它只能通过系统调用外完成用户态到内核态的切换,然后在完成相关操作后再有内核态切换回用户态。

Java 操作堆外内存

Unsafe类

sun.misc.Unsafe 提供了一组方法来进行堆外内存的分配,重新分配,以及释放。

public native long allocateMemory(long size); —— 分配一块内存空间。
public native long reallocateMemory(long address, long size); —— 重新分配一块内存,把数据从 address 指向的缓存中拷贝到新的内存块。
public native void freeMemory(long address); —— 释放内存。

然而 Unsafe 类的构造器是私有的,报错。而且,allocateMemory 方法也不是静态的,不能通过 Unsafe.allocateMemory 调用。

  1. Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
  2. theUnsafe.setAccessible(true);
  3. Unsafe unsafe = (Unsafe) theUnsafe.get(null);
  4. long a= unsafe.allocateMemory(1024);
  5. long b= unsafe.allocateMemory(2048);
  6. unsafe.reallocateMemory(a, 1024);
  7. unsafe.reallocateMemory(b, 1024);
  8. unsafe.freeMemory(a);
  9. unsafe.freeMemory(b);

ByteBuffer 类

下面是创建 ByteBuffer 对象的几种方式:

返回类型 方法
static ByteBuffer allocate(int capacity)
static ByteBuffer allocateDirect(int capacity)
static ByteBuffer wrap(byte[] array)
static ByteBuffer wrap(byte[] array, int offset, int length)

allocate方式创建的ByteBuffer对象我们称之为非直接缓冲区,这个ByteBuffer对象(和对象包含的缓冲数组)都位于JVM的堆区。wrap方式和allocate方式创建的ByteBuffer没有本质区别,都创建的是非直接缓冲区。

allocateDirect方法创建的ByteBuffer我们称之为直接缓冲区,此时ByteBuffer对象本身在堆区,而缓冲数组位于非堆区, ByteBuffer对象内部存储了这个非堆缓冲数组的地址。在非堆区的缓冲数组可以通过JNI(内部还是系统调用)方式进行IO操作,JNI不受gc影响,机器码执行速度也比较快,同时还避免了JVM堆区与操作系统内核缓冲区的数据拷贝,所以IO速度比非直接缓冲区快。然而allocateDirect方式创建ByteBuffer对象花费的时间和回收该对象花费的时间比较多,所以这个方法适用于创建那些需要重复使用的缓冲区对象。

ByteBuffer 属性和方法

ByteBuffer对象三个重要属性 position, limit和capacity。其中capacity表示了缓冲区的总容量,始终保持不变,初始时候position 等于 0 , limit 等于 capacity。

put:向缓冲区放入数据

返回类型 方法
abstract ByteBuffer put(byte b)
ByteBuffer put(byte[] src)
ByteBuffer put(byte[] src, int offset, int length)

调用put方法前,limit应该等于capacity,如果不等于,几乎可以肯定我们对缓冲区的操作有误。在put方法中0到position-1的区域表示有效数据,position到limit之间区域表示空闲区域。put方法会从position的当前位置放入数据,每放入一个数据position增加1,当position等于limit(即空闲区域使用完)时还继续放入数据就会抛出BufferUnderflowException异常

get:从缓冲区读取数据

返回类型 方法
abstract byte get()
ByteBuffer get(byte[] dst)
ByteBuffer get(byte[] dst, int offset, int length)

在get方法中, 0到position-1的区域表示已读数据,position到limit之间的区域表示未读取的数据。每读取一个数据position增加1,当position等于limit时继续读取数据就会抛出BufferUnderflowException 异常。

flip :将写模式转换成读模式

  1. public final Buffer flip() {
  2. limit = position;
  3. position = 0;
  4. mark = -1;
  5. return this;
  6. }

clear:清空缓冲区,将读模式转换写模式

  1. public final Buffer clear() {
  2. position = 0;
  3. limit = capacity;
  4. mark = -1;
  5. return this;
  6. }

compact:保留未读取的数据,将读模式转换写模式

  1. public ByteBuffer compact() {
  2. int pos = position();
  3. int lim = limit();
  4. assert (pos <= lim);
  5. int rem = (pos <= lim ? lim - pos : 0);
  6. unsafe.copyMemory(ix(pos), ix(0), (long)rem << 0);
  7. position(rem);
  8. limit(capacity());
  9. discardMark();
  10. return this;
  11. }

mark:保存当前position的位置到mark变量

  1. public final Buffer mark() {
  2. mark = position;
  3. return this;
  4. }

rest:将position置为mark变量中的值

  1. public final Buffer reset() {
  2. int m = mark;
  3. if (m < 0)
  4. throw new InvalidMarkException();
  5. position = m;
  6. return this;
  7. }

mark方法和rest方法联合使用可实现从指定位置的重读。

rewind:从头开始重读

  1. public final Buffer rewind() {
  2. position = 0;
  3. mark = -1;
  4. return this;
  5. }

ByteBuffer对象使用时又很多需要注意的地方,自认为这个API设计的不是很友好。比如一定不能连续两次调用flip和compact方法,flip方法调用以后不能再调用put方法,等等。要避免这些错误,只能在使用ByteBuffer前弄清楚当前缓冲区中0到position-1以及position到limit中数据表示的含义,这才是避免bug的根本办法。

从上面的介绍中我们可以看出,ByteBuffer对象既可以读,也可以写。除非我们能保证在读操作一次性使用完ByteBuffer对象中的所有数据,并且保证写入ByteBuffer对象向中的内容全部写入完成,否则同时用于读写的ByteBuffer对象会造成数据的混乱和错误。一般来说,我们都会创建两个ByteBuffer对象向,一个用于接收数据,另一个用于发送数据。

ByteBuffer是面向字节的,为方便基本数据类型的读取,ByteBuffer中还提供getInt,putInt,getFloat,putFloat等方法,这些方法方便我们在缓冲区存取单个基本数据类型。如果需要从基本数据类型数组中写入到ByteBuffer中,或者从ByteBuffer中读取到基本数据类型的数组中,那么我们可以通过已创建好的ByteBuffer对象的asXxxBuffer方法创建基本数据类型的Buffer。

返回类型 方法
abstract CharBuffer asCharBuffer()
abstract DoubleBuffer asDoubleBuffer()
abstract FloatBuffer asFloatBuffer()
abstract IntBuffer asIntBuffer()
abstract LongBuffer asLongBuffer()

假设有如下代码:
IntBuffer intBufferObj = byteBufferObj.asIntBuffer();

此时intBufferObj和byteBufferObj对象共享底层的数组。但是比较坑爹的是两个buffer的position,limit是独立的,这样极易产生bug,需要引起我们注意。

DirectByteBuffer 类

DirectByteBuffer是Java用于实现堆外内存的一个重要类,我们可以通过该类实现堆外内存的创建、使用和销毁。

direct_byte_buffer_relation

使用下面一行代码就可以创建一个1024字节的DirectBuffer:
ByteBuffer.allocateDirect(1024);

该方法调用的是new DirectByteBuffer(int cap)。DirectByteBuffer的构造函数是包级私有的,因此外部是调用不到的。下面我们来看一下这行代码背后的逻辑:

  1. DirectByteBuffer(int cap) { // package-private
  2. super(-1, 0, cap, cap);
  3. boolean pa = VM.isDirectMemoryPageAligned(); //是否页对齐
  4. int ps = Bits.pageSize(); //获取pageSize大小
  5. long size = Math.max(1L, (long) cap + (pa ? ps : 0)); //如果是页对齐的话,那么就加上一页的大小
  6. Bits.reserveMemory(size, cap); //对分配的直接内存做一个记录
  7. long base = 0;
  8. try {
  9. base = unsafe.allocateMemory(size); //实际分配内存
  10. } catch (OutOfMemoryError x) {
  11. Bits.unreserveMemory(size, cap);
  12. throw x;
  13. }
  14. unsafe.setMemory(base, size, (byte) 0); //初始化内存
  15. //计算地址
  16. if (pa && (base % ps != 0)) {
  17. // Round up to page boundary
  18. address = base + ps - (base & (ps - 1));
  19. } else {
  20. address = base;
  21. }
  22. //生成Cleaner
  23. cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
  24. att = null;
  25. }

DirectBuffer的构造函数主要做以下三个事情:

  1. 根据页对齐和pageSize来确定本次的要分配内存实际大小
  2. 实际分配内存,并且记录分配的内存大小
  3. 声明一个Cleaner对象用于清理该DirectBuffer内存

需要注意的是DirectBuffer的创建是比较耗时的,所以在一些高性能的中间件或者应用下一般会做一个对象池,用于重复利用DirectBuffer。

DirectBuffer 的使用

查看DirectBuffer类的方法声明,对于DirectBuffer的使用主要有两类方法,putXXX和getXXX。

putXXX方法(以putInt为例):

  1. public ByteBuffer putInt(int x) {
  2. putInt(ix(nextPutIndex((1 << 2))), x);
  3. return this;
  4. }
  5. private ByteBuffer putInt(long a, int x) {
  6. if (unaligned) {
  7. int y = (x);
  8. unsafe.putInt(a, (nativeByteOrder ? y : Bits.swap(y)));
  9. } else {
  10. Bits.putInt(a, x, bigEndian);
  11. }
  12. return this;
  13. }

putInt方法会根据是否是内存对齐分别调用unsafe.putInt或者Bits.putInt来把数据放到直接内存中。Bits.putInt实际上会根据是大端或者是小端来区分如何把数据放到直接内存中,放的方式同样是调用unsage.putInt。

getXXX方法(以getInt为例):

  1. public int getInt() {
  2. return getInt(ix(nextGetIndex((1 << 2))));
  3. }
  4. private int getInt(long a) {
  5. if (unaligned) {
  6. int x = unsafe.getInt(a);
  7. return (nativeByteOrder ? x : Bits.swap(x));
  8. }
  9. return Bits.getInt(a, bigEndian);
  10. }

首先判断是否是页对齐,如果不是页对齐,那么直接通过unsafe.getInt来获取数据;如果是页对齐,那么通过Bits.getInt方法来获取数据。Bits.getInt同样是根据大端还是小端,调用unsafe.getInt来获取数据。

DirectByteBuffer 堆外内存的创建和回收的源码解读

堆外内存分配

  1. DirectByteBuffer(int cap) { // package-private
  2. super(-1, 0, cap, cap);
  3. boolean pa = VM.isDirectMemoryPageAligned();
  4. int ps = Bits.pageSize();
  5. long size = Math.max(1L, (long)cap + (pa ? ps : 0));
  6. // 保留总分配内存(按页分配)的大小和实际内存的大小
  7. Bits.reserveMemory(size, cap);
  8. long base = 0;
  9. try {
  10. // 通过unsafe.allocateMemory分配堆外内存,并返回堆外内存的基地址
  11. base = unsafe.allocateMemory(size);
  12. } catch (OutOfMemoryError x) {
  13. Bits.unreserveMemory(size, cap);
  14. throw x;
  15. }
  16. unsafe.setMemory(base, size, (byte) 0);
  17. if (pa && (base % ps != 0)) {
  18. // Round up to page boundary
  19. address = base + ps - (base & (ps - 1));
  20. } else {
  21. address = base;
  22. }
  23. // 构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,堆外内存也会被释放
  24. cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
  25. att = null;
  26. }

Bits.reserveMemory(size, cap) 方法

  1. static void reserveMemory(long size, int cap) {
  2. if (!memoryLimitSet && VM.isBooted()) {
  3. maxMemory = VM.maxDirectMemory();
  4. memoryLimitSet = true;
  5. }
  6. // optimist!
  7. if (tryReserveMemory(size, cap)) {
  8. return;
  9. }
  10. final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
  11. // retry while helping enqueue pending Reference objects
  12. // which includes executing pending Cleaner(s) which includes
  13. // Cleaner(s) that free direct buffer memory
  14. while (jlra.tryHandlePendingReference()) {
  15. if (tryReserveMemory(size, cap)) {
  16. return;
  17. }
  18. }
  19. // trigger VM's Reference processing
  20. System.gc();
  21. // a retry loop with exponential back-off delays
  22. // (this gives VM some time to do it's job)
  23. boolean interrupted = false;
  24. try {
  25. long sleepTime = 1;
  26. int sleeps = 0;
  27. while (true) {
  28. if (tryReserveMemory(size, cap)) {
  29. return;
  30. }
  31. if (sleeps >= MAX_SLEEPS) {
  32. break;
  33. }
  34. if (!jlra.tryHandlePendingReference()) {
  35. try {
  36. Thread.sleep(sleepTime);
  37. sleepTime <<= 1;
  38. sleeps++;
  39. } catch (InterruptedException e) {
  40. interrupted = true;
  41. }
  42. }
  43. }
  44. // no luck
  45. throw new OutOfMemoryError("Direct buffer memory");
  46. } finally {
  47. if (interrupted) {
  48. // don't swallow interrupts
  49. Thread.currentThread().interrupt();
  50. }
  51. }
  52. }

该方法用于在系统中保存总分配内存(按页分配)的大小和实际内存的大小。

其中,如果系统中内存( 即,堆外内存 )不够的话:

  1. final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
  2. // retry while helping enqueue pending Reference objects
  3. // which includes executing pending Cleaner(s) which includes
  4. // Cleaner(s) that free direct buffer memory
  5. while (jlra.tryHandlePendingReference()) {
  6. if (tryReserveMemory(size, cap)) {
  7. return;
  8. }
  9. }

jlra.tryHandlePendingReference() 会触发一次非堵塞的Reference#tryHandlePending(false) 。该方法会将已经被JVM垃圾回收的 DirectBuffer 对象的堆外内存释放。因为在 Reference 的静态代码块中定义了:

  1. SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
  2. @Override
  3. public boolean tryHandlePendingReference() {
  4. return tryHandlePending(false);
  5. }
  6. });

如果在进行一次堆外内存资源回收后,还不够进行本次堆外内存分配的话,则

  1. // trigger VM's Reference processing
  2. System.gc();

System.gc()会触发一个full gc,当然前提是你没有显示的设置-XX:+DisableExplicitGC来禁用显式GC。并且你需要知道,调用System.gc()并不能够保证full gc马上就能被执行。
所以在后面打代码中,会进行最多9次尝试,看是否有足够的可用堆外内存来分配堆外内存。并且每次尝试之前,都对延迟等待时间,已给JVM足够的时间去完成full gc操作。如果9次尝试后依旧没有足够的可用堆外内存来分配本次堆外内存,则抛出OutOfMemoryError(“Direct buffer memory”)异常。

注意,这里之所以用使用full gc的很重要的一个原因是:System.gc()会对新生代的老生代都会进行内存回收,这样会比较彻底地回收DirectByteBuffer对象以及他们关联的堆外内存。

DirectByteBuffer对象本身其实是很小的,但是它后面可能关联了一个非常大的堆外内存,因此我们通常称之为冰山对象。
我们做ygc的时候会将新生代里的不可达的DirectByteBuffer对象及其堆外内存回收了,但是无法对old里的DirectByteBuffer对象及其堆外内存进行回收,这也是我们通常碰到的最大的问题。( 并且堆外内存多用于生命期中等或较长的对象 )

如果有大量的DirectByteBuffer对象移到了old,但是又一直没有做cms gc或者full gc,而只进行ygc,那么我们的物理内存可能被慢慢耗光,但是我们还不知道发生了什么,因为heap明明剩余的内存还很多(前提是我们禁用了System.gc – JVM参数DisableExplicitGC)。

总的来说,Bits.reserveMemory(size, cap)方法在可用堆外内存不足以分配给当前要创建的堆外内存大小时,会实现以下的步骤来尝试完成本次堆外内存的创建:

  1. 触发一次非堵塞的 Reference#tryHandlePending(false)。该方法会将已经被JVM垃圾回收的DirectBuffer对象的堆外内存释放。
  2. 如果进行一次堆外内存资源回收后,还不够进行本次堆外内存分配的话,则进行 System.gc()System.gc() 会触发一个 full gc,但你需要知道,调用 System.gc() 并不能够保证 full gc 马上就能被执行。所以在后面打代码中,会进行最多9次尝试,看是否有足够的可用堆外内存来分配堆外内存。并且每次尝试之前,都对延迟等待时间,已给JVM足够的时间去完成full gc操作。 注意,如果你设置了-XX:+DisableExplicitGC,将会禁用显示GC,这会使 System.gc() 调用无效。
  3. 如果9次尝试后依旧没有足够的可用堆外内存来分配本次堆外内存,则抛出OutOfMemoryError(“Direct buffer memory”)异常。

那么可用堆外内存到底是多少了?,即默认堆外存内存有多大:

  1. 如果我们没有通过 -XX:MaxDirectMemorySize 来指定最大的堆外内存。则使用第2条
  2. 如果我们没通过 -Dsun.nio.MaxDirectMemorySize 指定了这个属性,且它不等于-1。则使用第3条
  3. 那么最大堆外内存的值来自于directMemory = Runtime.getRuntime().maxMemory(),这是一个native方法。
  1. JNIEXPORT jlong JNICALL
  2. Java_java_lang_Runtime_maxMemory(JNIEnv *env, jobject this)
  3. {
  4. return JVM_MaxMemory();
  5. }
  6. JVM_ENTRY_NO_ENV(jlong, JVM_MaxMemory(void))
  7. JVMWrapper("JVM_MaxMemory");
  8. size_t n = Universe::heap()->max_capacity();
  9. return convert_size_t_to_jlong(n);
  10. JVM_END

其中在我们使用 CMS GC 的情况下也就是我们设置的 -Xmx 的值里除去一个 survivor 的大小就是默认的堆外内存的大小了。

DirectBuffer 内存回收

DirectBuffer内存回收主要有两种方式,一种是通过System.gc来回收,另一种是通过构造函数里创建的Cleaner对象来回收。

System.gc回收

在DirectBuffer的构造函数中,用到了Bit.reserveMemory这个方法,该方法如下:

  1. static void reserveMemory(long size, int cap) {
  2. ······
  3. if (tryReserveMemory(size, cap)) {
  4. return;
  5. }
  6. ······
  7. while (jlra.tryHandlePendingReference()) {
  8. if (tryReserveMemory(size, cap)) {
  9. return;
  10. }
  11. }
  12. System.gc();
  13. // a retry loop with exponential back-off delays
  14. // (this gives VM some time to do it's job)
  15. boolean interrupted = false;
  16. try {
  17. long sleepTime = 1;
  18. int sleeps = 0;
  19. while (true) {
  20. if (tryReserveMemory(size, cap)) {
  21. return;
  22. }
  23. if (sleeps >= MAX_SLEEPS) {
  24. break;
  25. }
  26. if (!jlra.tryHandlePendingReference()) {
  27. try {
  28. Thread.sleep(sleepTime);
  29. sleepTime <<= 1;
  30. sleeps++;
  31. } catch (InterruptedException e) {
  32. interrupted = true;
  33. }
  34. }
  35. }
  36. // no luck
  37. throw new OutOfMemoryError("Direct buffer memory");
  38. } finally {
  39. if (interrupted) {
  40. // don't swallow interrupts
  41. Thread.currentThread().interrupt();
  42. }
  43. }
  44. }

reserveMemory方法首先尝试分配内存,如果分配成功的话,那么就直接退出。如果分配失败那么就通过调用tryHandlePendingReference来尝试清理堆外内存(最终调用的是Cleaner的clean方法,其实就是unsafe.freeMemory然后释放内存),清理完内存之后再尝试分配内存。如果还是失败,调用System.gc()来触发一次FullGC进行回收(前提是没有加-XX:-+DisableExplicitGC参数)。GC完之后再进行内存分配,失败的话就会进行sleep,然后再进行尝试。每次sleep的时间是逐步增加的,规律是1, 2, 4, 8, 16, 32, 64, 128, 256 (total 511 ms ~ 0.5 s)。如果最终还没有可分配的内存,那么就会抛出OOM异常。

为什么是通过调用tryHandlePendingReference来回收内存呢?答案是JVM在判断内存不可达之后会把需要GC的不可达对象放在一个PendingList中,然后应用程序就可以看到这些对象。通过调用tryHandlePendingReference来访问这些不可达对象。如果不可达对象是Cleaner类型,也就是说关联了堆外的DirectBuffer,那么该DirectBuffer就可以被回收了,通过调用Cleaner的clean方法来回收这部分堆外内存。

这个逻辑就是进行堆外内存分配时触发的回收内存逻辑,也就是说在分配的时候如果遇到堆外内存不足,可能会触发FullGC,然后尝试进行分配。这也是为什么在一些用到堆外内存的应用中不建议加上-XX:-+DisableExplicitGC参数。

Cleaner 对象回收

另个触发堆外内存回收的时机是通过Cleaner对象的clean方法进行回收。在每次新建一个DirectBuffer对象的时候,会同时创建一个Cleaner对象,同一个进程创建的所有的DirectBuffer对象跟Cleaner对象的个数是一样的,并且所有的Cleaner对象会组成一个链表,前后相连。

  1. public static Cleaner create(Object ob, Runnable thunk) {
  2. if (thunk == null)
  3. return null;
  4. return add(new Cleaner(ob, thunk));
  5. }

Cleaner对象的clean方法执行时机是JVM在判断该Cleaner对象关联的DirectBuffer已经不被任何对象引用了(也就是经过可达性分析判定为不可达的时候)。此时Cleaner对象会被JVM挂到PendingList上。然后有一个固定的线程扫描这个List,如果遇到Cleaner对象,那么就执行clean方法。

DirectBuffer在一些高性能的中间件上使用还是相当广泛的。正确的使用可以提升程序的性能,降低GC的频率。

通过配置参数的方式来回收堆外内存

同时我们可以通过 -XX:MaxDirectMemorySize 来指定最大的堆外内存大小,当使用达到了阈值的时候将调用 System.gc() 来做一次 full gc,以此来回收掉没有被使用的堆外内存。

堆外内存那些事

使用堆外内存的原因

  • 对垃圾回收停顿的改善因为full gc意味着彻底回收,彻底回收时,垃圾收集器会对所有分配的堆内内存进行完整的扫描,这意味着一个重要的事实——这样一次垃圾收集对Java应用造成的影响,跟堆的大小是成正比的。过大的堆会影响Java应用的性能。如果使用堆外内存的话,堆外内存是直接受操作系统管理( 而不是虚拟机 )。这样做的结果就是能保持一个较小的堆内内存,以减少垃圾收集对应用的影响。

  • 在某些场景下可以提升程序I/O操纵的性能。少去了将数据从堆内内存拷贝到堆外内存的步骤。

什么情况下使用堆外内存

  • 堆外内存适用于生命周期中等或较长的对象。( 如果是生命周期较短的对象,在YGC的时候就被回收了,就不存在大内存且生命周期较长的对象在FGC对应用造成的性能影响 )。

  • 直接的文件拷贝操作,或者I/O操作。直接使用堆外内存就能少去内存从用户内存拷贝到系统内存的操作,因为I/O操作是系统内核内存和设备间的通信,而不是通过程序直接和外设通信的。

  • 同时,还可以使用 池+堆外内存 的组合方式,来对生命周期较短,但涉及到I/O操作的对象进行堆外内存的再使用。( Netty中就使用了该方式 )

堆外内存 VS 内存池

  • 内存池:主要用于两类对象:①生命周期较短,且结构简单的对象,在内存池中重复利用这些对象能增加CPU缓存的命中率,从而提高性能;②加载含有大量重复对象的大片数据,此时使用内存池能减少垃圾回收的时间。

  • 堆外内存:它和内存池一样,也能缩短垃圾回收时间,但是它适用的对象和内存池完全相反。内存池往往适用于生命期较短的可变对象,而生命期中等或较长的对象,正是堆外内存要解决的。

堆外内存的特点

  • 对于大内存有良好的伸缩性

  • 对垃圾回收停顿的改善可以明显感觉到

  • 在进程间可以共享,减少虚拟机间的复制

堆外内存的一些问题

  • 堆外内存回收问题,以及堆外内存的泄漏问题。这个在上面的源码解析已经提到了。

  • 堆外内存的数据结构问题:堆外内存最大的问题就是你的数据结构变得不那么直观,如果数据结构比较复杂,就要对它进行串行化(serialization),而串行化本身也会影响性能。另一个问题是由于你可以使用更大的内存,你可能开始担心虚拟内存(即硬盘)的速度对你的影响了。

本文内容来自:

  1. https://www.xttblog.com/?p=2714
  2. https://www.jianshu.com/p/007052ee3773
  3. https://www.cnblogs.com/yunxitalk/p/8909040.html

评论

发表评论 点击刷新验证码

提示

该功能暂未开放