目錄
String中的substring
集合大物件擴容
保持合適的物件粒度
Bitmap 把物件變小
数据的冷热分离
数据双写
写入 MQ 分发
使用 Binlog 同步
思维发散
首頁 Java java教程 如何處理Java中的大對象

如何處理Java中的大對象

Apr 19, 2023 am 08:07 AM
java

String中的substring

我們都知道,String 在 Java 中是不可變的,如果你改動了其中的內容,它就會產生一個新的字串。如果我們想要用到字串中的一部分數據,就可以使用 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 產生了一個新的字串,這個字串透過建構函式的 Arrays.copyOfRange 函式進行建構。

這個函數在 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;
}
登入後複製

可以看到,它在創建子字串的時候,並不僅僅拷貝所需要的對象,而是把整個 value 引用了起來。如果原字串比較大,即使不再使用,記憶體也不會釋放。

例如,一篇文章內容可能有幾兆,我們只是需要其中的摘要訊息,也不得不維持整個的大物件。

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

這對我們的借鏡意義是:如果你創建了比較大的對象,並基於這個對像生成了一些其他的信息,這個時候,一定要記得去掉和這個大對象的引用關係。

集合大物件擴容

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

我們先來看下 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 複製來源資料。

以下是 HashMap 的擴容程式碼,擴容後大小也是翻倍。它的擴容動作就複雜得多,除了有負載因子的影響,它還需要把原來的資料重新進行散列,由於無法使用 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 倍。

由於集合在程式碼中使用的頻率非常高,如果你知道特定的資料項目上限,那麼不妨設定一個合理的初始化大小。例如,HashMap 需要 1024 個元素,需要 7 次擴容,會影響應用的效能。面試中會經常出現這個問題,你需要了解這些擴充操作對表現的影響。

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

接下來,我將從資料的結構緯度和時間維度出發,並說明應用層面的最佳化。

保持合適的物件粒度

給你分享一個實際案例:我們有一個並發量非常高的業務系統,需要頻繁使用到使用者的基本資料。

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

如何處理Java中的大對象

為了加快資料的查詢速度,對資料進行了初步的緩存,放入到了Redis 中,查詢效能有了大的改善,但每次還是要查詢很多冗餘數據。

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

type: string 
key: user_${userid} 
value: json
登入後複製

這樣的設計有兩個問題:

查詢其中某個欄位的值,需要把所有json 資料查詢出來,自行解析;

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

針對這種大粒度 json 訊息,就可以採用打散的方式進行最佳化,使得每次更新和查詢,都有聚焦的目標。

接下來對Redis 中的資料進行了以下設計,採用hash 結構而不是json 結構:

type: hash 
key: user_${userid} 
value: {sex:f, id:1223, age:23}
登入後複製

這樣,我們使用hget 命令,或者hmget 命令,就可以取得到想要的數據,加快資訊流轉的速度。

Bitmap 把物件變小

除了以上操作,還能再進一步優化嗎?例如,我們系統中就頻繁用到了用戶的性別數據,用來發放一些禮品,推荐一些異性的好友,定時循環用戶做一些清理動作等;或者,存放一些用戶的狀態信息,比如是否在線,是否簽到,最近是否發送訊息等,從而統計一下活躍用戶等。那麼對是、否這兩個值的操作,就可以使用 Bitmap 這個結構來壓縮。

这里还有个高频面试问题,那就是 Java 的 Boolean 占用的是多少位?

在 Java 虚拟机规范里,描述是:将 Boolean 类型映射成的是 1 和 0 两个数字,它占用的空间是和 int 相同的 32 位。即使有的虚拟机实现把 Boolean 映射到了 byte 类型上,它所占用的空间,对于大量的、有规律的 Boolean 值来说,也是太大了。

如代码所示,通过判断 int 中的每一位,它可以保存 32 个 Boolean 值!

int a= 0b0001_0001_1111_1101_1001_0001_1111_1101;
登入後複製

Bitmap 就是使用 Bit 进行记录的数据结构,里面存放的数据不是 0 就是 1。还记得我们在之前 《分布式缓存系统必须要解决的四大问题》中提到的缓存穿透吗?就可以使用 Bitmap 避免,Java 中的相关结构类,就是 java.util.BitSet,BitSet 底层是使用 long 数组实现的,所以它的最小容量是 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"; 
}
登入後複製

这些数据,放在堆内内存中,还是过大了。幸运的是,Redis 也支持 Bitmap 结构,如果内存有压力,我们可以把这个结构放到 Redis 中,判断逻辑也是类似的。

再插一道面试算法题:给出一个 1GB 内存的机器,提供 60亿 int 数据,如何快速判断有哪些数据是重复的?

大家可以类比思考一下。Bitmap 是一个比较底层的结构,在它之上还有一个叫作布隆过滤器的结构(Bloom Filter),布隆过滤器可以判断一个值不存在,或者可能存在。

如何處理Java中的大對象

如图,它相比较 Bitmap,它多了一层 hash 算法。既然是 hash 算法,就会有冲突,所以有可能有多个值落在同一个 bit 上。它不像 HashMap一样,使用链表或者红黑树来处理冲突,而是直接将这个hash槽重复使用。从这个特性我们能够看出,布隆过滤器能够明确表示一个值不在集合中,但无法判断一个值确切的在集合中。

Guava 中有一个 BloomFilter 的类,可以方便地实现相关功能。

上面这种优化方式,本质上也是把大对象变成小对象的方式,在软件设计中有很多类似的思路。比如像一篇新发布的文章,频繁用到的是摘要数据,就不需要把整个文章内容都查询出来;用户的 feed 信息,也只需要保证可见信息的速度,而把完整信息存放在速度较慢的大型存储里。

数据的冷热分离

数据除了横向的结构纬度,还有一个纵向的时间维度,对时间维度的优化,最有效的方式就是冷热分离。

所谓热数据,就是靠近用户的,被频繁使用的数据;而冷数据是那些访问频率非常低,年代非常久远的数据。

同一句复杂的 SQL,运行在几千万的数据表上,和运行在几百万的数据表上,前者的效果肯定是很差的。所以,虽然你的系统刚开始上线时速度很快,但随着时间的推移,数据量的增加,就会渐渐变得很慢。

冷热分离是把数据分成两份,如下图,一般都会保持一份全量数据,用来做一些耗时的统计操作。

如何處理Java中的大對象

由于冷热分离在工作中经常遇到,所以面试官会频繁问到数据冷热分离的方案。下面简单介绍三种:

数据双写

把对冷热库的插入、更新、删除操作,全部放在一个统一的事务里面。由于热库(比如 MySQL)和冷库(比如 Hbase)的类型不同,这个事务大概率会是分布式事务。在项目初期,这种方式是可行的,但如果是改造一些遗留系统,分布式事务基本上是改不动的,我通常会把这种方案直接废弃掉。

写入 MQ 分发

通过 MQ 的发布订阅功能,在进行数据操作的时候,先不落库,而是发送到 MQ 中。单独启动消费进程,将 MQ 中的数据分别落到冷库、热库中。使用这种方式改造的业务,逻辑非常清晰,结构也比较优雅。像订单这种结构比较清晰、对顺序性要求较低的系统,就可以采用 MQ 分发的方式。但如果你的数据库实体量非常大,用这种方式就要考虑程序的复杂性了。

使用 Binlog 同步

针对 MySQL,就可以采用 Binlog 的方式进行同步,使用 Canal 组件,可持续获取最新的 Binlog 数据,结合 MQ,可以将数据同步到其他的数据源中。

思维发散

对于结果集的操作,我们可以再发散一下思维。可以将一个简单冗余的结果集,改造成复杂高效的数据结构。这个复杂的数据结构可以代理我们的请求,有效地转移耗时操作。

例如,我們常用的資料庫索引,就是一種對資料的重新組織、加速。 B tree 可以有效地減少資料庫與磁碟互動的次數,它透過類似 B tree 的資料結構,將最常用的資料進行索引,儲存在有限的儲存空間中。

還有就是,在 RPC 中常用的序列化。有的服務是採用的 SOAP 協議的 WebService,它是基於 XML 的一種協議,內容大傳輸慢,效率低。現在的 Web 服務中,大多數是使用 json 資料進行互動的,json 的效率相比 SOAP 就更高一些。

另外,大家應該都聽過 google 的 protobuf,由於它是二進位協議,而且對數據進行了壓縮,性能是非常優越的。 protobuf 壓縮資料後,大小只有 json 的 1/10,xml 的 1/20,但效能卻提高了 5-100 倍。

protobuf 的設計是值得借鏡的,它透過 tag|leng|value 三段對資料進行了非常緊湊的處理,解析和傳輸速度都特別快。

以上是如何處理Java中的大對象的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它們
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

Java 中的完美數 Java 中的完美數 Aug 30, 2024 pm 04:28 PM

Java 完美數指南。這裡我們討論定義,如何在 Java 中檢查完美數?

Java 中的隨機數產生器 Java 中的隨機數產生器 Aug 30, 2024 pm 04:27 PM

Java 隨機數產生器指南。在這裡,我們透過範例討論 Java 中的函數,並透過範例討論兩個不同的生成器。

Java中的Weka Java中的Weka Aug 30, 2024 pm 04:28 PM

Java 版 Weka 指南。這裡我們透過範例討論簡介、如何使用 weka java、平台類型和優點。

Java 中的史密斯數 Java 中的史密斯數 Aug 30, 2024 pm 04:28 PM

Java 史密斯數指南。這裡我們討論定義,如何在Java中檢查史密斯號?帶有程式碼實現的範例。

Java Spring 面試題 Java Spring 面試題 Aug 30, 2024 pm 04:29 PM

在本文中,我們保留了最常被問到的 Java Spring 面試問題及其詳細答案。這樣你就可以順利通過面試。

突破或從Java 8流返回? 突破或從Java 8流返回? Feb 07, 2025 pm 12:09 PM

Java 8引入了Stream API,提供了一種強大且表達力豐富的處理數據集合的方式。然而,使用Stream時,一個常見問題是:如何從forEach操作中中斷或返回? 傳統循環允許提前中斷或返回,但Stream的forEach方法並不直接支持這種方式。本文將解釋原因,並探討在Stream處理系統中實現提前終止的替代方法。 延伸閱讀: Java Stream API改進 理解Stream forEach forEach方法是一個終端操作,它對Stream中的每個元素執行一個操作。它的設計意圖是處

Java 中的時間戳至今 Java 中的時間戳至今 Aug 30, 2024 pm 04:28 PM

Java 中的時間戳記到日期指南。這裡我們也結合範例討論了介紹以及如何在java中將時間戳記轉換為日期。

Java程序查找膠囊的體積 Java程序查找膠囊的體積 Feb 07, 2025 am 11:37 AM

膠囊是一種三維幾何圖形,由一個圓柱體和兩端各一個半球體組成。膠囊的體積可以通過將圓柱體的體積和兩端半球體的體積相加來計算。本教程將討論如何使用不同的方法在Java中計算給定膠囊的體積。 膠囊體積公式 膠囊體積的公式如下: 膠囊體積 = 圓柱體體積 兩個半球體體積 其中, r: 半球體的半徑。 h: 圓柱體的高度(不包括半球體)。 例子 1 輸入 半徑 = 5 單位 高度 = 10 單位 輸出 體積 = 1570.8 立方單位 解釋 使用公式計算體積: 體積 = π × r2 × h (4

See all articles