如何从源码的角度深入剖析ByteBuffer

你只看见了调用一个方法就能创建符合要求的 ByteBuf 却不知为何如此简单,
你只看见了 Netty 使用了线程池却不知线程池用的是什么队列
你只看见了 Netty 到处都在使用 Promise 却不知 Promise 为何物

问题
我们知道,Netty 之所以如此高效,很大一部分原因得益于其对直接内存的高效使用,所以,今天,我想问:

1 Java 中的 ByteBuffer 有直接内存的实现吗?
2 Java 中如何使用直接内存?又如何释放直接内存呢?
3 Java 中 ByteBuffer 的直接内存实现又是如何管理直接内存的?

Buffer 的分类

今天我们的主角是堆内存实现和直接内存实现,它们分别是怎么实现的呢?有什么区别吗?

不过,在正式介绍之前,我想讲另外一个非常有意思的类,我把它称作 Java 中的魔法类 ——Unsafe

不安全的 Unsafe

看过并发集合或者原子类源码的同学,应该对 Unsafe 这个类印象比较深刻,像我们经常使用的 CAS 操作,底层就是使用 Unsafe 来实现的,比如,AtomicInteger 中的 compareAndSet () 方法:

public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

Unsafe 操作直接内存是通过下面几个方法实现的: 操作直接内存。

// 分配内存
public native long allocateMemory(long var1);
// 释放内存
public native void freeMemory(long var1);
// 设置内存值
public native void setMemory(Object var1, long var2, long var4, byte var6);
// 设置某种类型的值,比如putInt()
public native void putXxx(long var1, xxx var3);
// 获取某种类型的值,比如getInt()
public native xxx getXxx(long var1);

比如,我们可以使用 Unsafe 来实现一个直接内存实现的 int 数组。

public class DirectIntArray {// 一个int等于4个字节private static final int INT = 4;private long size;private long address;private static Unsafe unsafe;static {try {// Unsafe类有权限访问控制,只能通过反射获取其实例Field f = Unsafe.class.getDeclaredField("theUnsafe");f.setAccessible(true);unsafe = (Unsafe) f.get(null);} catch (NoSuchFieldException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}}public DirectIntArray(long size) {this.size = size;// 参数字节数address = unsafe.allocateMemory(size * INT);}// 获取某位置的值public int get(long i) {if (i >= size) {throw new ArrayIndexOutOfBoundsException();}return unsafe.getInt(address + i * INT);}// 设置某位置的值public void set(long i, int value) {if (i >= size) {throw new ArrayIndexOutOfBoundsException();}unsafe.putInt(address + i * INT, value);}// 数组大小public long size() {return size;}// 释放内存public void freeMemory() {unsafe.freeMemory(address);}public static void main(String[] args) {// 创建数组并赋值DirectIntArray array = new DirectIntArray(4);array.set(0, 1);array.set(1, 2);array.set(2, 3);array.set(3, 4);// 下面这行数组越界了
//        array.set(5, 5);int sum = 0;for (int i = 0; i < array.size(); i++) {sum += array.get(i);}// 打印10System.out.println(sum);// 最后别忘记释放内存array.freeMemory();}
}

学习源码一般遵循着先宏观再微观的原则。宏观上,一般先看继承体系,类的基本结构等,通过这种方式一般能找到一到两个突破口。微观上,一般根据宏观找到的突破口,进入调试,调试,调试。很多同学看源码喜欢干看,其实是不对的,一定要调试,不调试就无法掌握细节

从类的基本结构上,ByteBuffer 包含两个非常重要的方法:


// 创建一个直接内存实现的ByteBuffer
public static ByteBuffer allocateDirect(int capacity) {return new DirectByteBuffer(capacity);
}// 创建一个堆内存实现的ByteBuffer
public static ByteBuffer allocate(int capacity) {if (capacity < 0)throw new IllegalArgumentException();return new HeapByteBuffer(capacity, capacity);
}

微观分析 ByteBuffer
堆内存实现的 ByteBuffer——HeapByteBuffer
既然要调试,当然要写调试用例啦,我这里也准备了一个调试用例:

public class ByteBufferTest {public static void main(String[] args) {// 1. 创建一个堆内存实现的ByteBufferByteBuffer buffer = ByteBuffer.allocate(12);// 2. 写入值buffer.putInt(1);buffer.putInt(2);buffer.putInt(3);// 3. 切换为读模式buffer.flip();// 4. 读取值System.out.println(buffer.getInt());System.out.println(buffer.getInt());System.out.println(buffer.getInt());}
}

// 1. 创建堆内存实现的ByteBuffer
public static ByteBuffer allocate(int capacity) {if (capacity < 0)throw new IllegalArgumentException();return new HeapByteBuffer(capacity, capacity);
}// 2. HeapByteBuffer的构造方法
HeapByteBuffer(int cap, int lim) {            // package-private// lim = cap = 12// 创建了一个12大小的byte数组// 调用父构造方法super(-1, 0, lim, cap, new byte[cap], 0);
}// 3. ByteBuffer的构造方法
ByteBuffer(int mark, int pos, int lim, int cap,   // package-privatebyte[] hb, int offset)
{// 调用父构造方法// pos = 0,默认创建的就是写模式// lim = cap = 12super(mark, pos, lim, cap);// byte数组hb(heap buffer),为上面传过来的new byte[cap]this.hb = hb;this.offset = offset;
}// 4. Buffer的构造方法
Buffer(int mark, int pos, int lim, int cap) {       // package-privateif (cap < 0)throw new IllegalArgumentException("Negative capacity: " + cap);// 三个非常重要的变量:capacity、limit、positionthis.capacity = cap;limit(lim);position(pos);if (mark >= 0) {if (mark > pos)throw new IllegalArgumentException("mark > position: ("+ mark + " > " + pos + ")");this.mark = mark;}
}

整个创建的过程非常简单,主要包含以下逻辑:
1 创建了一个 byte 数组保存在 hb 这个变量中;
2 给几个重要的变量赋值,比如 capacity、limit、position,还有一个 mark,感兴趣的同学可以自己看看这个变量的作用;
3 默认创建的 ByteBuffer 为写模式,因为 position 从 0 开始且 capacity=limit = 数组大小;

OK,到这里 ByteBuffer 我们就创建好了,所谓堆内存的实现方式,就是使用的 Java 自带的 byte 数组来实现的,让我们再来看看写入 putInt () 这个方法是如何实现的。


// 写入一个int类型的数值
public ByteBuffer putInt(int x) {// 调用Bits工具类的putInt()方法,Bits是位的意思// 堆内存的实现中使用大端法来存储数据Bits.putInt(this, ix(nextPutIndex(4)), x, bigEndian);return this;
}// 移动position到下一个位置
// 因为一个int占4个字节,所以这里往后移动4位
final int nextPutIndex(int nb) {                    // package-private// 判断有没有越界if (limit - position < nb)throw new BufferOverflowException();int p = position;position += nb;// 注意,这里返回的是移动前的位置,初始值为0return p;
}// 计算写入的偏移量,初始值为0
protected int ix(int i) {return i + offset;
}// java.nio.Bits#putInt(java.nio.ByteBuffer, int, int, boolean)
static void putInt(ByteBuffer bb, int bi, int x, boolean bigEndian) {// 堆内存使用的是大端法,更符合人们的习惯if (bigEndian)// 大端法putIntB(bb, bi, x);elseputIntL(bb, bi, x);
}// java.nio.Bits#putIntB(java.nio.ByteBuffer, int, int)
static void putIntB(ByteBuffer bb, int bi, int x) {// 把一个int拆分成4个byte,分别写入// int3(int x) { return (byte)(x >> 24); }bb._put(bi    , int3(x));// int2(int x) { return (byte)(x >> 16); }bb._put(bi + 1, int2(x));// int1(int x) { return (byte)(x >>  8); }bb._put(bi + 2, int1(x));// int0(int x) { return (byte)(x      ); }bb._put(bi + 3, int0(x));
}// java.nio.HeapByteBuffer#_put
void _put(int i, byte b) {                  // package-private// 最终变成了修改byte数组hb[i] = b;
}

写入方法无非就是根据当前 position 的位置往后写入一个 int 大小的数据,写入的时候会把 int 拆分成 4 个 byte 分别写入,而最终其实就是修改前面创建的 byte 数组。

OK,同样地,读取方法应该就是先根据当前 position 计算读取的偏移量,再从数组中读取 4 个字节的数据,最后再拼装成一个 int 类型返回。这块的代码相对来说都比较简单,我们就不一一细看了。

综上所述,HeapByteBuffer 内部使用 byte 数组来存储数据,并根据 position 来写入或者读取数据,既然使用的是 Java 中的类型,自然使用的是堆内存。

直接内存实现的 ByteBuffer——DirectByteBuffer

public class ByteBufferTest {public static void main(String[] args) {// 创建一个直接内存实现的ByteBufferByteBuffer buffer = ByteBuffer.allocateDirect(12);// 写入值buffer.putInt(1);buffer.putInt(2);buffer.putInt(3);// 切换为读模式buffer.flip();// 读取值System.out.println(buffer.getInt());System.out.println(buffer.getInt());System.out.println(buffer.getInt());}
}

问题无处不在,在 DirectIntArray 的使用中,我们是手动调用 freeMemory () 来释放内存的,DirectByteBuffer 的使用过程中如何释放内存,保证内存不泄漏?

public static ByteBuffer allocateDirect(int capacity) {// 创建直接内存实现的ByteBufferreturn new DirectByteBuffer(capacity);
}DirectByteBuffer(int cap) {                   // package-private// 调用父构造方法,设置position/limit/capacity/mark这几个值// 与HeapByteBuffer类似,只不过没有创建hb那个数组super(-1, 0, cap, cap);// 是否页对齐,默认为否boolean pa = VM.isDirectMemoryPageAligned();// 每页大小int ps = Bits.pageSize();long size = Math.max(1L, (long)cap + (pa ? ps : 0));// 先预订内存,如果内存不够,会进行清理,并尝试几次Bits.reserveMemory(size, cap);long base = 0;try {// key1,重点来了,调用unsafe的allocateMemory()方法来分配内存base = unsafe.allocateMemory(size);} catch (OutOfMemoryError x) {Bits.unreserveMemory(size, cap);throw x;}// key2,初始化这片内存的值为0unsafe.setMemory(base, size, (byte) 0);// 根据是否页对齐计算实际的地址if (pa && (base % ps != 0)) {// Round up to page boundaryaddress = base + ps - (base & (ps - 1));} else {// 默认不页对齐,所以地址就等于allocateMemory()返回的地址address = base;}// key3,Cleaner是什么?干什么的?有什么作用?cleaner = Cleaner.create(this, new Deallocator(base, size, cap));att = null;}

看源码有个准则,一定要学会抓重点,对于看不懂的东西可以先记下并跳过,比如,DirectByteBuffer 的构造方法中其实牵涉到很多高阶知识,**像页对齐、弱引用 / 虚引用(在 reserveMemory () 方法中)**等相关的东西,这部分东西非常复杂且难以理解,先记下来,等把整体流程理清楚了,再回头深究这一块的东西,其实也是遵循着从宏观到微观的方法论,宏观使你了解整体流程,微观才能使你的知识体系得到升华。

好了,针对 DirectByteBuffer 的构造方法,整体流程与 HeapByteBuffer 是比较类似的,只不过不是创建一个 byte 数组来保存数据,而是调用 unsafe 来分配内存并保存数据,总结下来有三个非常重要的地方:

1 base = unsafe.allocateMemory(size);,调用 unsafe 的 allocateMemory () 方法来分配内存

2 unsafe.setMemory(base, size, (byte) 0);,初始化这片内存的值为 0,为什么要进行初始化?如果不初始化,之前这块内存可能被别的程序使用过,会残留一些数据,对当前的数据造成影响,这是我们写 DirectIntArray 没有考虑到的。

3 cleaner = Cleaner.create(this, new Deallocator(base, size, cap));,这行代码是干什么的?看着似乎跟清理内存有关,这个我们等会再看,先来看看如何写入数据和读取数据。
经过上面的折腾,我们终于创建好了一个 DirectByteBuffer,接下来,我们来一起看看如何写入数据和读取数据吧。

写入数据调用的是 buffer.putInt(1); 这个方法,同样地,调试跟踪进去:


// 写入一个int类型的数值
public ByteBuffer putInt(int x) {// 1 << 2 = 4,一个int占4个字节putInt(ix(nextPutIndex((1 << 2))), x);return this;
}// 计算下一个position的位置并返回当前position的值
final int nextPutIndex(int nb) {                    // package-privateif (limit - position < nb)throw new BufferOverflowException();int p = position;position += nb;// 返回移动前的值return p;
}// 计算偏移量,在address的基础上加上position的值
private long ix(int i) {return address + ((long)i << 0);
}private ByteBuffer putInt(long a, int x) {// unaligned不是之前讲的那个页对齐// 这里是跟CPU架构相关的一个参数if (unaligned) {int y = (x);// 在windows系统中内存值使用的是小端法,所以直接内存使用的是小端法// 因此,这里要转换一下// 调用unsafe的putInt()方法修改直接内存中对应地址的值unsafe.putInt(a, (nativeByteOrder ? y : Bits.swap(y)));} else {Bits.putInt(a, x, bigEndian);}return this;
}

写入方法无非就是根据当前 position 的位置往后写入一个 int 大小的数据,写入的时候会调用 unsafe 的 putInt () 方法在内存中对应地址的位置直接写入值,而不是像 HeapByteBuffer 那样修改 byte 数组对应位置的值。

OK,同样地,读取方法应该就是先根据当前 position 计算读取的偏移地址,再调用 unsafe 的 getInt () 方法在内存中对应地址的位置读取一个 int 大小的数据,这块的代码相对来说都比较简单,我们就不一一细看了。

综上所述,DirectByteBuffer 底层使用的是 Unsafe 来分配一块直接内存,并在写入数据和读取数据的时候使用 Unsafe 对应的方法来操作直接内存,了解了其原理,是不是也很简单呢?

这里新建了一个叫作 Deallocator 的对象,所谓 Deallocator,它等于 De + allocator,在英语中,De 前缀一般表示相反的意思,比如,increse 是升高的意思,而 decrease 是下降的意思,所以,allocate 是分配的意思,deallocate 应该是解除分配的意思,也就是清理的意思,变成名词就是 deallocator,可以理解为清理器的意思。

rivate static class Deallocator implements Runnable {private static Unsafe unsafe = Unsafe.getUnsafe();private long address;private long size;private int capacity;// 构造方法传入allocate的时候返回的地址,以及容量等参数private Deallocator(long address, long size, int capacity) {assert (address != 0);this.address = address;this.size = size;this.capacity = capacity;}public void run() {if (address == 0) {// Paranoiareturn;}// 调用unsafe的freeMemory释放内存unsafe.freeMemory(address);address = 0;// 取消预订的内存Bits.unreserveMemory(size, capacity);}}

Deallocator 实现了 Runnable 接口,Runnable 接口是什么?大家都比较熟悉了,它是线程执行的任务。那么,这个任务是在什么时候执行的呢?又干了什么呢?我们先来看第二个问题,从上面的代码中可以看到它调用了 unsafe 的 freeMemory () 方法来释放内存,所以,这个任务的作用就是清理内存。

但是,这个任务又是在什么时候执行的呢?或者说,在哪里执行的呢?还是回到创建的地方,也就是下面这行代码:

cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
// 虚引用
public class Cleaner extends PhantomReference<Object> {private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();private static Cleaner first = null;private Cleaner next = null;private Cleaner prev = null;private final Runnable thunk;private static synchronized Cleaner add(Cleaner var0) {// 省略部分代码,将var0添加到Cleaner链表中return var0;}private static synchronized boolean remove(Cleaner var0) {// 省略部分代码,将var0从链表中移除}private Cleaner(Object var1, Runnable var2) {// 调用父类的构造方法// ★Cleaner这个虚引用引用的对象是var1,也就是Deallocaotr对象// 先记住上面这句话!!!super(var1, dummyQueue);// var2即上面创建的Deallocator对象this.thunk = var2;}public static Cleaner create(Object var0, Runnable var1) {// 创建一个Cleaner对象,并返回这个对象// 它里面封装了一个任务return var1 == null ? null : add(new Cleaner(var0, var1));}public void clean() {// 从链表中移除当前对象if (remove(this)) {try {// 执行任务this.thunk.run();} catch (final Throwable var2) {AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {if (System.err != null) {(new Error("Cleaner terminated abnormally", var2)).printStackTrace();}System.exit(1);return null;}});}}}
}

可见,Cleaner 继承自一个叫作 PhantomReference 的类,PhantomReference 是什么呢?

PhantomReference 翻译过来叫作虚引用,它还有三个兄弟,一个叫作强引用,一个叫作软引用,还有一个叫作弱引用。

强引用,使用最普遍的引用。如果一个对象具有强引用,它绝对不会被 gc 回收。如果内存空间不足了,gc 宁愿抛出 OutOfMemoryError,也不是会回收具有强引用的对象。

软引用(SoftReference),如果一个对象只具有软引用,则内存空间足够时不会回收它,但内存空间不够时就会回收这部分对象。只要这个具有软引用对象没有被回收,程序就可以正常使用。因此,可以使用软引用来做缓存使用,有效减少 OOM 的出现。

弱引用(WeakReference),如果一个对象只具有弱引用,则不管内存空间够不够,当 gc 扫描到它时就会回收它。因此,弱引用也可用来作为缓存使用。

虚引用(PhantomReference),如果一个对象只具有虚引用,那么它就和没有任何引用一样,任何时候都可能被 gc 回收。虚引用主要用来跟踪对象被垃圾回收的活动。


软(弱、虚)引用通常和一个引用队列(ReferenceQueue)一起使用,当 gc 回收这个软(弱、虚)引用引用的对象时,会把这个软(弱、虚)引用本身放到这个引用队列中。(先记住这句话)

整个过程就是这样,比较绕,且牵涉到很多虚(软弱)引用相关的知识点,望多多体会。
其实,总结来说,在 DirectByteBuffer 的使用过程中,直接内存的回收还是 gc 控制的,只不过是一种间接控制。
好了,到这里 DirectByteBuffer 的整个源码就剖析完成了,你有没有 Get 到呢?

本节,我们从宏观和微观两个角度剖析了 ByteBuffer 在 Java 中的实现方式,并从源码层面对 HeapByteBuffer 和 DirectByteBuffer 做了非常深入的挖掘,特别是 DirectByteBuffer,它牵涉到很多 Java 中的高阶知识,相信通过本节的学习,你一定能够见识到很多未曾见过的知识,并且会发现很多自己感兴趣的点,比如大端法小端法、Unsafe、强软弱虚引用等,如果你对哪个点特别感兴趣,请死磕到底。

Netty 从源码的角度深入剖析 ByteBuffer相关推荐

  1. 【动态代理】从源码实现角度剖析JDK动态代理

    相比于静态代理,动态代理避免了开发人员编写各个繁锁的静态代理类,只需简单地指定一组接口及目标类对象就能动态的获得代理对象.动态代理类的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以不存在代 ...

  2. 从 Android 6.0 源码的角度剖析 Binder 工作原理 | CSDN 博文精选

    在从Android 6.0源码的角度剖析Activity的启动过程一文(https://blog.csdn.net/AndrExpert/article/details/81488503)中,我们了解 ...

  3. 从Android 6.0源码的角度剖析View的绘制原理

    在从Android 6.0源码的角度剖析Activity的启动过程和从Android 6.0源码的角度剖析Window内部机制原理的文章中,我们分别详细地阐述了一个界面(Activity)从启动到显示 ...

  4. netty高性能编程基础(BIO、NIO、Netty、源码、使用netty实现dubbo RPC)

    文章目录 第1章Netty介绍和应用场景 1.1Netty介绍 1.2应用场景 第2章Java BIO编程 2.1 IO模型 2.2BIO.NIO.AIO适用场景分析 2.3JavaBIO基本介绍 第 ...

  5. 【转】Android事件分发机制完全解析,带你从源码的角度彻底理解(下)

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/9153761 记得在前面的文章中,我带大家一起从源码的角度分析了Android中Vi ...

  6. Netty学习笔记(一)Netty客户端源码分析

    最近在学些BIO,NIO相关的知识,也学习了下Netty和它的源码,做个记录,方便以后继续学习,如果有错误的地方欢迎指正 如果不了解BIO,NIO这些基础知识,可以看下我的如下博客 IO中的阻塞.非阻 ...

  7. [学习总结]7、Android AsyncTask完全解析,带你从源码的角度彻底理解

    我们都知道,Android UI是线程不安全的,如果想要在子线程里进行UI操作,就需要借助Android的异步消息处理机制.之前我也写过了一篇文章从源码层面分析了Android的异步消息处理机制,感兴 ...

  8. 从源码的角度再看 React JS 中的 setState

    在这一篇文章中,我们从源码的角度再次理解下 setState 的更新机制,供深入研究学习之用. 在上一篇手记「深入理解 React JS 中的 setState」中,我们简单地理解了 React 中 ...

  9. Android Fragment 从源码的角度去解析(上)

    ###1.概述 本来想着昨天星期五可以早点休息,今天可以早点起来跑步,可没想到事情那么的多,晚上有人问我主页怎么做到点击才去加载Fragment数据,而不是一进入主页就去加载所有的数据,在这里自己就对 ...

最新文章

  1. android微信小程序自动填表_微信小程序自动化,记录趟过的坑!
  2. ubuntu16.04安装UR3/UR5/UR10机械臂的ROS驱动并实现gazebo下Moveit运动规划仿真以及真实UR3机械臂的运动控制(2)
  3. JMM和happens-before原则
  4. 变形监测期末复习_材料力学复习题
  5. NLP大魔王 · BERT 全解读
  6. DC.SetMapMode()的用法记录
  7. Open Language Tools:简介(1)
  8. 一张图,详解大数据技术架构
  9. 两个PDF比较标出差异_怎样核对两份word文档内容差异?我用2小时,同事仅用2分钟搞定...
  10. 无痕HOOK方式=硬断+VEH
  11. 【unity记录】导入标准资源包(Standard Assets)
  12. mapgis 转换为CAD格式图形 显示不出来的处理
  13. jsp显示中文文件名的图片 详细出处参考:http://www.jb51.net/article/37149.htm
  14. Linux性能优化大杀器—平均负载率详解(鼓励收藏)
  15. 车载蓝牙音乐主动获取播放进度(安富方案)
  16. linux_C_fork函数/execv/execl的使用_数据类型pid_t/getpid/sleep /warning: missing sentinel in function call
  17. 天天自习软件测试计划
  18. 广东省中医院微信公众号医保个账支付功能
  19. 白光模块?彩光模块?
  20. 函数式编程语言的入门级了解

热门文章

  1. idea 使用 Gradle 构建过程中控制台中文显示乱码解决
  2. linux网卡顺序问题,linux网卡绑定及网卡顺序变更测试.docx
  3. Java 面向对象:super关键字的理解
  4. connection url mysql_JDBC URL格式及其参数说明 oracle mysql
  5. 无需担心架构演变 入云的Teradata无处不在
  6. L2-008 最长对称字串 以下标i展开
  7. 在PHP代码中处理JSON 格式的字符串的两种方法:
  8. 非阻塞式JavaScript脚本介绍
  9. Solidity 0.5 address payable和address的区别是什么?
  10. 高性能apache服务器配置大并发教程MPM模块配置