幾百萬數據放入內存不會把系統撐爆嗎?

在公司有一個需求是要覈對一批數據,以前的作法是直接用SQL各類複雜操做給懟出來的,不只時間慢,並且後期也很差維護,就算原做者來了過一個月估計也忘了SQL什麼意思了,因而有一次我就想着問一下以前作這個需求的人爲何不將這些數據查出來後在內存裏面作篩選呢?直接說了你不怕把內存給撐爆嗎?此覈算服務器是單獨的服務器,配置是四核八G的,配置堆的大小是4G。本着懷疑的精神,就想要弄清楚幾百萬條數據真的放入內存的話會佔用多少內存呢?java

計算機的存儲單位
計算機的存儲單位經常使用的有bit、Byte、KB、MB、GB、TB後面還有可是咱們基本上用不上就不說了,咱們常常將bit稱之爲比特或者位、將Byte簡稱爲B或者字節,將KB簡稱爲K,將MB稱之爲M或者兆,將GB簡稱爲G。那麼他們的換算單位是怎樣的呢?數據庫

換算關係
首先咱們得知道在計算機中全部數據都是由0 1來組成的,那麼存儲0 1這些二進制數據是由什麼存放呢?就是由bit存放的,一個bit存放一位二進制數字。因此bit是計算機最小的單位。數組

大部分計算機目前都是使用8位的塊,就是咱們上面稱之爲的字節Byte,來做爲計算機容量的基本單位。因此咱們通常稱一個字符或者一個數字都是稱之爲佔用了多少字節。服務器

瞭解了上面關於位和字節的關係後,咱們能夠看一下其餘的單位換算關係ide

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

Java中對象佔用多少內存
在瞭解了上面的換算關係後,咱們來了解一下新建一個Java對象須要多少內存。函數

Java基本類型
咱們知道Java類型分爲基本類型和引用類型,八大基本類型有int、short、long、byte、float、double、boolean、char
幾百萬數據放入內存不會把系統撐爆嗎?工具

至於爲何Java中的char不管是中英文數字都佔用兩個字節,是由於Java中使用Unicode字符,全部的字符均以兩個字節存儲。佈局

Java引用類型
在一個對象中除了有基本數據類型之外,咱們也會有一些引用類型,引用類型的對象比較特殊,由於這些對象真正存儲在虛擬機中的堆內存中,對象中只是存儲了一個引用而已,若是是引用類型那麼就會存儲一個指向該引用的指針。指針默認狀況下是佔用4字節,是由於開啓了指針壓縮,若是沒有開的話,那麼一個引用就佔用8個字節。線程

對象在內存中的佈局
在HotSpot虛擬機中,對象在內存中存儲的佈局能夠分爲三個區域:對象頭(Header)、實例數據(Instance Data)、對齊填充(Padding)。
幾百萬數據放入內存不會把系統撐爆嗎?3d

對象頭

在對象頭中存儲了兩部分數據

運行時數據:存儲了對象自身運行時的數據,例如哈希碼、GC分代的年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID等等。這部分數據在32位和64位的虛擬機中分別爲32bit和64bit
類型指針:對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。若是對象是一個Java數組的話,那麼對象頭中還必須有一塊用於記錄數組長度的數據(佔用4個字節)。因此這是一個指針,默認JVM對指針進行了壓縮,用4個字節存儲。
咱們以虛擬機爲64位的機器爲例,那麼對象頭佔用的內存是8(運行時數據)+4(類型指針)=12Byte。若是是數組的話那麼就是16Byte

實例數據

實例數據中也擁有兩部分數據,一部分是基本類型數據,一部分是引用指針。這兩部分數據咱們在上面已經講了。具體佔用多少內存咱們須要結合具體的對象繼續分析,下面咱們會有具體的分析。

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

對齊填充

對齊填充並非必然存在的,也沒有特別的含義。它僅僅起着佔位符的做用。因爲HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是對象的大小必須是8字節的整數倍。而若是對象頭加上實例數據不是8的整數倍的話那麼就會經過對其填充進行補全。

實戰演練

咱們在上面分析一大堆,那麼是否是就如咱們分析的同樣,新建一個對象在內存中的分配大小就是如此呢?咱們能夠新建一個對象。

lass Animal{

   private int age;

}

那麼怎麼知道這個對象在內存中佔用多少內存呢?JDK提供了一個工具jol-core能夠給咱們分析出來一個對象在內存中佔用的內存大小。直接在項目中引入包便可。

--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函數中調用以下

public class AboutObjectMemory {

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

就能夠查看到輸出的內容了,能夠看到輸出結果佔用的內存是16字節,和咱們分析的同樣。

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佔用多少內存
String字符串在Java中是個特殊的存在,好比一個字符串"abcdefg"這樣一個字符串佔用多少字節呢?相信會有人回答說是7個字節或者是14個字節,這兩個答案都是不許確的,咱們先看一下String類在內存中佔用的內存是多少。
咱們先本身進行分析一下。在String類中有兩個屬性,其中對象頭固定了是12字節,int是4字節,char[]數組其實在這裏至關於引用對象存的,因此存的是地址,所以佔用4個字節,因此大小爲對象頭12Byte+實例數據8Byte+填充數據4Byte=24Byte這裏的對象頭和實例數據加起來不是8的倍數,因此須要填充數據進行填充。

private final char value[];

private int hash; // Default to 0

那麼咱們分析的到底對不對呢,咱們仍是用上面的工具進行分析一下。能夠看到咱們算出的結果和咱們分析的結果是一致的。

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

那麼一個空字符串佔用多少內存呢?咱們剛纔獲得的是一個String對象佔用了24字節,其實char[]數組仍是會佔用內存的,咱們在上面講對象頭的時候說過,數組對象也是一個實例對象,它的對象頭比通常的對象多出來4字節,用來描述此數組的長度,因此char[]數組的對象頭長度爲16字節,因爲此時是空字符串,因此實例數據長度爲0。所以一個空char[]數組佔用內存大小爲對象頭16Byte+實例數據0Byte=16Byte。一個空字符串佔用內存爲String對象+char[]數組對象=40Byte

那麼咱們上面舉的例子abcdefg佔用多少內存呢?其中String對象佔用的內存是不會變了,變化的是char[]數組中的內容,這裏咱們須要知道字符串是存放於char[]數組中的,而一個char佔用2個字節,因此abcdefg的char[]數組大小爲對象頭16Byte+實例數據14Byte+對齊填充2Byte=32Byte。那麼abcdefg佔用內存大小就是String對象+char[]數組對象=56Byte

用List存儲對象
那麼咱們在內存中放入二千萬個這個對象的話,須要佔用多少內存呢?根據上面的知識咱們能大概估算一下。咱們定義一個List數組用於存放此對象,不讓其回收。

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

注意這裏我是直接將集合的大小初始化爲了二千萬的大小,因此程序在正常啓動的時候佔用內存是100+MB,正常程序啓動僅僅佔用30+MB的,因此多出來的60+MB正好是咱們初始化的數組的大小。至於爲何要初始化大小的緣由就是爲了消除集合在擴容時對咱們觀察結果的影響

這裏我貼一張,集合未初始化大小和初始化大小內存佔用對比圖,你們能夠看到是有內存上的差別,在ArrayList數組中用於存放數據的是transient Object[] elementData;Object數組,因此它裏面存放的是指向對象的指針,一個指針佔用4個字節,因此就有兩千萬個指針,那麼就是76M。咱們能夠看到差別圖和咱們預想的同樣。
幾百萬數據放入內存不會把系統撐爆嗎?

上面咱們已經算出來了一個Animal對象佔用16個字節,因此兩千萬個佔用大概是305MB,和集合加起來就是將近380MB的空間大小,接下來咱們就啓動程序來看一下咱們結果是否是對的呢,接下來我用的jconsole工具查看內存佔用狀況。
幾百萬數據放入內存不會把系統撐爆嗎?

咱們能夠看到和咱們預算的結果是相吻合的。

那麼之後若是有大量的對象須要從數據庫中查找出來放入內存的話,那麼若是是使用對象來接的話,那麼咱們就應該儘可能減小對象中的字段,由於即便你不賦值,其實他也是佔用着內存的,咱們接下來再舉個例子看一下對個屬性值的話佔用內存是否是又高了。咱們將Animal對象改造以下

class Animal{

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

}

此時咱們可以計算獲得一個Animal對象佔用的內存大小是(對象頭12Byte+實例數據20Byte=32Byte)此時32因爲是8的倍數因此無需進行填充補齊。那麼此時若是仍是二千萬條數據的話,此對象佔用內存應該是610MB,加上剛纔集合中指針的數據76MB,那麼加起來將近佔用686MB,那麼預期結果是否和咱們的同樣呢,咱們從新啓動程序觀察,能夠看到下圖。能夠看到和咱們分析的數據是差很少的。
幾百萬數據放入內存不會把系統撐爆嗎?

用Map存儲對象
用Map存儲對象計算內存大小有些麻煩了,衆所周知Map的結構是以下圖所示。
幾百萬數據放入內存不會把系統撐爆嗎?

它是一個數組加鏈表(或者紅黑樹)的結構,而數組中存放的數據是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對象須要的內存大小對象頭12Byte+實例數據16Byte+對其填充4Byte=32Byte,固然這裏的key和value的值還須要另算,由於Node對象此時存放的僅僅是他們的引用而已。一個Animal對象所佔用內存大小咱們上面也說了是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對象中存放二千萬條數據,計算大概在內存中佔用多少內存。

數組佔用內存大小:咱們先來計算一下數組佔了多少,這裏有個小知識點,在HashMap中初始化大小是按照2的倍數來的,好比你定義了大小爲60,那麼系統會給你初始化大小爲64。因此咱們定義爲二千萬,系統實際上是會給咱們初始化爲33554432,因此此時僅僅HashMap中數組就佔用了將近132MB
數據佔用內存大小:咱們上面計算了一個Node節點佔用了64Byte,那麼兩千萬條數據就佔用了1280MB
兩個佔用內存大小相加咱們能夠知道大概系統中佔用了1.4G內存的大小。那麼事實是不是咱們想象的呢?咱們運行程序能夠看到內存大小如圖所示。能夠看到結果確實和咱們猜測的同樣。
幾百萬數據放入內存不會把系統撐爆嗎?

總結迴歸到上面所說的需求,幾百萬數據放到內存中會把內存撐爆嗎?這時候你能夠經過本身的計算獲得。最終咱們那個需求通過我算出來其實佔用內存量幾百兆,對於4個G的堆內存來講其實遠遠還沒達到撐爆的地步。因此有時候咱們對任何東西都要存在懷疑的態度。你們能夠到GitHub中下載代碼本身在本地跑一下監測一下,而且能夠本身定義幾個對象而後計算看是否是和圖中的內存大小一致。這樣才能記憶更深入。送給你們一句話歷來如此,便對嗎?。其實我寫的文章裏面也留了一個小坑,你們能夠試着找找,是在對集合進行初始化計算那一塊。

相關文章
相關標籤/搜索