Okio之Segment和SegmentPool

Segment

官方解释

  • Segment 是 buffer 切割后的组成部分.
  • 每个 buffer 中的 Segment 都是循环链表中的节点,持有上一个 Segment 和下一个 Segment 的引用.
  • 每个缓存池中的 Segment 都是单链表中的节点.
  • Segment 底层数组可以被 buffer 和字符串共享.当一个 Segment 是被共享状态时不可以被回收,其字节数据只能读不可写.
    • 唯一例外的是当 owner 为 true 时,数据区间为 [limit,SIZE) 中可以做写操作.
  • 每个数组对应一个 Segment 作为持有者.
  • Positions, limits, prev, next 不可以被共享.

成员变量

// Segment.java

// segment 数据字节数最大值为 8kb
static final int SIZE = 8192;

/* SHARE_MINIMUM 是调用 split() 时根据操作字节大小(byteCount)判断使用共享 segment 实现还是
   使用数据复制实现的标准 */
static final int SHARE_MINIMUM = 1024;

// 存放数据的字节数组
final byte[] data;

// 下一个可读字节的下标
int pos;

// 第一个可写字节的下标,即最后一个可读数据下标为 limit-1
int limit;

// 是否与其他 segment 持有同一个数组 data 对象
boolean shared;

// 是否为 data 的所有者
//  true  对 data 有读写的权限,可写的范围与 shared 有关
//  false 只读
boolean owner;

// 下一个 segment
Segment next;

// 上一个 segment
Segment prev;
  • 字节数组中有两个特殊区域,分别是已读区域 [pos, limit-1] 和可写区域 [limit, SIZE)
  • shared owner 共同作用限制了 Segment 的读写权限以及可写的范围,下文阅读 writeTo() 会介绍他们的作用.

构造方法

// Segment.java
Segment() {
  this.data = new byte[SIZE];
  this.owner = true;
  this.shared = false;
}

Segment(byte[] data, int pos, int limit, boolean shared, boolean owner) {
  this.data = data;
  this.pos = pos;
  this.limit = limit;
  this.shared = shared;
  this.owner = owner;
}
  • 无参构造方法构造默认的 Segment ,data 大小是默认 8k, onwer 为 true, shared 为 false,此时 Segment 没有数据.

  • 有参构造可以构造自定义的 Segment,并设置 data pos limit shared owner 的值.


sharedCopy 和 unsharedCopy

// Segment.java

/**
 * 返回一个新的 segment 并与当前 segment 使用同一个 data 引用,相当于浅克隆
 */
Segment sharedCopy() {
  // 设置当前 segment 的 shared 为 true 可以防止 segment 被回收
  shared = true;
  return new Segment(data, pos, limit, true, false);
}

/** 
 * 返回一个新的 segment , data 是 segment.data 深克隆得到的对象 
 */
Segment unsharedCopy() {
  return new Segment(data.clone(), pos, limit, false, true);
}

两个方法都是复制当前 Segment 且当前 Segment 和复制品的 pos 和 limit 值相同,两个方法区别:

  • sharedCopy() 被调用时,当前 Segment 和复制得到的 Segment 会持有相同的数组对象 data ,所以两个 Segment 的 shared 都要设置为 true 且复制的 Segment 的 owner 值为 false.
  • unsharedCopy() 被调用时,复制的 Segment 持有的是当前 Segment 数组 data 的深克隆得到的对象,所以他是克隆对象 data 的持有者, owner 为 true 且 shared 为 false.

由此可见只有最初持有 data 的 Segment 是数组的持有者 owner 为 true,其他调用 sharedCopy 复制的 Segment 都为 false.


push

// Segment.java
public Segment push(Segment segment) {
  segment.prev = this;
  segment.next = next;
  next.prev = segment;
  next = segment;
  return segment;
}

在当前 Segment 与上一个节点之间插入一个 Segment 并返回被插入的 Segment.


pop

public @Nullable Segment pop() {
  /* 如果当前 segment 下一个节点就是指向它自己,那么链表只有一个 segment,result 为 null,
     且下面两行代码的执行毫无意义. */
  Segment result = next != this ? next : null;
  prev.next = next;
  next.prev = prev;
  next = null;
  prev = null;
  return result;
}

把当前 Segment 从循环链表中移除并返回,如果当前 Segment 本身就不在循环链表内就返回 null.


writeTo

// Segment.java
public void writeTo(Segment sink, int byteCount) {
  // 1
  if (!sink.owner) throw new IllegalArgumentException();
  // 2
  if (sink.limit + byteCount > SIZE) {
    // 2.1
    if (sink.shared) throw new IllegalArgumentException();
    // 2.2
    if (sink.limit + byteCount - sink.pos > SIZE) throw new IllegalArgumentException();
    System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos);
    sink.limit -= sink.pos;
    sink.pos = 0;
  }

  System.arraycopy(data, pos, sink.data, sink.limit, byteCount);
  sink.limit += byteCount;
  pos += byteCount;
}

读取当前 Segment 数据并写入目标 Segment (sink) 中.

  1. 如果 sink.owner 为 false 即 sink 的数据只读不写,直接抛异常.

当前数组 data 可以分为三个部分 [0, pos) [pos, limit) [limit, SIZE) ,第一个区域在非共享状态下是可回收区域,第二第三个就是之前说的可读和可写区域.

  1. 首先判断 sink 的可写区域是否足够:
  • true 如果足够直接从 sink 数组下标 limit 开始写入数据.
  • false 如果不足够可以通过移动数组中的可读数据到 [0,limit-pos) ,把第一个区域的空间回收到可写区域中,但实现该方法需要考虑 sink.shared 的值:
    • true sink 是共享状态(2.1)数据是不可以移动的只能抛异常.
    • false sink 不是共享状态数组数据可移动,但移动之前还需计算判断移动后的可写区域大小是否足够(2.2):
      • false 移动之后还是不够空间,抛异常.
      • true 移动后有足够空间,通过数组复制方式往 sink 中写入数据.

数据通过调用 System.arraycopy 写进目标 Segment 后还需要设置当前 pos 和目标 Segment 的 limit.

该方法还表明了 owner 与 shared 之间的联系:

  • owner = true && shared = true : 数组 [0, limit-1] 范围内只读不可写,可写范围 [limit, SIZE] ,写大小为 SIZE - limit.
  • owner = true && shared = false : 整个数组 data 都是可读可写的,可以把可读数据在数组中随意移动,可写大小为 SIZE - (limit - pos).
  • owner = false 时 shared 只能是 true, Segment 持有的数据不可以做任何修改,只读不写.

split

// Segment.java
public Segment split(int byteCount) {
  // byteCount 不可以 <= 0 || byteCount 必须大于有效数据的大小,不然没必要拆分
  if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
  Segment prefix;

  // 两个性能指标:
  // - 避免复制操作.可以使用共享 segment 达到目的.
  // - 避免共享数据量小的 segment,这样会在链表中出现一串数据量小的 segment 且他们都是只读,会影响性能.
  // 为了得到平衡,只会在复制操作代价足够大的时候才使用共享 segment
  if (byteCount >= SHARE_MINIMUM) {
    prefix = sharedCopy();
  } else {
    prefix = SegmentPool.take();
    System.arraycopy(data, pos, prefix.data, 0, byteCount);
  }

  prefix.limit = prefix.pos + byteCount;
  pos += byteCount;
  prev.push(prefix);
  return prefix;
}

split() 以 byteCount 为数据大小分界线,把当前 Segment 一分为二.

从数据内容角度看是把下标区间 [pos, limit) 分为 [pos, pos+byteCount) 和 [pos+byteCount, limit).

分割的实现有两种方法:

  • 调用 sharedCopy 构建新的 Segment 且两个 Segment 共享同一个数组 data 对象,然后设置两个 Segment 的 pos 和 limit.
  • 从缓存池中获取 Segment 并用 System.arraycopy 复制数据,然后设置两个 Segment 的 pos 和 limit.

优点:

  • share 方案可以减少 System.arraycopy 的调用提高了性能,共享 Segment 持有同一个数组 data 对象减少内存消耗.

缺点:

  • share 方案中的 Segment 且非数组持有者都只是可读不可写的,即使是数组持有者可写范围也受到限制,当循环链表中存在大量共享状态且数据量小的 Segment 的时候,这些 Segment 对象会占用过多内存资源.
  • 数组复制方案涉及到底层方法,占用 CPU 资源,操作的字节数越大时性能损耗越明显.

所以 Segment 规定当操作数据大小小于 1k 时用数据复制方案,超过 1k 用共享方案.


compact

// Segment.java
public void compact() {
  if (prev == this) throw new IllegalStateException();
  // 当 prev 是只读的时候不可以合并
  if (!prev.owner) return; 
  // 操作字节数就是当前 segment 的数据大小
  int byteCount = limit - pos;
  // 根据是否共享状态计算 prev 的可写范围大小:
  //    true  共享状态可写范围是 [limit, SIZE),
  //    false 非共享可写范围是 [0, pos) + [limit, SIZE)
  int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
  // 如果 prev 可写大小小于当前 segment 数据大小,就不可以合并
  if (byteCount > availableByteCount) return; 
  // 把当前 segment 数据写进 prev 中
  writeTo(prev, byteCount);
  // 把当前 segment 从循环链表中移除
  pop();
  // 回收当前 segment
  SegmentPool.recycle(this);
}

条件允许情况下合并当前 Segment 数据到上一个 Segment 中,可以减少循环链表的节点数且尽可能地保证所有节点的数据占用率在 50% 以上.

  • 当前 Segment 的上一个 Segment 最大可写大小 >= 当前 Segment 数据大小的时候,合并这两个 Segment 中的数据到上一个 Segment 中并把当前 Segment 从循环链表中移除然后添加到缓存池.
  • 通常是由链表中的尾结点 tail 调用该方法.

SegmentPool

// SegmentPool.java
/**
 * 无用 Segment 的集合,防止被 GC 和零填充
 * 缓存池是静态单例的保证了线程安全
 */
final class SegmentPool {
  // 缓存池最大容量 64kb
  static final long MAX_SIZE = 64 * 1024; 

  // 指向单链表中下一个节点,就是单链表的头结点
  static @Nullable Segment next;

  // 记录缓存池中所有 Segment 数据大小之和
  static long byteCount;

  private SegmentPool() {
  }

  // 从缓存池中获取一个 Segment
  static Segment take() {
    synchronized (SegmentPool.class) {
      if (next != null) {
        Segment result = next;
        next = result.next;
        result.next = null;
        // 减去一个 Segment 容量
        byteCount -= Segment.SIZE;
        return result;
      }
    }
    return new Segment(); 
  }

  // 回收一个 Segment
  static void recycle(Segment segment) {
    // segment 必须不存在上一个节点和下一个节点的引用,否则报错
    if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
    // 如果该 segment 是被共享状态,不可以被回收
    if (segment.shared) return; 
    synchronized (SegmentPool.class) {
      // 缓存池已满,return
      if (byteCount + Segment.SIZE > MAX_SIZE) return;
      // byteCount 添加一个 segment 的容量
      byteCount += Segment.SIZE;
      segment.next = next;
      segment.pos = segment.limit = 0;
      next = segment;
    }
  }
}
  • SegmentPool 通过一个单链表实现,缓存最大容量是 64kb 个(有点多).
  • SegmentPool 不会回收共享状态的 Segment.
  • SegmentPool 只回收指向上一个节点和下一个节点都为 null 的 Segment.
  • SegmentPool 中获取的 Segment 可能保留着上次使用时的数据.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容

  • 简介 先看看源码中该类的简介: 大概意思是:1.缓冲区的组成单位结构 2.每一个Segment是一个双向循环链表,...
    OkCoco阅读 1,027评论 0 1
  • 自从Google官方将OkHttp作为底层的网络请求之后,作为OkHttp底层IO操作的Okio也是走进开发者的视...
    sheepm阅读 11,175评论 13 75
  • 一、温故而知新 1. 内存不够怎么办 内存简单分配策略的问题地址空间不隔离内存使用效率低程序运行的地址不确定 关于...
    SeanCST阅读 7,762评论 0 27
  • square在开源社区的贡献是卓越的,这里是square在Android领域贡献的开源项目。 1. okio概念 ...
    王英豪阅读 1,173评论 0 2
  • 简介 okio 补充了 java.io 和 java.nio 的内容,使得数据访问、存储和处理更加便捷。本文将简单...
    MrFengZH阅读 2,602评论 0 1