您好,登錄后才能下訂單哦!
在公司有一個需求是要核對一批數據,之前的做法是直接用SQL各種復雜操作給懟出來的,不僅時間慢,而且后期也不好維護,就算原作者來了過一個月估計也忘了SQL什么意思了,于是有一次我就想著問一下之前做這個需求的人為什么不將這些數據查出來后在內存里面做篩選呢?直接說了你不怕把內存給撐爆嗎?此核算服務器是單獨的服務器,配置是四核八G的,配置堆的大小是4G。本著懷疑的精神,就想要弄清楚幾百萬條數據真的放入內存的話會占用多少內存呢?
計算機的存儲單位
計算機的存儲單位常用的有bit、Byte、KB、MB、GB、TB后面還有但是我們基本上用不上就不說了,我們經常將bit稱之為比特或者位、將Byte簡稱為B或者字節,將KB簡稱為K,將MB稱之為M或者兆,將GB簡稱為G。那么他們的換算單位是怎樣的呢?
換算關系
首先我們得知道在計算機中所有數據都是由0 1來組成的,那么存儲0 1這些二進制數據是由什么存放呢?就是由bit存放的,一個bit存放一位二進制數字。所以bit是計算機最小的單位。
大部分計算機目前都是使用8位的塊,就是我們上面稱之為的字節Byte,來作為計算機容量的基本單位。所以我們一般稱一個字符或者一個數字都是稱之為占用了多少字節。
了解了上面關于位和字節的關系后,我們可以看一下其他的單位換算關系
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)。
對象頭
在對象頭中存儲了兩部分數據
運行時數據:存儲了對象自身運行時的數據,例如哈希碼、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中下載代碼自己在本地跑一下監測一下,并且可以自己定義幾個對象然后計算看是不是和圖中的內存大小一致。這樣才能記憶更深刻。送給大家一句話從來如此,便對嗎?。其實我寫的文章里面也留了一個小坑,大家可以試著找找,是在對集合進行初始化計算那一塊。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。