Okio之Segment和SegmentPool

Segment

官方解釋

  • Segment 是 buffer 切割后的組成部分.
  • 每個 buffer 中的 Segment 都是循環(huán)鏈表中的節(jié)點(diǎn),持有上一個 Segment 和下一個 Segment 的引用.
  • 每個緩存池中的 Segment 都是單鏈表中的節(jié)點(diǎn).
  • Segment 底層數(shù)組可以被 buffer 和字符串共享.當(dāng)一個 Segment 是被共享狀態(tài)時不可以被回收,其字節(jié)數(shù)據(jù)只能讀不可寫.
    • 唯一例外的是當(dāng) owner 為 true 時,數(shù)據(jù)區(qū)間為 [limit,SIZE) 中可以做寫操作.
  • 每個數(shù)組對應(yīng)一個 Segment 作為持有者.
  • Positions, limits, prev, next 不可以被共享.

成員變量

// Segment.java

// segment 數(shù)據(jù)字節(jié)數(shù)最大值為 8kb
static final int SIZE = 8192;

/* SHARE_MINIMUM 是調(diào)用 split() 時根據(jù)操作字節(jié)大小(byteCount)判斷使用共享 segment 實(shí)現(xiàn)還是
   使用數(shù)據(jù)復(fù)制實(shí)現(xiàn)的標(biāo)準(zhǔn) */
static final int SHARE_MINIMUM = 1024;

// 存放數(shù)據(jù)的字節(jié)數(shù)組
final byte[] data;

// 下一個可讀字節(jié)的下標(biāo)
int pos;

// 第一個可寫字節(jié)的下標(biāo),即最后一個可讀數(shù)據(jù)下標(biāo)為 limit-1
int limit;

// 是否與其他 segment 持有同一個數(shù)組 data 對象
boolean shared;

// 是否為 data 的所有者
//  true  對 data 有讀寫的權(quán)限,可寫的范圍與 shared 有關(guān)
//  false 只讀
boolean owner;

// 下一個 segment
Segment next;

// 上一個 segment
Segment prev;
  • 字節(jié)數(shù)組中有兩個特殊區(qū)域,分別是已讀區(qū)域 [pos, limit-1] 和可寫區(qū)域 [limit, SIZE)
  • shared owner 共同作用限制了 Segment 的讀寫權(quán)限以及可寫的范圍,下文閱讀 writeTo() 會介紹他們的作用.

構(gòu)造方法

// 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;
}
  • 無參構(gòu)造方法構(gòu)造默認(rèn)的 Segment ,data 大小是默認(rèn) 8k, onwer 為 true, shared 為 false,此時 Segment 沒有數(shù)據(jù).

  • 有參構(gòu)造可以構(gòu)造自定義的 Segment,并設(shè)置 data pos limit shared owner 的值.


sharedCopy 和 unsharedCopy

// Segment.java

/**
 * 返回一個新的 segment 并與當(dāng)前 segment 使用同一個 data 引用,相當(dāng)于淺克隆
 */
Segment sharedCopy() {
  // 設(shè)置當(dāng)前 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);
}

兩個方法都是復(fù)制當(dāng)前 Segment 且當(dāng)前 Segment 和復(fù)制品的 pos 和 limit 值相同,兩個方法區(qū)別:

  • sharedCopy() 被調(diào)用時,當(dāng)前 Segment 和復(fù)制得到的 Segment 會持有相同的數(shù)組對象 data ,所以兩個 Segment 的 shared 都要設(shè)置為 true 且復(fù)制的 Segment 的 owner 值為 false.
  • unsharedCopy() 被調(diào)用時,復(fù)制的 Segment 持有的是當(dāng)前 Segment 數(shù)組 data 的深克隆得到的對象,所以他是克隆對象 data 的持有者, owner 為 true 且 shared 為 false.

由此可見只有最初持有 data 的 Segment 是數(shù)組的持有者 owner 為 true,其他調(diào)用 sharedCopy 復(fù)制的 Segment 都為 false.


push

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

在當(dāng)前 Segment 與上一個節(jié)點(diǎn)之間插入一個 Segment 并返回被插入的 Segment.


pop

public @Nullable Segment pop() {
  /* 如果當(dāng)前 segment 下一個節(jié)點(diǎn)就是指向它自己,那么鏈表只有一個 segment,result 為 null,
     且下面兩行代碼的執(zhí)行毫無意義. */
  Segment result = next != this ? next : null;
  prev.next = next;
  next.prev = prev;
  next = null;
  prev = null;
  return result;
}

把當(dāng)前 Segment 從循環(huán)鏈表中移除并返回,如果當(dāng)前 Segment 本身就不在循環(huán)鏈表內(nèi)就返回 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;
}

讀取當(dāng)前 Segment 數(shù)據(jù)并寫入目標(biāo) Segment (sink) 中.

  1. 如果 sink.owner 為 false 即 sink 的數(shù)據(jù)只讀不寫,直接拋異常.

當(dāng)前數(shù)組 data 可以分為三個部分 [0, pos) [pos, limit) [limit, SIZE) ,第一個區(qū)域在非共享狀態(tài)下是可回收區(qū)域,第二第三個就是之前說的可讀和可寫區(qū)域.

  1. 首先判斷 sink 的可寫區(qū)域是否足夠:
  • true 如果足夠直接從 sink 數(shù)組下標(biāo) limit 開始寫入數(shù)據(jù).
  • false 如果不足夠可以通過移動數(shù)組中的可讀數(shù)據(jù)到 [0,limit-pos) ,把第一個區(qū)域的空間回收到可寫區(qū)域中,但實(shí)現(xiàn)該方法需要考慮 sink.shared 的值:
    • true sink 是共享狀態(tài)(2.1)數(shù)據(jù)是不可以移動的只能拋異常.
    • false sink 不是共享狀態(tài)數(shù)組數(shù)據(jù)可移動,但移動之前還需計算判斷移動后的可寫區(qū)域大小是否足夠(2.2):
      • false 移動之后還是不夠空間,拋異常.
      • true 移動后有足夠空間,通過數(shù)組復(fù)制方式往 sink 中寫入數(shù)據(jù).

數(shù)據(jù)通過調(diào)用 System.arraycopy 寫進(jìn)目標(biāo) Segment 后還需要設(shè)置當(dāng)前 pos 和目標(biāo) Segment 的 limit.

該方法還表明了 owner 與 shared 之間的聯(lián)系:

  • owner = true && shared = true : 數(shù)組 [0, limit-1] 范圍內(nèi)只讀不可寫,可寫范圍 [limit, SIZE] ,寫大小為 SIZE - limit.
  • owner = true && shared = false : 整個數(shù)組 data 都是可讀可寫的,可以把可讀數(shù)據(jù)在數(shù)組中隨意移動,可寫大小為 SIZE - (limit - pos).
  • owner = false 時 shared 只能是 true, Segment 持有的數(shù)據(jù)不可以做任何修改,只讀不寫.

split

// Segment.java
public Segment split(int byteCount) {
  // byteCount 不可以 <= 0 || byteCount 必須大于有效數(shù)據(jù)的大小,不然沒必要拆分
  if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
  Segment prefix;

  // 兩個性能指標(biāo):
  // - 避免復(fù)制操作.可以使用共享 segment 達(dá)到目的.
  // - 避免共享數(shù)據(jù)量小的 segment,這樣會在鏈表中出現(xiàn)一串?dāng)?shù)據(jù)量小的 segment 且他們都是只讀,會影響性能.
  // 為了得到平衡,只會在復(fù)制操作代價足夠大的時候才使用共享 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 為數(shù)據(jù)大小分界線,把當(dāng)前 Segment 一分為二.

從數(shù)據(jù)內(nèi)容角度看是把下標(biāo)區(qū)間 [pos, limit) 分為 [pos, pos+byteCount) 和 [pos+byteCount, limit).

分割的實(shí)現(xiàn)有兩種方法:

  • 調(diào)用 sharedCopy 構(gòu)建新的 Segment 且兩個 Segment 共享同一個數(shù)組 data 對象,然后設(shè)置兩個 Segment 的 pos 和 limit.
  • 從緩存池中獲取 Segment 并用 System.arraycopy 復(fù)制數(shù)據(jù),然后設(shè)置兩個 Segment 的 pos 和 limit.

優(yōu)點(diǎn):

  • share 方案可以減少 System.arraycopy 的調(diào)用提高了性能,共享 Segment 持有同一個數(shù)組 data 對象減少內(nèi)存消耗.

缺點(diǎn):

  • share 方案中的 Segment 且非數(shù)組持有者都只是可讀不可寫的,即使是數(shù)組持有者可寫范圍也受到限制,當(dāng)循環(huán)鏈表中存在大量共享狀態(tài)且數(shù)據(jù)量小的 Segment 的時候,這些 Segment 對象會占用過多內(nèi)存資源.
  • 數(shù)組復(fù)制方案涉及到底層方法,占用 CPU 資源,操作的字節(jié)數(shù)越大時性能損耗越明顯.

所以 Segment 規(guī)定當(dāng)操作數(shù)據(jù)大小小于 1k 時用數(shù)據(jù)復(fù)制方案,超過 1k 用共享方案.


compact

// Segment.java
public void compact() {
  if (prev == this) throw new IllegalStateException();
  // 當(dāng) prev 是只讀的時候不可以合并
  if (!prev.owner) return; 
  // 操作字節(jié)數(shù)就是當(dāng)前 segment 的數(shù)據(jù)大小
  int byteCount = limit - pos;
  // 根據(jù)是否共享狀態(tài)計算 prev 的可寫范圍大小:
  //    true  共享狀態(tài)可寫范圍是 [limit, SIZE),
  //    false 非共享可寫范圍是 [0, pos) + [limit, SIZE)
  int availableByteCount = SIZE - prev.limit + (prev.shared ? 0 : prev.pos);
  // 如果 prev 可寫大小小于當(dāng)前 segment 數(shù)據(jù)大小,就不可以合并
  if (byteCount > availableByteCount) return; 
  // 把當(dāng)前 segment 數(shù)據(jù)寫進(jìn) prev 中
  writeTo(prev, byteCount);
  // 把當(dāng)前 segment 從循環(huán)鏈表中移除
  pop();
  // 回收當(dāng)前 segment
  SegmentPool.recycle(this);
}

條件允許情況下合并當(dāng)前 Segment 數(shù)據(jù)到上一個 Segment 中,可以減少循環(huán)鏈表的節(jié)點(diǎn)數(shù)且盡可能地保證所有節(jié)點(diǎn)的數(shù)據(jù)占用率在 50% 以上.

  • 當(dāng)前 Segment 的上一個 Segment 最大可寫大小 >= 當(dāng)前 Segment 數(shù)據(jù)大小的時候,合并這兩個 Segment 中的數(shù)據(jù)到上一個 Segment 中并把當(dāng)前 Segment 從循環(huán)鏈表中移除然后添加到緩存池.
  • 通常是由鏈表中的尾結(jié)點(diǎn) tail 調(diào)用該方法.

SegmentPool

// SegmentPool.java
/**
 * 無用 Segment 的集合,防止被 GC 和零填充
 * 緩存池是靜態(tài)單例的保證了線程安全
 */
final class SegmentPool {
  // 緩存池最大容量 64kb
  static final long MAX_SIZE = 64 * 1024; 

  // 指向單鏈表中下一個節(jié)點(diǎn),就是單鏈表的頭結(jié)點(diǎn)
  static @Nullable Segment next;

  // 記錄緩存池中所有 Segment 數(shù)據(jù)大小之和
  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 必須不存在上一個節(jié)點(diǎn)和下一個節(jié)點(diǎn)的引用,否則報錯
    if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
    // 如果該 segment 是被共享狀態(tài),不可以被回收
    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 通過一個單鏈表實(shí)現(xiàn),緩存最大容量是 64kb 個(有點(diǎn)多).
  • SegmentPool 不會回收共享狀態(tài)的 Segment.
  • SegmentPool 只回收指向上一個節(jié)點(diǎn)和下一個節(jié)點(diǎn)都為 null 的 Segment.
  • SegmentPool 中獲取的 Segment 可能保留著上次使用時的數(shù)據(jù).
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末哥力,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子古徒,更是在濱河造成了極大的恐慌介蛉,老刑警劉巖绰疤,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件题禀,死亡現(xiàn)場離奇詭異该押,居然都是意外死亡裙犹,警方通過查閱死者的電腦和手機(jī)伙判,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進(jìn)店門象对,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人宴抚,你說我怎么就攤上這事勒魔。” “怎么了酱塔?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵沥邻,是天一觀的道長。 經(jīng)常有香客問我羊娃,道長唐全,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮邮利,結(jié)果婚禮上弥雹,老公的妹妹穿的比我還像新娘。我一直安慰自己延届,他們只是感情好剪勿,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著方庭,像睡著了一般厕吉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上械念,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天头朱,我揣著相機(jī)與錄音,去河邊找鬼龄减。 笑死项钮,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的希停。 我是一名探鬼主播烁巫,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼宠能!你這毒婦竟也來了亚隙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤违崇,失蹤者是張志新(化名)和其女友劉穎恃鞋,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體亦歉,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年畅哑,在試婚紗的時候發(fā)現(xiàn)自己被綠了肴楷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡荠呐,死狀恐怖赛蔫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情泥张,我是刑警寧澤呵恢,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站媚创,受9級特大地震影響渗钉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一鳄橘、第九天 我趴在偏房一處隱蔽的房頂上張望声离。 院中可真熱鬧,春花似錦瘫怜、人聲如沸术徊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赠涮。三九已至,卻和暖如春暗挑,著一層夾襖步出監(jiān)牢的瞬間笋除,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工窿祥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留株憾,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓晒衩,卻偏偏與公主長得像嗤瞎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子听系,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 簡介 先看看源碼中該類的簡介: 大概意思是:1.緩沖區(qū)的組成單位結(jié)構(gòu) 2.每一個Segment是一個雙向循環(huán)鏈表贝奇,...
    OkCoco閱讀 1,038評論 0 1
  • 自從Google官方將OkHttp作為底層的網(wǎng)絡(luò)請求之后,作為OkHttp底層IO操作的Okio也是走進(jìn)開發(fā)者的視...
    sheepm閱讀 11,215評論 13 75
  • 一靠胜、溫故而知新 1. 內(nèi)存不夠怎么辦 內(nèi)存簡單分配策略的問題地址空間不隔離內(nèi)存使用效率低程序運(yùn)行的地址不確定 關(guān)于...
    SeanCST閱讀 7,813評論 0 27
  • square在開源社區(qū)的貢獻(xiàn)是卓越的掉瞳,這里是square在Android領(lǐng)域貢獻(xiàn)的開源項(xiàng)目。 1. okio概念 ...
    王英豪閱讀 1,187評論 0 2
  • 簡介 okio 補(bǔ)充了 java.io 和 java.nio 的內(nèi)容浪漠,使得數(shù)據(jù)訪問陕习、存儲和處理更加便捷。本文將簡單...
    MrFengZH閱讀 2,706評論 0 1