再談Java類型擦除與其對Flink類型和序列化的影響

前言

本文前半部分的內(nèi)容在很久之前講過鼠锈,但是最近又有交接到團(tuán)隊(duì)內(nèi)的歷史任務(wù)出現(xiàn)這方面導(dǎo)致的性能問題,故有必要再講一次星著,并擴(kuò)展一部分新內(nèi)容购笆。先通過兩個例子來引入Java類型擦除。

Java類型擦除的表現(xiàn)

  • 例一

這段代碼無法通過編譯虚循,提示兩個方法簽名沖突由桌,因?yàn)椴脸愋拖嗤H绻サ羝渲幸粋€方法邮丰,反編譯之后的代碼如下行您。

public void foo(List list) { }
  • 例二

這段代碼會返回true。并且Java只允許List.class的寫法剪廉,不允許List<T>.class的寫法娃循。

認(rèn)識類型擦除

我們知道,泛型是高級語言中比較令人頭疼的問題斗蒋,一般來講要實(shí)現(xiàn)泛型有兩種方式:

  • Code Sharing:對同一個原始類型下的泛型類型只生成同一份目標(biāo)代碼(在Java中就是字節(jié)碼)捌斧。
  • Code Specialization:對每一個泛型類型都生成不同的目標(biāo)代碼笛质。

Java使用的泛型實(shí)現(xiàn)是前者,而C++和C#使用的是后者捞蚂,它們也分別稱為“假”泛型和“真”泛型妇押。Code Sharing通過類型擦除來保證只生成一份目標(biāo)代碼,但也導(dǎo)致程序在運(yùn)行時(shí)對泛型類型沒有感知姓迅,所以上述例子一的代碼反編譯后只剩下了List敲霍,例子二中的類型比較實(shí)際上都是Class<? extends ArrayList>的比較。如果Java也采用Code Specialization機(jī)制(想一想C++ Template)的話丁存,所有List<T>就都是顯式不同的類型了肩杈。

為什么Java要采用Code Sharing和類型擦除呢?主要有兩點(diǎn)原因:一是Java泛型是到1.5版本才出現(xiàn)的特性解寝,在此之前Java已經(jīng)在無泛型的條件下經(jīng)歷了較長時(shí)間的發(fā)展扩然,如果采用Code Specialization,就得對Java類型系統(tǒng)做傷筋動骨的改動聋伦,并且無法保證向前兼容性夫偶;二是Code Specialization對每個泛型類型都生成不同的目標(biāo)代碼,如果有10個不同泛型的List觉增,就要生成10份字節(jié)碼兵拢,加重解釋和執(zhí)行負(fù)擔(dān)。

由此可見抑片,類型擦除讓JVM省了不少事卵佛,但是加重了編譯器的工作量杨赤。編譯器必須在運(yùn)行期之前就進(jìn)行檢查敞斋,禁止模糊的或者不合法的泛型使用方式。再舉一個例子疾牲。

這種用法也是不允許的植捎,換句話說,里氏替換原則不適用于Java的泛型類型參數(shù)阳柔。這并不難理解:對于一個List<Object>而言焰枢,向其中添加字符串是完全合法的,但是如果方法傳入的參數(shù)為List<Integer>的話就會直接造成ClassCastException舌剂,因此編譯器會提前block掉這種可能性济锄。

還沒完,如果把traverse()方法參數(shù)中的List<Object>換成用通配符表示的List<?>霍转,那么traverse()方法調(diào)用就沒問題荐绝,但list.add()語句就會編譯不通過。這是因?yàn)?code>list.add()方法無法判斷具體要加入列表的是Object的哪個子類實(shí)例避消,因此會用最簡單粗暴的方法來處理低滩,即全部拒絕召夹。相對地,list.get()則是可以編譯通過的恕沫,因?yàn)榫幾g器能夠通過<? extends T><? super T>得知泛型類型的上下界限监憎。

如果泛型類型有界限,在類型擦除時(shí)會根據(jù)最左側(cè)的泛型參數(shù)來替換婶溯,例如下面的泛型類鲸阔。

class Test<T extends Comparable & Serializable> {
  private T value;
  public T getValue() { return value; }
  public void setValue(T value) { this.value = value; }
}

類型擦除后就會變成:

class Test {
  private Comparable value;
  public Comparable getValue() { return value; }
  public void setValue(Comparable value) { this.value = value; }
}

同理,如果沒有規(guī)定T是哪個類的子類或者超類爬虱,就會替換為Object隶债。

下面來看類型擦除為Flink類型體系帶來的問題,并介紹Flink規(guī)避此問題的類型提示(Type Hint)機(jī)制跑筝。

Flink的類型提示機(jī)制

以Flink自帶示例中的SocketWindowWordCount為例死讹,如果我們將它的主邏輯改寫成Lambda表達(dá)式,如下:

    DataStream<WordWithCount> windowCounts = text
      .flatMap((String value, Collector<WordWithCount> out) -> {
        for (String word : value.split("\\s")) {
          out.collect(new WordWithCount(word, 1L));
        }
      })
      .keyBy("word")
      .timeWindow(Time.seconds(5))
      .reduce((a, b) ->
        new WordWithCount(a.word, a.count + b.count)
      );

執(zhí)行時(shí)會拋出如下異常曲梗。

這說明程序無法在運(yùn)行時(shí)推斷出flatMap()算子的返回類型赞警。為什么之前采用匿名內(nèi)部類就沒有問題?因?yàn)槟涿麅?nèi)部類會被真正地編譯為.class文件虏两,而Lambda表達(dá)式是在運(yùn)行時(shí)調(diào)用invokedynamic指令愧旦,亦即在第一次執(zhí)行其邏輯時(shí)才會確定。因此Lambda表達(dá)式比起匿名內(nèi)部類定罢,會丟失更多的類型信息笤虫。看一下flatMap()算子的簽名:

void flatMap(T value, Collector<O> out);

經(jīng)過類型擦除祖凫,Collector的泛型參數(shù)被抹掉了(參看報(bào)錯The generic type parameters of Collector are missing)琼蚯,自然就會拋出無法確定返回類型的異常。如果我們采用的不是flatMap()算子而是map()惠况,就不會出現(xiàn)這種問題遭庶,因?yàn)?code>map()的返回類型可以自動推斷。

為了克服類型擦除帶來的問題稠屠,F(xiàn)link類型系統(tǒng)中內(nèi)置了類型提示機(jī)制峦睡,即用戶在調(diào)用此類算子之后,手動指明返回的類型信息权埠。在flatMap()之后調(diào)用returns()方法榨了,就可以指定返回類型了。

    text.flatMap((String value, Collector<WordWithCount> out) -> {
        for (String word : value.split("\\s")) {
          out.collect(new WordWithCount(word, 1L));
        }
      })
      .returns(TypeInformation.of(WordWithCount.class));

但是攘蔽,如果返回類型本身就有泛型龙屉,比如在Flink中常用的元組(TupleX),就得另外換一種寫法秩彤,即通過繼承TypeHint的匿名內(nèi)部類保留泛型信息叔扼。

.returns(TypeInformation.of(new TypeHint<Tuple2<String, String>>() { }))

下面再來看一個比較隱匿的可能會引發(fā)性能問題的場景事哭。

POJO序列化fallback問題與類型注入

我們知道,F(xiàn)link對標(biāo)準(zhǔn)的Java POJO類型有專門的PojoSerializer序列化器支持瓜富,性能相當(dāng)好鳍咱。但是也有例外,考慮以下包含容器成員的POJO:

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class MyPojo {
    private String myId;
    private List<Integer> myTags;
    private Map<String, Integer> myFlags;
}

在Flink應(yīng)用執(zhí)行時(shí)与柑,會提示myTagsmyFlags兩個字段是Generic Types谤辜,也就是要fallback到Kryo Serializer做序列化,而非Flink原生的ListSerializerMapSerializer价捧。如果我們調(diào)用ExecutionConfig#disableGenericTypes()方法來禁用fallback丑念,則應(yīng)用無法執(zhí)行,并有異常提示Generic types have been disabled and type java.util.Map is treated as a generic type结蟋。

可見脯倚,由于類型擦除的存在,POJO中的所有泛型參數(shù)都無法識別嵌屎,無法通過原生序列化器操作推正,故序列化性能會有數(shù)倍的下降,在涉及狀態(tài)操作和網(wǎng)絡(luò)傳輸時(shí)尤其明顯宝惰。本文開頭提到的出現(xiàn)問題的歷史Flink任務(wù)植榕,就是因?yàn)閱蝹€POJO中包含了十幾個Map,且均有狀態(tài)讀寫尼夺,近期因新業(yè)務(wù)上線尊残,流量增大,導(dǎo)致大量時(shí)間耗費(fèi)在序列化層面淤堵,任務(wù)嚴(yán)重反壓寝衫。

由此可見,盡量讓POJO內(nèi)不包含泛型類型(多數(shù)情況下就是避免使用Java容器)是最好的粘勒,但如果必須使用的話竞端,如何解決這個問題呢屎即?答案是自行實(shí)現(xiàn)對應(yīng)類型的TypeInfoFactory庙睡,并通過@TypeInfo注解對泛型字段做類型注入。例如技俐,我們實(shí)現(xiàn)一個無嵌套的Map對應(yīng)的TypeInfoFactory乘陪。

@SuppressWarnings("unchecked")
public class SimpleMapTypeInfoFactory<K, V> extends TypeInfoFactory<Map<K, V>> {
    @Override
    public TypeInformation<Map<K, V>> createTypeInfo(Type t, Map<String, TypeInformation<?>> genericParameters) {
        return Types.MAP(
            (TypeInformation<K>) genericParameters.get("K"),
            (TypeInformation<V>) genericParameters.get("V")
        );
    }
}

然后為myFlags字段注入此類型,在序列化時(shí)雕擂,MapSerializer就會對此字段生效啡邑,性能恢復(fù)正常。

@TypeInfo(SimpleMapTypeInfoFactory.class)
private Map<String, Integer> myFlags;

當(dāng)然井赌,對所有其他無嵌套的Map<K, V>類型字段谤逼,都可以復(fù)用上面的SimpleMapTypeInfoFactory類贵扰,無需重復(fù)編寫。至于其他類型與組合流部,讀者舉一反三即可戚绕。

最后再提醒一句,Set不是Flink序列化器原生支持的類型枝冀,即不存在SetSerializer舞丛,故所有需要用到Set<T>的場合,都需要用Map<T, Boolean>代替果漾,并注入上述SimpleMapTypeInfoFactory球切,以獲得最佳性能。關(guān)于誤用Set這檔事绒障,筆者之前另有文章說明吨凑,不再贅述。

The End

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末户辱,一起剝皮案震驚了整個濱河市怀骤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌焕妙,老刑警劉巖蒋伦,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異焚鹊,居然都是意外死亡痕届,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門末患,熙熙樓的掌柜王于貴愁眉苦臉地迎上來研叫,“玉大人,你說我怎么就攤上這事璧针∪侣” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵探橱,是天一觀的道長申屹。 經(jīng)常有香客問我,道長隧膏,這世上最難降的妖魔是什么哗讥? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮胞枕,結(jié)果婚禮上杆煞,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好决乎,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布队询。 她就那樣靜靜地躺著,像睡著了一般构诚。 火紅的嫁衣襯著肌膚如雪娘摔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天唤反,我揣著相機(jī)與錄音凳寺,去河邊找鬼。 笑死彤侍,一個胖子當(dāng)著我的面吹牛肠缨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播盏阶,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼晒奕,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了名斟?” 一聲冷哼從身側(cè)響起脑慧,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎砰盐,沒想到半個月后闷袒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡岩梳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年囊骤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片冀值。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡也物,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出列疗,到底是詐尸還是另有隱情滑蚯,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布抵栈,位于F島的核電站告材,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏竭讳。R本人自食惡果不足惜创葡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一浙踢、第九天 我趴在偏房一處隱蔽的房頂上張望绢慢。 院中可真熱鬧,春花似錦、人聲如沸胰舆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缚窿。三九已至棘幸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間倦零,已是汗流浹背误续。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扫茅,地道東北人蹋嵌。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像葫隙,于是被迫代替她去往敵國和親栽烂。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345

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