Java核心技術----int和Integer分析

1.概述

(1)int: 整型數據類型, 是 Java 的 8 個原始數據類型(Primitive Types,boolean、byte 、short、char、int、float、double、long)之一。 Java 語言雖然號稱一切都是對象,但原始數據類型是例外。

(2)Integer: 是 int 對應的包裝類, 它有一個 int 類型的字段存儲數據,並且提供了基本操作,比如數學運算、int 和字符串之間轉換等。在 Java 5 中,引入了自動裝箱和自動拆箱功能(boxing/unboxing),Java 可以根據上下文,自動進行轉換,極大地簡化了相關編程。

  • 構建 Integer 對象的傳統方式是直接調用構造器,直接 new 一個對象。但是根據實踐,我們發現大部分數據操作都是集中在有限的、較小的數值範圍,因而,在 Java 5 中新增了靜態工廠方法 valueOf,在調用它的時候會利用一個緩存機制,帶來了明顯的性能改進。按照 Javadoc,這個值默認緩存是 -128 到 127 之間
  • Integer的值在-128到127時,Integer對象是在IntegerCache.cache產生,會複用已有對象,也就是說,這個區間的Integer可以直接用等號進行判斷。Integer的值在-128到127之外時,Integer對象在堆上產生,不會複用已有對象,用等號會返回false。

2.具體分析

(1)Integer的自動裝箱、拆箱

自動裝箱實際上算是一種語法糖(Java 平臺爲我們自動進行了一些轉換,保證不同的寫法在運行時等價,它們發生在編譯階段,也就是生成的字節碼是一致的)

  • javac 替我們自動把裝箱轉換爲 Integer.valueOf(),把拆箱替換爲 integer.intValue() [intValue()是java.lang.Number類的方法,Number是一個抽象類。Java中所有的數值類都繼承它。也就是說,不單是Integer有intValue方法,Double,Long等都有此方法] ,這似乎這也順道回答了另一個問題,既然調用的是 Integer.valueOf,自然能夠得到緩存的好處啊。
  • 這種緩存機制並不是只有 Integer 纔有,同樣存在於其他的一些包裝類:
    • Boolean,緩存了 true/false 對應實例,確切說,只會返回兩個常量實例 Boolean.TRUE/FALSE。
    • Short,同樣是緩存了 -128 到 127 之間的數值。
    • Byte,數值有限,所以全部都被緩存。
    • Character,緩存範圍 ‘\u0000’ 到 ‘\u007F’。

原則上,建議避免無意中的裝箱、拆箱行爲,尤其是在性能敏感的場合,創建 10 萬個 Java 對象和 10 萬個整數的開銷可不是一個數量級的,不管是內存使用還是處理速度,光是對象頭的空間佔用就已經是數量級的差距了。

(2)Integer的源碼分析

  • Integer 的緩存範圍雖然默認是 -128 到 127, 但是在特別的應用場景,比如我們明確知道應用會頻繁使用更大的數值,緩存上限值實際是可以根據需要調整的,JVM 提供了參數設置:
-XX:AutoBoxCacheMax=N
  • 不管是 Integer 還 Boolean 等,都被聲明爲“private final”,所以,它們同樣是不可變類型!

  • Integer 等包裝類,定義了類似 SIZE 或者 BYTES 這樣的常量,Java 語言規範裏面,不管是 32 位還是 64 位環境,開發者無需擔心數據的位數差異。

(3)原始數據類型線程安全問題

  • 原始數據類型的變量,顯然要使用併發相關手段,才能保證線程安全,特別的是,部分比較寬的數據類型,比如 float、double,甚至不能保證更新操作的原子性,可能出現程序讀取到只更新了一半數據位的數值!

  • 如果有線程安全的計算需要,建議考慮使用類型AtomicInteger、AtomicLong 這樣的線程安全類。

(4)原始數據類型和引用類型侷限性

  • 原始數據類型和 Java 泛型並不能配合使用
    這是因爲 Java 的泛型某種程度上可以算作僞泛型,它完全是一種編譯期的技巧,Java 編譯期會自動將類型轉換爲對應的特定類型,這就決定了使用泛型,必須保證相應類型可以轉換爲 Object。

  • 引用類型無法高效地表達數據,也不便於表達複雜的數據結構,比如 vector 和 tuple
    我們知道 Java 的對象都是引用類型,如果是一個原始數據類型數組,它在內存裏是一段連續的內存,而對象數組則不然,數據存儲的是引用,對象往往是分散地存儲在堆的不同位置。這種設計雖然帶來了極大靈活性,但是也導致了數據操作的低效,尤其是無法充分利用現代 CPU 緩存機制。

  • 使用原始數據類型、數組甚至本地代碼實現等,在性能極度敏感的場景往往具有比較大的優勢,用其替換掉包裝類、動態數組(如 ArrayList)等可以作爲性能優化的備選項。一些追求極致性能的產品或者類庫,會極力避免創建過多對象。

  • 基本類型均具有取值範圍,在大數*大數的時候,有可能會出現越界的情況。

  • 慎用基本類型處理貨幣存儲。如採用double常會帶來差距,常採用BigDecimal、整型(如果要精確表示分,可將值擴大100倍轉化爲整型)解決該問題。

  • 基本類型轉換時,使用聲明的方式。


3.其他

Java 爲對象內建了各種多態、線程安全等方面的支持,但這不是所有場合的需求,尤其是數據處理重要性日益提高,更加高密度的值類型是非常現實的需求。

HotSpot虛擬機中, 對象在內存中存儲的佈局可以分爲3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

  • 對象頭包括兩部分信息,第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32bit和64bit,官方稱它爲”Mark Word”。對象頭的另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。並不是所有的虛擬機實現都必須在對象數據上保留類型指針,換句話說,查找對象的元數據信息並不一定要經過對象本身,這點將在2.3.3節討論。另外,如果對象是一個Java數組,那在對象頭中還必須有一塊用於記錄數組長度的數據,因爲虛擬機可以通過普通Java對象的元數據信息確定Java對象的大小,但是從數組的元數據中卻無法確定數組的大小。

  • 實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。

  • 對齊填充並不是必然存在的,也沒有特別的含義,它僅僅起着佔位符的作用。由於HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的整數倍。

要點:
1. 在32位系統下,存放Class指針的空間大小是4字節,MarkWord是4字節,對象頭爲8字節。
2. 在64位系統下,存放Class指針的空間大小是8字節,MarkWord是8字節,對象頭爲16字節。
3. 64位開啓指針壓縮的情況下,存放Class指針的空間大小是4字節,MarkWord是8字節,對象頭爲12字節。 數組長度4字節+數組對象頭8字節(對象引用4字節(未開啓指針壓縮的64位爲8字節)+數組markword爲4字節(64位未開啓指針壓縮的爲8字節))+對齊4=16字節。
4. 靜態屬性不算在對象大小內。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章