溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

幾百萬數(shù)據(jù)放入內(nèi)存不會把系統(tǒng)撐爆嗎?

發(fā)布時間:2020-06-27 11:10:14 來源:網(wǎng)絡 閱讀:2902 作者:wx5d6cccb1cb158 欄目:編程語言

在公司有一個需求是要核對一批數(shù)據(jù),之前的做法是直接用SQL各種復雜操作給懟出來的,不僅時間慢,而且后期也不好維護,就算原作者來了過一個月估計也忘了SQL什么意思了,于是有一次我就想著問一下之前做這個需求的人為什么不將這些數(shù)據(jù)查出來后在內(nèi)存里面做篩選呢?直接說了你不怕把內(nèi)存給撐爆嗎?此核算服務器是單獨的服務器,配置是四核八G的,配置堆的大小是4G。本著懷疑的精神,就想要弄清楚幾百萬條數(shù)據(jù)真的放入內(nèi)存的話會占用多少內(nèi)存呢?

計算機的存儲單位
計算機的存儲單位常用的有bit、Byte、KB、MB、GB、TB后面還有但是我們基本上用不上就不說了,我們經(jīng)常將bit稱之為比特或者位、將Byte簡稱為B或者字節(jié),將KB簡稱為K,將MB稱之為M或者兆,將GB簡稱為G。那么他們的換算單位是怎樣的呢?

換算關系
首先我們得知道在計算機中所有數(shù)據(jù)都是由0 1來組成的,那么存儲0 1這些二進制數(shù)據(jù)是由什么存放呢?就是由bit存放的,一個bit存放一位二進制數(shù)字。所以bit是計算機最小的單位。

大部分計算機目前都是使用8位的塊,就是我們上面稱之為的字節(jié)Byte,來作為計算機容量的基本單位。所以我們一般稱一個字符或者一個數(shù)字都是稱之為占用了多少字節(jié)。

了解了上面關于位和字節(jié)的關系后,我們可以看一下其他的單位換算關系

11B(Byte 字節(jié)) = 8bit(位)
21KB = 1024B
31MB = 1024KB
41GB = 1024MB
51TB = 1024GB

Java中對象占用多少內(nèi)存
在了解了上面的換算關系后,我們來了解一下新建一個Java對象需要多少內(nèi)存。

Java基本類型
我們知道Java類型分為基本類型和引用類型,八大基本類型有int、short、long、byte、float、double、boolean、char
幾百萬數(shù)據(jù)放入內(nèi)存不會把系統(tǒng)撐爆嗎?

至于為什么Java中的char無論是中英文數(shù)字都占用兩個字節(jié),是因為Java中使用Unicode字符,所有的字符均以兩個字節(jié)存儲。

Java引用類型
在一個對象中除了有基本數(shù)據(jù)類型以外,我們也會有一些引用類型,引用類型的對象比較特殊,因為這些對象真正存儲在虛擬機中的堆內(nèi)存中,對象中只是存儲了一個引用而已,如果是引用類型那么就會存儲一個指向該引用的指針。指針默認情況下是占用4字節(jié),是因為開啟了指針壓縮,如果沒有開的話,那么一個引用就占用8個字節(jié)。

對象在內(nèi)存中的布局
在HotSpot虛擬機中,對象在內(nèi)存中存儲的布局可以分為三個區(qū)域:對象頭(Header)、實例數(shù)據(jù)(Instance Data)、對齊填充(Padding)。
幾百萬數(shù)據(jù)放入內(nèi)存不會把系統(tǒng)撐爆嗎?

對象頭

在對象頭中存儲了兩部分數(shù)據(jù)

運行時數(shù)據(jù):存儲了對象自身運行時的數(shù)據(jù),例如哈希碼、GC分代的年齡、鎖狀態(tài)標志、線程持有的鎖、偏向線程ID等等。這部分數(shù)據(jù)在32位和64位的虛擬機中分別為32bit和64bit
類型指針:對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。如果對象是一個Java數(shù)組的話,那么對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù)(占用4個字節(jié))。所以這是一個指針,默認JVM對指針進行了壓縮,用4個字節(jié)存儲。
我們以虛擬機為64位的機器為例,那么對象頭占用的內(nèi)存是8(運行時數(shù)據(jù))+4(類型指針)=12Byte。如果是數(shù)組的話那么就是16Byte

實例數(shù)據(jù)

實例數(shù)據(jù)中也擁有兩部分數(shù)據(jù),一部分是基本類型數(shù)據(jù),一部分是引用指針。這兩部分數(shù)據(jù)我們在上面已經(jīng)講了。具體占用多少內(nèi)存我們需要結合具體的對象繼續(xù)分析,下面我們會有具體的分析。

從父類中繼承下來的變量也是需要進行計算的

對齊填充

對齊填充并不是必然存在的,也沒有特別的含義。它僅僅起著占位符的作用。由于HotSpot VM的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,換句話說就是對象的大小必須是8字節(jié)的整數(shù)倍。而如果對象頭加上實例數(shù)據(jù)不是8的整數(shù)倍的話那么就會通過對其填充進行補全。

實戰(zhàn)演練

我們在上面分析一大堆,那么是不是就如我們分析的一樣,新建一個對象在內(nèi)存中的分配大小就是如此呢?我們可以新建一個對象。

lass Animal{

   private int age;

}

那么怎么知道這個對象在內(nèi)存中占用多少內(nèi)存呢?JDK提供了一個工具jol-core可以給我們分析出來一個對象在內(nèi)存中占用的內(nèi)存大小。直接在項目中引入包即可。

--Gradle
compile 'org.openjdk.jol:jol-core:0.9'

--Maven
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

然后我們在main函數(shù)中調(diào)用如下

public class AboutObjectMemory {

    public static void main(String[] args) {
        System.out.print(ClassLayout.parseClass(Animal.class).toPrintable());
    }
}

就可以查看到輸出的內(nèi)容了,可以看到輸出結果占用的內(nèi)存是16字節(jié),和我們分析的一樣。

aboutjava.other.Animal object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4    int Animal.age                                N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

String占用多少內(nèi)存
String字符串在Java中是個特殊的存在,比如一個字符串"abcdefg"這樣一個字符串占用多少字節(jié)呢?相信會有人回答說是7個字節(jié)或者是14個字節(jié),這兩個答案都是不準確的,我們先看一下String類在內(nèi)存中占用的內(nèi)存是多少。
我們先自己進行分析一下。在String類中有兩個屬性,其中對象頭固定了是12字節(jié),int是4字節(jié),char[]數(shù)組其實在這里相當于引用對象存的,所以存的是地址,因此占用4個字節(jié),所以大小為對象頭12Byte+實例數(shù)據(jù)8Byte+填充數(shù)據(jù)4Byte=24Byte這里的對象頭和實例數(shù)據(jù)加起來不是8的倍數(shù),所以需要填充數(shù)據(jù)進行填充。

private final char value[];

private int hash; // Default to 0

那么我們分析的到底對不對呢,我們還是用上面的工具進行分析一下??梢钥吹轿覀兯愠龅慕Y果和我們分析的結果是一致的。

java.lang.String object internals:
 OFFSET  SIZE     TYPE DESCRIPTION                               VALUE
      0    12          (object header)                           N/A
     12     4   char[] String.value                              N/A
     16     4      int String.hash                               N/A
     20     4          (loss due to the next object alignment)
Instance size: 24 bytes

那么一個空字符串占用多少內(nèi)存呢?我們剛才得到的是一個String對象占用了24字節(jié),其實char[]數(shù)組還是會占用內(nèi)存的,我們在上面講對象頭的時候說過,數(shù)組對象也是一個實例對象,它的對象頭比一般的對象多出來4字節(jié),用來描述此數(shù)組的長度,所以char[]數(shù)組的對象頭長度為16字節(jié),由于此時是空字符串,所以實例數(shù)據(jù)長度為0。因此一個空char[]數(shù)組占用內(nèi)存大小為對象頭16Byte+實例數(shù)據(jù)0Byte=16Byte。一個空字符串占用內(nèi)存為String對象+char[]數(shù)組對象=40Byte

那么我們上面舉的例子abcdefg占用多少內(nèi)存呢?其中String對象占用的內(nèi)存是不會變了,變化的是char[]數(shù)組中的內(nèi)容,這里我們需要知道字符串是存放于char[]數(shù)組中的,而一個char占用2個字節(jié),所以abcdefg的char[]數(shù)組大小為對象頭16Byte+實例數(shù)據(jù)14Byte+對齊填充2Byte=32Byte。那么abcdefg占用內(nèi)存大小就是String對象+char[]數(shù)組對象=56Byte

用List存儲對象
那么我們在內(nèi)存中放入二千萬個這個對象的話,需要占用多少內(nèi)存呢?根據(jù)上面的知識我們能大概估算一下。我們定義一個List數(shù)組用于存放此對象,不讓其回收。

List<Animal> animals = new ArrayList<>(20000000);
for (int i = 0; i < 20000000; i++) {
    Animal animal = new Animal();
    animals.add(animal);
}

注意這里我是直接將集合的大小初始化為了二千萬的大小,所以程序在正常啟動的時候占用內(nèi)存是100+MB,正常程序啟動僅僅占用30+MB的,所以多出來的60+MB正好是我們初始化的數(shù)組的大小。至于為什么要初始化大小的原因就是為了消除集合在擴容時對我們觀察結果的影響

這里我貼一張,集合未初始化大小和初始化大小內(nèi)存占用對比圖,大家可以看到是有內(nèi)存上的差異,在ArrayList數(shù)組中用于存放數(shù)據(jù)的是transient Object[] elementData;Object數(shù)組,所以它里面存放的是指向?qū)ο蟮闹羔槪粋€指針占用4個字節(jié),所以就有兩千萬個指針,那么就是76M。我們可以看到差異圖和我們預想的一樣。
幾百萬數(shù)據(jù)放入內(nèi)存不會把系統(tǒng)撐爆嗎?

上面我們已經(jīng)算出來了一個Animal對象占用16個字節(jié),所以兩千萬個占用大概是305MB,和集合加起來就是將近380MB的空間大小,接下來我們就啟動程序來看一下我們結果是不是對的呢,接下來我用的jconsole工具查看內(nèi)存占用情況。
幾百萬數(shù)據(jù)放入內(nèi)存不會把系統(tǒng)撐爆嗎?

我們可以看到和我們預算的結果是相吻合的。

那么以后如果有大量的對象需要從數(shù)據(jù)庫中查找出來放入內(nèi)存的話,那么如果是使用對象來接的話,那么我們就應該盡量減少對象中的字段,因為即使你不賦值,其實他也是占用著內(nèi)存的,我們接下來再舉個例子看一下對個屬性值的話占用內(nèi)存是不是又高了。我們將Animal對象改造如下

class Animal{

    private int age;
    private int age1;
    private int age2;
    private int age3;
    private int age4;

}

此時我們能夠計算得到一個Animal對象占用的內(nèi)存大小是(對象頭12Byte+實例數(shù)據(jù)20Byte=32Byte)此時32由于是8的倍數(shù)所以無需進行填充補齊。那么此時如果還是二千萬條數(shù)據(jù)的話,此對象占用內(nèi)存應該是610MB,加上剛才集合中指針的數(shù)據(jù)76MB,那么加起來將近占用686MB,那么預期結果是否和我們的一樣呢,我們重新啟動程序觀察,可以看到下圖??梢钥吹胶臀覀兎治龅臄?shù)據(jù)是差不多的。
幾百萬數(shù)據(jù)放入內(nèi)存不會把系統(tǒng)撐爆嗎?

用Map存儲對象
用Map存儲對象計算內(nèi)存大小有些麻煩了,眾所周知Map的結構是如下圖所示。
幾百萬數(shù)據(jù)放入內(nèi)存不會把系統(tǒng)撐爆嗎?

它是一個數(shù)組加鏈表(或者紅黑樹)的結構,而數(shù)組中存放的數(shù)據(jù)是Node對象。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
}

我們舉例定義下面一個Map對象

Map<Animal,Animal> map

此時我們可以自己計算一下一個Node對象需要的內(nèi)存大小對象頭12Byte+實例數(shù)據(jù)16Byte+對其填充4Byte=32Byte,當然這里的key和value的值還需要另算,因為Node對象此時存放的僅僅是他們的引用而已。一個Animal對象所占用內(nèi)存大小我們上面也說了是16Byte,所以這里一個Node對象占用的大小為32Byte+16Byte+16Byte=64Byte。

下面我們用實際例子來驗證下我們的猜想

Map<Animal,Animal> map = new HashMap<>(20000000);
for (int i = 0; i < 20000000; i++) {
    map.put(new Animal(),new Animal());
}

上面的例子在一個Map對象中存放二千萬條數(shù)據(jù),計算大概在內(nèi)存中占用多少內(nèi)存。

數(shù)組占用內(nèi)存大?。何覀兿葋碛嬎阋幌聰?shù)組占了多少,這里有個小知識點,在HashMap中初始化大小是按照2的倍數(shù)來的,比如你定義了大小為60,那么系統(tǒng)會給你初始化大小為64。所以我們定義為二千萬,系統(tǒng)其實是會給我們初始化為33554432,所以此時僅僅HashMap中數(shù)組就占用了將近132MB
數(shù)據(jù)占用內(nèi)存大小:我們上面計算了一個Node節(jié)點占用了64Byte,那么兩千萬條數(shù)據(jù)就占用了1280MB
兩個占用內(nèi)存大小相加我們可以知道大概系統(tǒng)中占用了1.4G內(nèi)存的大小。那么事實是否是我們想象的呢?我們運行程序可以看到內(nèi)存大小如圖所示??梢钥吹浇Y果確實和我們猜想的一樣。
幾百萬數(shù)據(jù)放入內(nèi)存不會把系統(tǒng)撐爆嗎?

總結
回歸到上面所說的需求,幾百萬數(shù)據(jù)放到內(nèi)存中會把內(nèi)存撐爆嗎?這時候你可以通過自己的計算得到。最終我們那個需求經(jīng)過我算出來其實占用內(nèi)存量幾百兆,對于4個G的堆內(nèi)存來說其實遠遠還沒達到撐爆的地步。所以有時候我們對任何東西都要存在懷疑的態(tài)度。大家可以到GitHub中下載代碼自己在本地跑一下監(jiān)測一下,并且可以自己定義幾個對象然后計算看是不是和圖中的內(nèi)存大小一致。這樣才能記憶更深刻。送給大家一句話從來如此,便對嗎?。其實我寫的文章里面也留了一個小坑,大家可以試著找找,是在對集合進行初始化計算那一塊。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI