在公司有一個(gè)需求是要核對(duì)一批數(shù)據(jù)石蔗,之前的做法是直接用SQL各種復(fù)雜操作給懟出來(lái)的喧锦,不僅時(shí)間慢,而且后期也不好維護(hù)抓督,就算原作者來(lái)了過(guò)一個(gè)月估計(jì)也忘了SQL什么意思了燃少,于是有一次我就想著問(wèn)一下之前做這個(gè)需求的人為什么不將這些數(shù)據(jù)查出來(lái)后在內(nèi)存里面做篩選呢?直接說(shuō)了你不怕把內(nèi)存給撐爆嗎铃在?此核算服務(wù)器是單獨(dú)的服務(wù)器阵具,配置是四核八G的碍遍,配置堆的大小是4G。本著懷疑的精神阳液,就想要弄清楚幾百萬(wàn)條數(shù)據(jù)真的放入內(nèi)存的話會(huì)占用多少內(nèi)存呢怕敬?
計(jì)算機(jī)的存儲(chǔ)單位
計(jì)算機(jī)的存儲(chǔ)單位常用的有bit
、Byte
帘皿、KB
东跪、MB
、GB
鹰溜、TB
后面還有但是我們基本上用不上就不說(shuō)了虽填,我們經(jīng)常將bit
稱之為比特或者位、將Byte
簡(jiǎn)稱為B
或者字節(jié)曹动,將KB
簡(jiǎn)稱為K
斋日,將MB
稱之為M或者兆,將GB
簡(jiǎn)稱為G
墓陈。那么他們的換算單位是怎樣的呢恶守?
換算關(guān)系
首先我們得知道在計(jì)算機(jī)中所有數(shù)據(jù)都是由0 1
來(lái)組成的,那么存儲(chǔ)0 1
這些二進(jìn)制數(shù)據(jù)是由什么存放呢贡必?就是由bit
存放的兔港,一個(gè)bit
存放一位二進(jìn)制數(shù)字。所以bit
是計(jì)算機(jī)最小的單位仔拟。
大部分計(jì)算機(jī)目前都是使用8位的塊押框,就是我們上面稱之為的字節(jié)Byte
,來(lái)作為計(jì)算機(jī)容量的基本單位理逊。所以我們一般稱一個(gè)字符或者一個(gè)數(shù)字都是稱之為占用了多少字節(jié)橡伞。
了解了上面關(guān)于位和字節(jié)的關(guān)系后,我們可以看一下其他的單位換算關(guān)系
1B(Byte 字節(jié)) = 8bit(位)
1KB = 1024B
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
Java中對(duì)象占用多少內(nèi)存
在了解了上面的換算關(guān)系后晋被,我們來(lái)了解一下新建一個(gè)Java對(duì)象需要多少內(nèi)存兑徘。
Java基本類型
我們知道Java類型分為基本類型和引用類型,八大基本類型有int羡洛、short挂脑、long、byte欲侮、float崭闲、double、boolean威蕉、char
數(shù)據(jù)類型 | 占用內(nèi)存(單位為Byte) |
---|---|
boolean | 1 |
byte | 1 |
short | 2 |
char | 2 |
int | 4 |
float | 4 |
long | 8 |
double | 8 |
至于為什么Java中的char無(wú)論是中英文數(shù)字都占用兩個(gè)字節(jié)刁俭,是因?yàn)镴ava中使用Unicode字符,所有的字符均以兩個(gè)字節(jié)存儲(chǔ)韧涨。
Java引用類型
在一個(gè)對(duì)象中除了有基本數(shù)據(jù)類型以外牍戚,我們也會(huì)有一些引用類型侮繁,引用類型的對(duì)象比較特殊,因?yàn)檫@些對(duì)象真正存儲(chǔ)在虛擬機(jī)中的堆內(nèi)存中如孝,對(duì)象中只是存儲(chǔ)了一個(gè)引用而已宪哩,如果是引用類型那么就會(huì)存儲(chǔ)一個(gè)指向該引用的指針。指針默認(rèn)情況下是占用4字節(jié)第晰,是因?yàn)殚_(kāi)啟了指針壓縮锁孟,如果沒(méi)有開(kāi)的話,那么一個(gè)引用就占用8個(gè)字節(jié)茁瘦。
對(duì)象在內(nèi)存中的布局
在HotSpot虛擬機(jī)中品抽,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為三個(gè)區(qū)域:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)腹躁、對(duì)齊填充(Padding)。
對(duì)象頭
在對(duì)象頭中存儲(chǔ)了兩部分?jǐn)?shù)據(jù)
- 運(yùn)行時(shí)數(shù)據(jù):存儲(chǔ)了對(duì)象自身運(yùn)行時(shí)的數(shù)據(jù)南蓬,例如哈希碼纺非、GC分代的年齡、鎖狀態(tài)標(biāo)志赘方、線程持有的鎖烧颖、偏向線程ID等等。這部分?jǐn)?shù)據(jù)在32位和64位的虛擬機(jī)中分別為32bit和64bit
- 類型指針:對(duì)象指向它的類元數(shù)據(jù)的指針窄陡,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例炕淮。如果對(duì)象是一個(gè)Java數(shù)組的話,那么對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù)(占用4個(gè)字節(jié))跳夭。所以這是一個(gè)指針涂圆,默認(rèn)JVM對(duì)指針進(jìn)行了壓縮,用4個(gè)字節(jié)存儲(chǔ)币叹。
我們以虛擬機(jī)為64位的機(jī)器為例润歉,那么對(duì)象頭占用的內(nèi)存是8(運(yùn)行時(shí)數(shù)據(jù))+4(類型指針)=12Byte。如果是數(shù)組的話那么就是16Byte
實(shí)例數(shù)據(jù)
實(shí)例數(shù)據(jù)中也擁有兩部分?jǐn)?shù)據(jù)颈抚,一部分是基本類型數(shù)據(jù)踩衩,一部分是引用指針。這兩部分?jǐn)?shù)據(jù)我們?cè)谏厦嬉呀?jīng)講了贩汉。具體占用多少內(nèi)存我們需要結(jié)合具體的對(duì)象繼續(xù)分析驱富,下面我們會(huì)有具體的分析。
從父類中繼承下來(lái)的變量也是需要進(jìn)行計(jì)算的
對(duì)齊填充
對(duì)齊填充并不是必然存在的匹舞,也沒(méi)有特別的含義褐鸥。它僅僅起著占位符的作用。由于HotSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍赐稽,換句話說(shuō)就是對(duì)象的大小必須是8字節(jié)的整數(shù)倍晶疼。而如果對(duì)象頭加上實(shí)例數(shù)據(jù)不是8的整數(shù)倍的話那么就會(huì)通過(guò)對(duì)其填充進(jìn)行補(bǔ)全酒贬。
實(shí)戰(zhàn)演練
我們?cè)谏厦娣治鲆淮蠖眩敲词遣皇蔷腿缥覀兎治龅囊粯哟浠簦陆ㄒ粋€(gè)對(duì)象在內(nèi)存中的分配大小就是如此呢锭吨?我們可以新建一個(gè)對(duì)象。
class Animal{
private int age;
}
那么怎么知道這個(gè)對(duì)象在內(nèi)存中占用多少內(nèi)存呢寒匙?JDK提供了一個(gè)工具jol-core
可以給我們分析出來(lái)一個(gè)對(duì)象在內(nèi)存中占用的內(nèi)存大小零如。直接在項(xiàng)目中引入包即可。
--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>
然后我們?cè)趍ain函數(shù)中調(diào)用如下
public class AboutObjectMemory {
public static void main(String[] args) {
System.out.print(ClassLayout.parseClass(Animal.class).toPrintable());
}
}
就可以查看到輸出的內(nèi)容了锄弱,可以看到輸出結(jié)果占用的內(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中是個(gè)特殊的存在会宪,比如一個(gè)字符串"abcdefg"
這樣一個(gè)字符串占用多少字節(jié)呢肖卧?相信會(huì)有人回答說(shuō)是7個(gè)字節(jié)或者是14個(gè)字節(jié),這兩個(gè)答案都是不準(zhǔn)確的掸鹅,我們先看一下String類在內(nèi)存中占用的內(nèi)存是多少塞帐。
我們先自己進(jìn)行分析一下。在String類中有兩個(gè)屬性巍沙,其中對(duì)象頭固定了是12字節(jié)葵姥,int是4字節(jié),char[]數(shù)組其實(shí)在這里相當(dāng)于引用對(duì)象存的句携,所以存的是地址榔幸,因此占用4個(gè)字節(jié),所以大小為對(duì)象頭12Byte+實(shí)例數(shù)據(jù)8Byte+填充數(shù)據(jù)4Byte=24Byte
這里的對(duì)象頭和實(shí)例數(shù)據(jù)加起來(lái)不是8的倍數(shù)矮嫉,所以需要填充數(shù)據(jù)進(jìn)行填充削咆。
private final char value[];
private int hash; // Default to 0
那么我們分析的到底對(duì)不對(duì)呢,我們還是用上面的工具進(jìn)行分析一下蠢笋√粒可以看到我們算出的結(jié)果和我們分析的結(jié)果是一致的。
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
那么一個(gè)空字符串占用多少內(nèi)存呢挺尿?我們剛才得到的是一個(gè)String對(duì)象占用了24字節(jié)奏黑,其實(shí)char[]數(shù)組還是會(huì)占用內(nèi)存的,我們?cè)谏厦嬷v對(duì)象頭的時(shí)候說(shuō)過(guò)编矾,數(shù)組對(duì)象也是一個(gè)實(shí)例對(duì)象熟史,它的對(duì)象頭比一般的對(duì)象多出來(lái)4字節(jié),用來(lái)描述此數(shù)組的長(zhǎng)度窄俏,所以char[]數(shù)組的對(duì)象頭長(zhǎng)度為16字節(jié)蹂匹,由于此時(shí)是空字符串,所以實(shí)例數(shù)據(jù)長(zhǎng)度為0凹蜈。因此一個(gè)空char[]數(shù)組占用內(nèi)存大小為對(duì)象頭16Byte+實(shí)例數(shù)據(jù)0Byte=16Byte
限寞。一個(gè)空字符串占用內(nèi)存為String對(duì)象+char[]數(shù)組對(duì)象=40Byte
那么我們上面舉的例子abcdefg
占用多少內(nèi)存呢忍啸?其中String對(duì)象占用的內(nèi)存是不會(huì)變了,變化的是char[]數(shù)組中的內(nèi)容履植,這里我們需要知道字符串是存放于char[]數(shù)組中的计雌,而一個(gè)char占用2個(gè)字節(jié),所以abcdefg
的char[]數(shù)組大小為對(duì)象頭16Byte+實(shí)例數(shù)據(jù)14Byte+對(duì)齊填充2Byte=32Byte
玫霎。那么abcdefg
占用內(nèi)存大小就是String對(duì)象+char[]數(shù)組對(duì)象=56Byte
用List存儲(chǔ)對(duì)象
那么我們?cè)趦?nèi)存中放入二千萬(wàn)個(gè)這個(gè)對(duì)象的話凿滤,需要占用多少內(nèi)存呢?根據(jù)上面的知識(shí)我們能大概估算一下庶近。我們定義一個(gè)List數(shù)組用于存放此對(duì)象翁脆,不讓其回收。
List<Animal> animals = new ArrayList<>(20000000);
for (int i = 0; i < 20000000; i++) {
Animal animal = new Animal();
animals.add(animal);
}
注意這里我是直接將集合的大小初始化為了二千萬(wàn)的大小鼻种,所以程序在正常啟動(dòng)的時(shí)候占用內(nèi)存是100+MB反番,正常程序啟動(dòng)僅僅占用30+MB的,所以多出來(lái)的60+MB正好是我們初始化的數(shù)組的大小叉钥。至于為什么要初始化大小的原因就是為了消除集合在擴(kuò)容時(shí)對(duì)我們觀察結(jié)果的影響
這里我貼一張罢缸,集合未初始化大小和初始化大小內(nèi)存占用對(duì)比圖,大家可以看到是有內(nèi)存上的差異沼侣,在ArrayList數(shù)組中用于存放數(shù)據(jù)的是transient Object[] elementData;
Object數(shù)組祖能,所以它里面存放的是指向?qū)ο蟮闹羔樓革粋€(gè)指針占用4個(gè)字節(jié)蛾洛,所以就有兩千萬(wàn)個(gè)指針,那么就是76M雁芙。我們可以看到差異圖和我們預(yù)想的一樣轧膘。
上面我們已經(jīng)算出來(lái)了一個(gè)Animal
對(duì)象占用16個(gè)字節(jié),所以兩千萬(wàn)個(gè)占用大概是305MB兔甘,和集合加起來(lái)就是將近380MB的空間大小谎碍,接下來(lái)我們就啟動(dòng)程序來(lái)看一下我們結(jié)果是不是對(duì)的呢,接下來(lái)我用的jconsole
工具查看內(nèi)存占用情況洞焙。
我們可以看到和我們預(yù)算的結(jié)果是相吻合的蟆淀。
那么以后如果有大量的對(duì)象需要從數(shù)據(jù)庫(kù)中查找出來(lái)放入內(nèi)存的話,那么如果是使用對(duì)象來(lái)接的話澡匪,那么我們就應(yīng)該盡量減少對(duì)象中的字段熔任,因?yàn)榧词鼓悴毁x值,其實(shí)他也是占用著內(nèi)存的唁情,我們接下來(lái)再舉個(gè)例子看一下對(duì)個(gè)屬性值的話占用內(nèi)存是不是又高了疑苔。我們將Animal
對(duì)象改造如下
class Animal{
private int age;
private int age1;
private int age2;
private int age3;
private int age4;
}
此時(shí)我們能夠計(jì)算得到一個(gè)Animal
對(duì)象占用的內(nèi)存大小是(對(duì)象頭12Byte+實(shí)例數(shù)據(jù)20Byte=32Byte)此時(shí)32由于是8的倍數(shù)所以無(wú)需進(jìn)行填充補(bǔ)齊。那么此時(shí)如果還是二千萬(wàn)條數(shù)據(jù)的話甸鸟,此對(duì)象占用內(nèi)存應(yīng)該是610MB惦费,加上剛才集合中指針的數(shù)據(jù)76MB兵迅,那么加起來(lái)將近占用686MB,那么預(yù)期結(jié)果是否和我們的一樣呢薪贫,我們重新啟動(dòng)程序觀察恍箭,可以看到下圖『罄祝可以看到和我們分析的數(shù)據(jù)是差不多的季惯。
用Map存儲(chǔ)對(duì)象
用Map存儲(chǔ)對(duì)象計(jì)算內(nèi)存大小有些麻煩了,眾所周知Map的結(jié)構(gòu)是如下圖所示臀突。
它是一個(gè)數(shù)組加鏈表(或者紅黑樹(shù))的結(jié)構(gòu)勉抓,而數(shù)組中存放的數(shù)據(jù)是Node對(duì)象。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
我們舉例定義下面一個(gè)Map對(duì)象
Map<Animal,Animal> map
此時(shí)我們可以自己計(jì)算一下一個(gè)Node對(duì)象需要的內(nèi)存大小對(duì)象頭12Byte+實(shí)例數(shù)據(jù)16Byte+對(duì)其填充4Byte=32Byte
候学,當(dāng)然這里的key和value的值還需要另算藕筋,因?yàn)镹ode對(duì)象此時(shí)存放的僅僅是他們的引用而已。一個(gè)Animal對(duì)象所占用內(nèi)存大小我們上面也說(shuō)了是16Byte梳码,所以這里一個(gè)Node對(duì)象占用的大小為32Byte+16Byte+16Byte=64Byte隐圾。
下面我們用實(shí)際例子來(lái)驗(yàn)證下我們的猜想
Map<Animal,Animal> map = new HashMap<>(20000000);
for (int i = 0; i < 20000000; i++) {
map.put(new Animal(),new Animal());
}
上面的例子在一個(gè)Map對(duì)象中存放二千萬(wàn)條數(shù)據(jù),計(jì)算大概在內(nèi)存中占用多少內(nèi)存掰茶。
- 數(shù)組占用內(nèi)存大邢静亍:我們先來(lái)計(jì)算一下數(shù)組占了多少,這里有個(gè)小知識(shí)點(diǎn)濒蒋,在HashMap中初始化大小是按照2的倍數(shù)來(lái)的盐碱,比如你定義了大小為60,那么系統(tǒng)會(huì)給你初始化大小為64沪伙。所以我們定義為二千萬(wàn)瓮顽,系統(tǒng)其實(shí)是會(huì)給我們初始化為33554432,所以此時(shí)僅僅HashMap中數(shù)組就占用了將近132MB
- 數(shù)據(jù)占用內(nèi)存大形稹:我們上面計(jì)算了一個(gè)Node節(jié)點(diǎn)占用了64Byte暖混,那么兩千萬(wàn)條數(shù)據(jù)就占用了1280MB
兩個(gè)占用內(nèi)存大小相加我們可以知道大概系統(tǒng)中占用了1.4G內(nèi)存的大小。那么事實(shí)是否是我們想象的呢翁授?我們運(yùn)行程序可以看到內(nèi)存大小如圖所示拣播。可以看到結(jié)果確實(shí)和我們猜想的一樣收擦。
總結(jié)
回歸到上面所說(shuō)的需求贮配,幾百萬(wàn)數(shù)據(jù)放到內(nèi)存中會(huì)把內(nèi)存撐爆嗎?這時(shí)候你可以通過(guò)自己的計(jì)算得到炬守。最終我們那個(gè)需求經(jīng)過(guò)我算出來(lái)其實(shí)占用內(nèi)存量幾百兆牧嫉,對(duì)于4個(gè)G的堆內(nèi)存來(lái)說(shuō)其實(shí)遠(yuǎn)遠(yuǎn)還沒(méi)達(dá)到撐爆的地步。所以有時(shí)候我們對(duì)任何東西都要存在懷疑的態(tài)度。大家可以到GitHub中下載代碼自己在本地跑一下監(jiān)測(cè)一下酣藻,并且可以自己定義幾個(gè)對(duì)象然后計(jì)算看是不是和圖中的內(nèi)存大小一致曹洽。這樣才能記憶更深刻。送給大家一句話從來(lái)如此辽剧,便對(duì)嗎送淆?。其實(shí)我寫的文章里面也留了一個(gè)小坑怕轿,大家可以試著找找偷崩,是在對(duì)集合進(jìn)行初始化計(jì)算那一塊。
項(xiàng)目源碼地址
有感興趣的可以關(guān)注一下我新建的公眾號(hào)撞羽,搜索[程序猿的百寶袋]阐斜。或者直接掃下面的碼也行诀紊。