亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

詳解處理Java中的大對象的方法

 更新時間:2022年04月14日 09:23:53   作者:農民工老王  
本文我們將講解一下對于“大對象”的優(yōu)化。這里的“大對象”,是一個泛化概念,它可能存放在?JVM?中,也可能正在網絡上傳輸,也可能存在于數(shù)據(jù)庫中,快跟隨小編一起學習一下

本文我們將講解一下對于“大對象”的優(yōu)化。這里的“大對象”,是一個泛化概念,它可能存放在 JVM 中,也可能正在網絡上傳輸,也可能存在于數(shù)據(jù)庫中。

那么為什么大對象會影響我們的應用性能呢?

第一,大對象占用的資源多,垃圾回收器要花一部分精力去對它進行回收;

第二,大對象在不同的設備之間交換,會耗費網絡流量,以及昂貴的 I/O;

第三,對大對象的解析和處理操作是耗時的,對象職責不聚焦,就會承擔額外的性能開銷。

結合我們前面提到的緩存,以及對象的池化操作,加上對一些中間結果的保存,我們能夠對大對象進行初步的提速。

但這還遠遠不夠,我們僅僅減少了對象的創(chuàng)建頻率,但并沒有改變對象“大”這個事實。本文,將從 JDK 的一些知識點講起,先來看幾個面試頻率比較高的對象復用問題;接下來,從數(shù)據(jù)的結構緯度和時間維度出發(fā),分別逐步看一下一些把對象變小,把操作聚焦的策略。

String中的substring

我們都知道,String 在 Java 中是不可變的,如果你改動了其中的內容,它就會生成一個新的字符串。如果我們想要用到字符串中的一部分數(shù)據(jù),就可以使用 substring 方法。

下面是Java11中String的源碼。

public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = length() - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    if (beginIndex == 0) {
        return this;
    }
    return isLatin1() ? StringLatin1.newString(value, beginIndex, subLen)
                      : StringUTF16.newString(value, beginIndex, subLen);
}

public static String newString(byte[] val, int index, int len) {
    if (String.COMPACT_STRINGS) {
        byte[] buf = compress(val, index, len);
        if (buf != null) {
            return new String(buf, LATIN1);
        }
    }
    int last = index + len;
    return new String(Arrays.copyOfRange(val, index << 1, last << 1), UTF16);
}

如上述代碼所示,當我們需要一個子字符串的時候,substring 生成了一個新的字符串,這個字符串通過構造函數(shù)的 Arrays.copyOfRange 函數(shù)進行構造。

這個函數(shù)在 Java7 之后是沒有問題的,但在Java6 中,卻有著內存泄漏的風險,我們可以學習一下這個案例,來看一下大對象復用可能會產生的問題。下面是Java6中的代碼:

public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > count) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    if (beginIndex > endIndex) {
        throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
    }
    return ((beginIndex == 0) && (endIndex == count)) ? 
            this :
            new String(offset + beginIndex, endIndex - beginIndex, value);
}

String(int offset, int count, char value[]) {
    this.value = value;
    this.offset = offset;
    this.count = count;
}

可以看到,它在創(chuàng)建子字符串的時候,并不只拷貝所需要的對象,而是把整個 value 引用了起來。如果原字符串比較大,即使不再使用,內存也不會釋放。

比如,一篇文章內容可能有幾兆,我們僅僅是需要其中的摘要信息,也不得不維持整個的大對象。

有一些工作年限比較長的面試官,對 substring 還停留在 JDK6 的印象,但其實,Java 已經將這個 bug 給修改了。如果面試時遇到這個問題,保險起見,可以把這個改善過程答出來。

這對我們的借鑒意義是:如果你創(chuàng)建了比較大的對象,并基于這個對象生成了一些其他的信息,這個時候,一定要記得去掉和這個大對象的引用關系。

集合大對象擴容

對象擴容,在 Java 中是司空見慣的現(xiàn)象,比如 StringBuilder、StringBuffer、HashMap,ArrayList 等。概括來講,Java 的集合,包括 List、Set、Queue、Map 等,其中的數(shù)據(jù)都不可控。在容量不足的時候,都會有擴容操作,擴容操作需要重新組織數(shù)據(jù),所以都不是線程安全的。

我們先來看下 StringBuilder 的擴容代碼:

void expandCapacity(int minimumCapacity) {
    int newCapacity = value.length * 2 + 2;
    if (newCapacity - minimumCapacity < 0)
        newCapacity = minimumCapacity;
    if (newCapacity < 0) {
        if (minimumCapacity < 0) // overflow 
            throw new OutOfMemoryError();
        newCapacity = Integer.MAX_VALUE;
    }
    value = Arrays.copyOf(value, newCapacity);
}

容量不夠的時候,會將內存翻倍,并使用 Arrays.copyOf 復制源數(shù)據(jù)。

下面是 HashMap 的擴容代碼,擴容后大小也是翻倍。它的擴容動作就復雜得多,除了有負載因子的影響,它還需要把原來的數(shù)據(jù)重新進行散列,由于無法使用 native 的 Arrays.copy 方法,速度就會很慢。

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    createEntry(hash, key, value, bucketIndex);
}

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int) Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

List 的代碼大家可自行查看,也是阻塞性的,擴容策略是原長度的 1.5 倍。

由于集合在代碼中使用的頻率非常高,如果你知道具體的數(shù)據(jù)項上限,那么不妨設置一個合理的初始化大小。比如,HashMap 需要 1024 個元素,需要 7 次擴容,會影響應用的性能。面試中會頻繁出現(xiàn)這個問題,你需要了解這些擴容操作對性能的影響。

但是要注意,像 HashMap 這種有負載因子的集合(0.75),初始化大小 = 需要的個數(shù)/負載因子+1,如果你不是很清楚底層的結構,那就不妨保持默認。

接下來,我將從數(shù)據(jù)的結構緯度和時間維度出發(fā),講解一下應用層面的優(yōu)化。

保持合適的對象粒度

給你分享一個實際案例:我們有一個并發(fā)量非常高的業(yè)務系統(tǒng),需要頻繁使用到用戶的基本數(shù)據(jù)。

如下圖所示,由于用戶的基本信息,都是存放在另外一個服務中,所以每次用到用戶的基本信息,都需要有一次網絡交互。更加讓人無法接受的是,即使是只需要用戶的性別屬性,也需要把所有的用戶信息查詢,拉取一遍。

為了加快數(shù)據(jù)的查詢速度,對數(shù)據(jù)進行了初步的緩存,放入到了 Redis 中,查詢性能有了大的改善,但每次還是要查詢很多冗余數(shù)據(jù)。

原始的 redis key 是這樣設計的:

type: string 
key: user_${userid} 
value: json

這樣的設計有兩個問題:

查詢其中某個字段的值,需要把所有 json 數(shù)據(jù)查詢出來,并自行解析;

更新其中某個字段的值,需要更新整個 json 串,代價較高。

針對這種大粒度 json 信息,就可以采用打散的方式進行優(yōu)化,使得每次更新和查詢,都有聚焦的目標。

接下來對 Redis 中的數(shù)據(jù)進行了以下設計,采用 hash 結構而不是 json 結構:

type: hash 
key: user_${userid} 
value: {sex:f, id:1223, age:23}

這樣,我們使用 hget 命令,或者 hmget 命令,就可以獲取到想要的數(shù)據(jù),加快信息流轉的速度。

Bitmap 把對象變小

除了以上操作,還能再進一步優(yōu)化嗎?比如,我們系統(tǒng)中就頻繁用到了用戶的性別數(shù)據(jù),用來發(fā)放一些禮品,推薦一些異性的好友,定時循環(huán)用戶做一些清理動作等;或者,存放一些用戶的狀態(tài)信息,比如是否在線,是否簽到,最近是否發(fā)送信息等,從而統(tǒng)計一下活躍用戶等。那么對是、否這兩個值的操作,就可以使用 Bitmap 這個結構進行壓縮。

這里還有個高頻面試問題,那就是 Java 的 Boolean 占用的是多少位?

在 Java 虛擬機規(guī)范里,描述是:將 Boolean 類型映射成的是 1 和 0 兩個數(shù)字,它占用的空間是和 int 相同的 32 位。即使有的虛擬機實現(xiàn)把 Boolean 映射到了 byte 類型上,它所占用的空間,對于大量的、有規(guī)律的 Boolean 值來說,也是太大了。

如代碼所示,通過判斷 int 中的每一位,它可以保存 32 個 Boolean 值!

int a= 0b0001_0001_1111_1101_1001_0001_1111_1101;

Bitmap 就是使用 Bit 進行記錄的數(shù)據(jù)結構,里面存放的數(shù)據(jù)不是 0 就是 1。還記得我們在之前 《分布式緩存系統(tǒng)必須要解決的四大問題》中提到的緩存穿透嗎?就可以使用 Bitmap 避免,Java 中的相關結構類,就是 java.util.BitSet,BitSet 底層是使用 long 數(shù)組實現(xiàn)的,所以它的最小容量是 64。

10 億的 Boolean 值,只需要 128MB 的內存,下面既是一個占用了 256MB 的用戶性別的判斷邏輯,可以涵蓋長度為 10 億的 ID。

static BitSet missSet = new BitSet(010_000_000_000); 
static BitSet sexSet = new BitSet(010_000_000_000); 
String getSex(int userId) { 
    boolean notMiss = missSet.get(userId); 
    if (!notMiss) { 
        //lazy fetch 
        String lazySex = dao.getSex(userId); 
        missSet.set(userId, true); 
        sexSet.set(userId, "female".equals(lazySex)); 
    } 
    return sexSet.get(userId) ? "female" : "male"; 
}

這些數(shù)據(jù),放在堆內內存中,還是過大了。幸運的是,Redis 也支持 Bitmap 結構,如果內存有壓力,我們可以把這個結構放到 Redis 中,判斷邏輯也是類似的。

再插一道面試算法題:給出一個 1GB 內存的機器,提供 60億 int 數(shù)據(jù),如何快速判斷有哪些數(shù)據(jù)是重復的?

大家可以類比思考一下。Bitmap 是一個比較底層的結構,在它之上還有一個叫作布隆過濾器的結構(Bloom Filter),布隆過濾器可以判斷一個值不存在,或者可能存在。

如圖,它相比較 Bitmap,它多了一層 hash 算法。既然是 hash 算法,就會有沖突,所以有可能有多個值落在同一個 bit 上。它不像 HashMap一樣,使用鏈表或者紅黑樹來處理沖突,而是直接將這個hash槽重復使用。從這個特性我們能夠看出,布隆過濾器能夠明確表示一個值不在集合中,但無法判斷一個值確切的在集合中。

Guava 中有一個 BloomFilter 的類,可以方便地實現(xiàn)相關功能。

上面這種優(yōu)化方式,本質上也是把大對象變成小對象的方式,在軟件設計中有很多類似的思路。比如像一篇新發(fā)布的文章,頻繁用到的是摘要數(shù)據(jù),就不需要把整個文章內容都查詢出來;用戶的 feed 信息,也只需要保證可見信息的速度,而把完整信息存放在速度較慢的大型存儲里。

數(shù)據(jù)的冷熱分離

數(shù)據(jù)除了橫向的結構緯度,還有一個縱向的時間維度,對時間維度的優(yōu)化,最有效的方式就是冷熱分離。

所謂熱數(shù)據(jù),就是靠近用戶的,被頻繁使用的數(shù)據(jù);而冷數(shù)據(jù)是那些訪問頻率非常低,年代非常久遠的數(shù)據(jù)。

同一句復雜的 SQL,運行在幾千萬的數(shù)據(jù)表上,和運行在幾百萬的數(shù)據(jù)表上,前者的效果肯定是很差的。所以,雖然你的系統(tǒng)剛開始上線時速度很快,但隨著時間的推移,數(shù)據(jù)量的增加,就會漸漸變得很慢。

冷熱分離是把數(shù)據(jù)分成兩份,如下圖,一般都會保持一份全量數(shù)據(jù),用來做一些耗時的統(tǒng)計操作。

由于冷熱分離在工作中經常遇到,所以面試官會頻繁問到數(shù)據(jù)冷熱分離的方案。下面簡單介紹三種:

數(shù)據(jù)雙寫

把對冷熱庫的插入、更新、刪除操作,全部放在一個統(tǒng)一的事務里面。由于熱庫(比如 MySQL)和冷庫(比如 Hbase)的類型不同,這個事務大概率會是分布式事務。在項目初期,這種方式是可行的,但如果是改造一些遺留系統(tǒng),分布式事務基本上是改不動的,我通常會把這種方案直接廢棄掉。

寫入 MQ 分發(fā)

通過 MQ 的發(fā)布訂閱功能,在進行數(shù)據(jù)操作的時候,先不落庫,而是發(fā)送到 MQ 中。單獨啟動消費進程,將 MQ 中的數(shù)據(jù)分別落到冷庫、熱庫中。使用這種方式改造的業(yè)務,邏輯非常清晰,結構也比較優(yōu)雅。像訂單這種結構比較清晰、對順序性要求較低的系統(tǒng),就可以采用 MQ 分發(fā)的方式。但如果你的數(shù)據(jù)庫實體量非常大,用這種方式就要考慮程序的復雜性了。

使用 Binlog 同步

針對 MySQL,就可以采用 Binlog 的方式進行同步,使用 Canal 組件,可持續(xù)獲取最新的 Binlog 數(shù)據(jù),結合 MQ,可以將數(shù)據(jù)同步到其他的數(shù)據(jù)源中。

思維發(fā)散

對于結果集的操作,我們可以再發(fā)散一下思維??梢詫⒁粋€簡單冗余的結果集,改造成復雜高效的數(shù)據(jù)結構。這個復雜的數(shù)據(jù)結構可以代理我們的請求,有效地轉移耗時操作。

比如,我們常用的數(shù)據(jù)庫索引,就是一種對數(shù)據(jù)的重新組織、加速。B+ tree 可以有效地減少數(shù)據(jù)庫與磁盤交互的次數(shù),它通過類似 B+ tree 的數(shù)據(jù)結構,將最常用的數(shù)據(jù)進行索引,存儲在有限的存儲空間中。

還有就是,在 RPC 中常用的序列化。有的服務是采用的 SOAP 協(xié)議的 WebService,它是基于 XML 的一種協(xié)議,內容大傳輸慢,效率低下。現(xiàn)在的 Web 服務中,大多數(shù)是使用 json 數(shù)據(jù)進行交互的,json 的效率相比 SOAP 就更高一些。

另外,大家應該都聽過 google 的 protobuf,由于它是二進制協(xié)議,而且對數(shù)據(jù)進行了壓縮,性能是非常優(yōu)越的。protobuf 對數(shù)據(jù)壓縮后,大小只有 json 的 1/10,xml 的 1/20,但是性能卻提高了 5-100 倍。

protobuf 的設計是值得借鑒的,它通過 tag|leng|value 三段對數(shù)據(jù)進行了非常緊湊的處理,解析和傳輸速度都特別快。

小結

最后總結一下本文的內容重點:

首先,我們看了比較老的 JDK 版本中,String 為了復用引起的內容泄漏問題,所以我們平常的編碼中,一定要注意大對象的回收,及時切斷與它的聯(lián)系。

接下來,我們看了 Java 中集合的一些擴容操作,如果你知道確切的集合大小,就可以指定一個初始值,避免耗時的擴容操作。

針對大對象,我們有結構緯度的優(yōu)化和時間維度的優(yōu)化兩種方法:

從結構緯度來說,通過把對象切分成合適的粒度,可以把操作集中在小數(shù)據(jù)結構上,減少時間處理成本;通過把對象進行壓縮、轉換,或者提取熱點數(shù)據(jù),就可以避免大對象的存儲和傳輸成本。

從時間緯度來說,就可以通過冷熱分離的手段,將常用的數(shù)據(jù)存放在高速設備中,減少數(shù)據(jù)處理的集合,加快處理速度。

到現(xiàn)在為止,我們學習了緩沖、緩存、對象池化、結果緩存池、大對象處理等優(yōu)化性能的手段,由于它們都加入了額外的中間層,會使得編程模型變得復雜。

到此這篇關于詳解處理Java中的大對象的方法的文章就介紹到這了,更多相關Java大對象內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • Java利用redis zset實現(xiàn)延時任務詳解

    Java利用redis zset實現(xiàn)延時任務詳解

    zset作為redis的有序集合數(shù)據(jù)結構存在,排序的依據(jù)就是score。本文就將利用zset score這個排序的這個特性,來實現(xiàn)延時任務,感興趣的可以了解一下
    2022-08-08
  • Mybatis SQL運行流程源碼詳解

    Mybatis SQL運行流程源碼詳解

    這篇文章主要介紹了Mybatis SQL運行流程源碼詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-10-10
  • Java實現(xiàn)合并word文檔的示例代碼

    Java實現(xiàn)合并word文檔的示例代碼

    在做項目中,經常會遇到一種情況,需要將一個小word文檔的內容插入到一個大word(主文檔)中。本文就為大家準備了Java實現(xiàn)合并word文檔的方法,需要的可以參考一下
    2022-08-08
  • 詳解SpringBoot與SpringCloud的版本對應詳細版

    詳解SpringBoot與SpringCloud的版本對應詳細版

    這篇文章主要介紹了詳解SpringBoot與SpringCloud的版本對應詳細版,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-09-09
  • DoytoQuery 聚合查詢方案示例詳解

    DoytoQuery 聚合查詢方案示例詳解

    這篇文章主要為大家介紹了DoytoQuery 聚合查詢方案示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-12-12
  • springboot默認文件緩存(easy-captcha?驗證碼)

    springboot默認文件緩存(easy-captcha?驗證碼)

    這篇文章主要介紹了springboot的文件緩存(easy-captcha?驗證碼),本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-06-06
  • MyBatis攔截器動態(tài)替換表名的方法詳解

    MyBatis攔截器動態(tài)替換表名的方法詳解

    因為我們持久層框架更多地使用MyBatis,那我們就借助于MyBatis的攔截器來完成我們的功能,這篇文章主要給大家介紹了關于MyBatis攔截器動態(tài)替換表名的相關資料,需要的朋友可以參考下
    2022-04-04
  • 教新手使用java如何對一個大的文本文件內容進行去重

    教新手使用java如何對一個大的文本文件內容進行去重

    用HashSet對內容去重這個過程jvm會內存溢出,只能首先將這個大文件中的內容讀取出來,對每行String的hashCode取模取正整數(shù),可用取模結果作為文件名,將相同模數(shù)的行寫入同一個文件,再單獨對每個小文件進行去重,最后再合并
    2021-06-06
  • SpringCloud創(chuàng)建多模塊項目的實現(xiàn)示例

    SpringCloud創(chuàng)建多模塊項目的實現(xiàn)示例

    ,Spring Cloud作為一個強大的微服務框架,提供了豐富的功能和組件,本文主要介紹了SpringCloud創(chuàng)建多模塊項目的實現(xiàn)示例,具有一定的參考價值,感興趣的可以了解一下
    2024-02-02
  • 關于Spring事務隔離、傳播屬性與@Transactional注解

    關于Spring事務隔離、傳播屬性與@Transactional注解

    這篇文章主要介紹了關于事務隔離、Spring傳播屬性與@Transactional注解,如果一組處理步驟或者全部發(fā)生或者一步也不執(zhí)行,我們稱該組處理步驟為一個事務,需要的朋友可以參考下
    2023-05-05

最新評論