前言
不知道大家有沒有這樣一種感覺术陶,程序員的數(shù)量井噴了∶汉郏可能是因?yàn)榛ヂ?lián)網(wǎng)火了梧宫,也可能是各家培訓(xùn)機(jī)構(gòu)為我們拉來了大量的同行,更有可能是各行各業(yè)都不景氣了摆碉,而互聯(lián)網(wǎng)(在這前一段時(shí)間中)真是紅遍了大江南北塘匣,燒紅了整片天,所以到處都是我們的同行巷帝,所以競爭更加激烈了忌卤,那么我們就更得穩(wěn)扎穩(wěn)打,不能再過那種:“我會(huì)用就好了”的日子了楞泼,我們必須刨根問底驰徊,明白我們賴以生存的編程語言到底是怎么工作的。
1. Java數(shù)組與內(nèi)存控制
一堕阔、Java數(shù)組初始化
Java數(shù)組是靜態(tài)的棍厂,即當(dāng)數(shù)組被初始化之后,該數(shù)組的長度是不可變的超陆。Java數(shù)組使用之前必須先對數(shù)組對象進(jìn)行初始化牺弹,所謂初始化,就是為數(shù)組的所有元素分配內(nèi)存空間时呀,并為每個(gè)數(shù)組元素指定初始值张漂。
Java基本類型數(shù)組的兩種初始化方式
- 靜態(tài)初始化:初始化時(shí)由程序員顯式指定每個(gè)數(shù)組元素的初始值,由系統(tǒng)決定數(shù)組長度谨娜。
- 動(dòng)態(tài)初始化:初始化時(shí)程序員只指定數(shù)組長度航攒,由系統(tǒng)為數(shù)組元素分配初始值。
不要同時(shí)使用靜態(tài)初始化和動(dòng)態(tài)初始化趴梢,也就是說漠畜,不要在進(jìn)行數(shù)組初始化時(shí),既指定數(shù)組的長度垢油,也為每個(gè)數(shù)組元素分配初始值盆驹。
Java的數(shù)組是靜態(tài)的,一旦數(shù)組初始化完成滩愁,數(shù)組元素的內(nèi)存空間分配即結(jié)束躯喇,程序只能改變數(shù)組元素的值,而無法改變數(shù)組的長度。Java的數(shù)組變量是一種引用類型的變量廉丽,數(shù)組變量并不是數(shù)組本身倦微,它只是指向堆內(nèi)存中的數(shù)組對象。因此正压,可以改變一個(gè)數(shù)組變量所引用的數(shù)組欣福,這樣可以造成數(shù)組長度可變的假象。
二:Java數(shù)組內(nèi)存存放
對于基本類型數(shù)組而言焦履,數(shù)組元素的值直接存儲在對應(yīng)的數(shù)組元素中拓劝,因此基本類型數(shù)組的初始化比較簡單:程序直接先為數(shù)組分配內(nèi)存空間,再將數(shù)組元素的值存入對應(yīng)內(nèi)存里嘉裤。
所有局部變量都是存放在棧內(nèi)存里保存的郑临,不管其是基本類型的變量,還是引用類型的變量屑宠,都是存儲在各自的方法棧區(qū)中厢洞;但引用類型變量所引用的對象(數(shù)組、普通Java對象)則總是存儲在堆內(nèi)存中典奉。
對于很多Java程序員而言躺翻,他們最容易混淆的是:引用類型變量何時(shí)只是棧內(nèi)存中的變量本身,何時(shí)又變?yōu)橐脤?shí)際的Java對象卫玖。其實(shí)規(guī)則很簡單:引用變量本質(zhì)上只是一個(gè)指針公你,只要程序通過引用變量訪問屬性,或者通過調(diào)用引用變量來調(diào)用方法骇笔,該引用變量將會(huì)由它所引用的對象代替省店。
三:使用數(shù)組
當(dāng)數(shù)組引用變量指向一個(gè)有效的數(shù)組對象之后嚣崭,程序就可以通過該數(shù)組引用變量來訪問數(shù)組對象笨触。Java語言不允許直接訪問堆內(nèi)存中的數(shù)據(jù),因此無法直接訪問堆內(nèi)存中的數(shù)組對象雹舀,程序?qū)⑼ㄟ^數(shù)組引用變量來訪問數(shù)組芦劣。
2.Java內(nèi)存回收機(jī)制
一、Java對象在內(nèi)存引用狀態(tài)
內(nèi)存泄露:程序運(yùn)行過程中说榆,會(huì)不斷分配內(nèi)存空間虚吟,那些不再使用的內(nèi)存空間應(yīng)該即時(shí)回收它們,從而保證系統(tǒng)可以再次使用這些內(nèi)存签财,如果存在無用的內(nèi)存沒有被回收回來串慰,這就是內(nèi)存泄漏。
- (1)強(qiáng)引用
這是java程序中最常見的引用方式唱蒸,程序創(chuàng)建一個(gè)對象邦鲫,并把這個(gè)對象賦給一個(gè)引用變量,這個(gè)引用變量就是強(qiáng)引用.java程序可通過強(qiáng)引用來訪問實(shí)際的對象。當(dāng)一個(gè)對象被一個(gè)或一個(gè)以上的強(qiáng)引用變量引用時(shí)庆捺,它處于可達(dá)狀態(tài)古今,它不可能被系統(tǒng)垃圾回收機(jī)制回收。
強(qiáng)引用是Java編程中廣泛使用的引用類型滔以,被強(qiáng)引用所引用的Java對象絕不會(huì)被垃圾回收機(jī)制回收捉腥,即使系統(tǒng)內(nèi)存緊張;即使有些Java對象以后永遠(yuǎn)也不會(huì)被用到,JVM也不會(huì)回收被強(qiáng)引用所引用的Java對象.
由于JVM肯定不會(huì)回收強(qiáng)引用所引用的JAVA對象你画,因此強(qiáng)引用是造成JAVA內(nèi)存泄漏的主要原因抵碟。
如 **ReceiptBean rb=new ReceiptBean(); **rb就代表了一種強(qiáng)引用的方式。 - (2)軟引用
軟引用需要通過SoftReference類來實(shí)現(xiàn)坏匪,當(dāng)一個(gè)對象只具有軟引用時(shí)立磁,它可能被垃圾回收機(jī)制回收。對于只有軟引用的對象而言剥槐,當(dāng)系統(tǒng)內(nèi)存空間足夠時(shí)唱歧,它不會(huì)被系統(tǒng)回收,程序也可以使用該對象;當(dāng)系統(tǒng)內(nèi)存空間不足時(shí)粒竖,系統(tǒng)將回收它.
軟引用通常用在對內(nèi)存敏感的程序中颅崩,軟引用是強(qiáng)引用很好的替代。對于軟引用蕊苗,當(dāng)系統(tǒng)內(nèi)存空間充足時(shí)沿后,軟引用與強(qiáng)引用沒有太大的區(qū)別,當(dāng)系統(tǒng)內(nèi)存空間不足時(shí)朽砰,被軟引用所引用的JAVA對象可以被垃圾回收機(jī)制回收尖滚,從而避免系統(tǒng)內(nèi)存不足的異常.
當(dāng)程序需要大量創(chuàng)建某個(gè)類的新對象,而且有可能重新訪問已創(chuàng)建老對象時(shí)瞧柔,可以充分使用軟引用來解決內(nèi)存緊張的問題漆弄。
例如需要訪問1000個(gè)Person對象,可以有兩種方式
方法一 依次創(chuàng)建1000個(gè)對象造锅,但只有一個(gè)Person引用指向最后一個(gè)Person對象撼唾。
方法二 定義一個(gè)長度為1000個(gè)的Person數(shù)組,每個(gè)數(shù)組元素引用一個(gè)Person對象哥蔚。
對于方法一倒谷,弱點(diǎn)很明顯,程序不允許需要重新訪問前面創(chuàng)建的Person對象糙箍,即使這個(gè)對象所占的空間還沒有被回收渤愁。但已經(jīng)失去了這個(gè)對象的引用,因此也不得不重新創(chuàng)建一個(gè)新的Person對象(重新分配內(nèi)存)深夯,而那個(gè)已有的Person對象(完整的抖格,正確的,可用的)則只能等待垃圾回收。
對于方法二,優(yōu)勢是可以隨時(shí)重新訪問前面創(chuàng)建的每個(gè)Person對象他挎,但弱點(diǎn)也有筝尾,如果系統(tǒng)堆內(nèi)存空間緊張,而1000個(gè)Person對象都被強(qiáng)引用引著办桨,垃圾回收機(jī)制也不可能回收它們的堆內(nèi)存空間筹淫,系統(tǒng)性能將變成非常差,甚至因此內(nèi)存不足導(dǎo)致程序中止呢撞。
如果用軟引用則是一種較好的方案损姜,當(dāng)堆內(nèi)存空間足夠時(shí),垃圾回收機(jī)制不會(huì)回收Person對象殊霞,可以隨時(shí)重新訪問一個(gè)已有的Person對象摧阅,這和普通的強(qiáng)引用沒有任何區(qū)別。但當(dāng)heap堆內(nèi)存空間不足時(shí)绷蹲,系統(tǒng)也可以回收軟引用引用的Person對象棒卷,從而提高程序運(yùn)行性能,避免垃圾回收祝钢。
當(dāng)程序使用強(qiáng)引用時(shí)比规,無論系統(tǒng)堆內(nèi)存如何緊張,JVM垃圾回收機(jī)制都不會(huì)回收被強(qiáng)引用所引用的Java對象拦英,因此最后導(dǎo)致程序因內(nèi)存不足而中止蜒什。但如果把強(qiáng)引用改為軟引用,就完成可以避免這種情況疤估,這就是軟引用的優(yōu)勢所在灾常。 - (3)弱引用
弱引用與軟引用有點(diǎn)相似,區(qū)別在于弱引用所引用對象的生存期更短铃拇。弱引用通過WeakReference類實(shí)現(xiàn)钞瀑,弱引用和軟引用很像,但弱引用的引用級別更低锚贱。對于只有弱引用的對象而言仔戈,當(dāng)系統(tǒng)垃圾回收機(jī)制運(yùn)行時(shí),不管系統(tǒng)內(nèi)存是否足夠拧廊,總會(huì)回收該對象所占用的內(nèi)存。當(dāng)然晋修,并不是說當(dāng)一個(gè)對象只有弱引用時(shí)吧碾,它就會(huì)立即被回收,正如那些失去引用的對象一樣墓卦,必須等到系統(tǒng)垃圾回收機(jī)制運(yùn)行時(shí)才會(huì)被回收.
總結(jié)說明:
1.弱引用具有很大的不確定性倦春,因?yàn)槊看卫厥諜C(jī)制執(zhí)行時(shí)都會(huì)回收弱引用所引用的對象,而垃圾回收機(jī)制的運(yùn)行又不受程序員的控制,因此程序獲取弱引用所引用的java對象時(shí)必須小心空指針異常睁本,通過弱引用所獲取的java對象可能是null
2.由于垃圾回收的不確定性尿庐,當(dāng)程序希望從弱引用中取出被引用對象時(shí),可能這個(gè)被引用對象已經(jīng)被釋放了呢堰。如果程序需要使用被引用的對象抄瑟,則必須重新創(chuàng)建該對象。 - (4)虛引用
軟引用和弱引用可以單獨(dú)使用枉疼,但虛引用不能單獨(dú)使用皮假,單獨(dú)使用虛引用沒有太大的意義。虛引用的主要作用就是跟蹤對象被垃圾回收的狀態(tài)骂维,程序可以通過檢查虛引用關(guān)聯(lián)的引用隊(duì)列中是否包含指定的虛引用惹资,從而了解虛引用所引用的對象是否將被回收.
引用隊(duì)列由java.lang.ref.ReferenceQueue類表示,它用于保存被回收對象的引用航闺。當(dāng)把軟引用褪测,弱引用和引用隊(duì)列聯(lián)合使用時(shí),系統(tǒng)回收被引用的對象之后潦刃,將會(huì)把被回收對象對應(yīng)的引用添加到關(guān)聯(lián)的引用隊(duì)列中汰扭。與軟引用和弱引用不同的是,虛引用在對象被釋放之前福铅,將把它對應(yīng)的虛引用添加到關(guān)聯(lián)的隊(duì)列中萝毛,這使得可以在對象被回收之前采取行動(dòng)。
虛引用通過PhantomReference類實(shí)現(xiàn)滑黔,它完全類似于沒有引用笆包。虛引用對對象本身沒有大的影響,對象甚至感覺不到虛引用的存在略荡。如果一個(gè)對象只有一個(gè)虛引用庵佣,那它和沒有引用的效果大致相同。虛引用主要用于跟蹤對象被垃圾回收的狀態(tài)汛兜,虛引用不能單獨(dú)使用巴粪,虛引用必須和隊(duì)列ReferenceQueue聯(lián)合使用.
二、Java內(nèi)存泄露
對于c++程序來說粥谬,對象占用的內(nèi)存空間都必須由程序顯式回收肛根,如果程序員忘記了回收它們,那它們所占用的內(nèi)存空間就會(huì)產(chǎn)生內(nèi)存泄漏;對于java程序來說漏策,所有不可達(dá)的對象都由垃圾回收機(jī)制負(fù)責(zé)回收派哲,因此程序員不需要考慮這部分的內(nèi)存泄漏。但如果程序中有一些java對象掺喻,它們處于可達(dá)狀態(tài)芭届,但程序以后永遠(yuǎn)都不會(huì)再訪問它們储矩,那它們所占用的空間也不會(huì)被回收,它們所占用的空間也會(huì)產(chǎn)生內(nèi)存泄漏褂乍。
三持隧、內(nèi)存管理的小技巧
盡可能多的掌握J(rèn)ava的內(nèi)存回收,垃圾回收機(jī)制是為了更好地管理JVM的內(nèi)存逃片,這樣才能提高java程序的運(yùn)行性能屡拨。根據(jù)前面介紹的內(nèi)存機(jī)制,下面給出java內(nèi)存管理的幾個(gè)小技巧题诵。
- (1)盡量使用直接量
當(dāng)需要使用字符串洁仗,還有Byte,Short,Integer,Long,Float,Double,Boolean,Charater包裝類的實(shí)例時(shí),程序不應(yīng)該采用new的方式來創(chuàng)建對象性锭,而應(yīng)該直接采用直接量來創(chuàng)建它們赠潦。
例如,程序需要"hello"字符串草冈,應(yīng)該采用如下代碼
String str="hello"'
上面這種方式創(chuàng)建一個(gè)"hello"字符串她奥,而且JVM的字符串緩存池還會(huì)緩存這個(gè)字符串。但如果程序采用
String str=new String("hello");
此時(shí)程序同樣創(chuàng)建了一個(gè)緩存在字符串緩存池中的"hello"字符串怎棱。除此之外哩俭,str所引用的String對象底層還包含一個(gè)char[]數(shù)組,這個(gè)char[]數(shù)組里依次存放了h,e,l,l.o等字符串拳恋。
- (2)使用StringBuffer和StringBuilder進(jìn)行字符串拼接
如果程序中采用多個(gè)String對象進(jìn)行字符串連接運(yùn)算凡资,在運(yùn)行時(shí)將生成大量臨時(shí)字符串,這些字符串會(huì)保存在內(nèi)存中從而導(dǎo)致程序性能下降 - (3)盡早釋放無用對象的引用
大部分時(shí)候谬运,方法局部引用變量所引用的對象會(huì)隨著方法結(jié)束而變成垃圾隙赁,因?yàn)榫植孔兞康纳嬷芷诤芏蹋?dāng)方法運(yùn)行結(jié)束之時(shí)梆暖,該方法內(nèi)的局部變量就結(jié)束了生命周期伞访。因此,大部分時(shí)候程序無需將局部引用變量顯式設(shè)為null.
但是下面程序中的情形則需顯式設(shè)為null比較好了
如
public void info() {
Object obj=new Objec();
System.out.println(obj.toString());
System.out.println(obj.toString());
obj=null;
//執(zhí)行耗時(shí)轰驳,耗內(nèi)存的操作
//或者調(diào)用耗時(shí)厚掷,耗內(nèi)存的操作的方法
...
}
對于上面程序所示的info()方法,如果需要“執(zhí)行耗時(shí)级解,耗內(nèi)存的操作”或者"或者調(diào)用耗時(shí)冒黑,耗內(nèi)存的操作的方法",那么上面程序中顯式設(shè)置obj=null就是有必要的了∪涑茫可能的情況是:當(dāng)程序在“執(zhí)行耗時(shí)薛闪,耗內(nèi)存的操作”或者"或者調(diào)用耗時(shí),耗內(nèi)存的操作的方法",obj之前所引用的對象可能被垃圾加收了俺陋。
- (4)盡量少用靜態(tài)變量
從理論來說豁延,Java對象對象何時(shí)被回收由垃圾回收機(jī)制決定,對程序員來說是不確定的腊状。由于垃圾回收機(jī)制判斷一個(gè)對象是否是垃圾的唯一標(biāo)準(zhǔn)就是該對象是否有引用變量引用它诱咏,因此要盡早釋放對象的引用。
最壞的情況是某個(gè)對象被static變量所引用缴挖,那么垃圾回收機(jī)制通常是不會(huì)回收這個(gè)對象所占用的內(nèi)存的袋狞。
如
Class Person {
static Object obj=new Object();
}
對于上面的Object對象而言,只要obj變量還引用它映屋,就會(huì)不會(huì)被垃圾回收機(jī)制所回收
Person類所對應(yīng)的Class對象會(huì)常駐內(nèi)存苟鸯,直到程序結(jié)束,因此obj所引用的Object對象一旦被創(chuàng)建棚点,也會(huì)常駐內(nèi)存早处,直到程序運(yùn)行結(jié)束。
- (5)避免在經(jīng)常調(diào)用的方法瘫析,循環(huán)中創(chuàng)建Java對象
如
public class Test {
public static void main(String[] args) {
for(int i=0;i<10;i++) {
Object obj=new Object();
//執(zhí)行其它操作...
}
}
}
上面物循環(huán)產(chǎn)生了10個(gè)對象砌梆,系統(tǒng)要不斷地為這些對象分配內(nèi)存空間,執(zhí)行初始化操作贬循。它們的生存時(shí)間并不長咸包,接下來系統(tǒng)又需要回收它們所占用的內(nèi)存空間是,這種不斷分配內(nèi)存杖虾,回收操作中烂瘫,程序的性能受到了很大的影響。
- (6)緩存經(jīng)常使用的對象
如果有些對象需要經(jīng)常使用奇适,可以考慮把這些對象用緩存池保存起來坟比,這樣下次需要時(shí)就可直接拿出來這些對象來用。典型的緩存池是數(shù)據(jù)連接池滤愕,數(shù)據(jù)連接池里緩存了大量的數(shù)據(jù)庫連接温算,每次程序需要訪問數(shù)據(jù)庫時(shí)都可直接取出數(shù)據(jù)庫連接。
除此之外间影,如果系統(tǒng)里還有一些常用的基礎(chǔ)信息注竿,比如信息化信息里包含的員工信息,物料信息等魂贬,也可以考慮對它們進(jìn)行緩存巩割。
使用緩存通常有兩種方法
1.使用HashMap進(jìn)行緩存
2.直接使用開源緩存項(xiàng)目。(如OSCache,Ehcahe等) - (7)盡量不要用finalize方法
在一個(gè)對象失去引用之后付燥,垃圾回收器準(zhǔn)備回收該對象之前宣谈,垃圾回收器會(huì)先調(diào)用對象的finalize()方法來執(zhí)行資源清理。出于這種考慮键科,可能有些開發(fā)者會(huì)考慮使用finalize()方法來進(jìn)和清理闻丑。
在垃圾回收器本身已經(jīng)嚴(yán)重制約應(yīng)用程序性能的情況下漩怎,如果再選擇使用finalize方法進(jìn)行資源清理,無疑是一種火上澆油的行為嗦嗡,這將導(dǎo)致垃圾回收器的負(fù)擔(dān)更大勋锤,導(dǎo)致程序運(yùn)行效率更低 - (8)考慮使用SoftReference軟引用
3.Java內(nèi)存管理--內(nèi)存分配
一、Java內(nèi)存分配
1侥祭、 Java有幾種存儲區(qū)域叁执?
- 寄存器
-- 在CPU內(nèi)部,開發(fā)人員不能通過代碼來控制寄存器的分配矮冬,由編譯器來管理 - 棧
-- 在Windows下, 棧是向低地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu)谈宛,是一塊連續(xù)的內(nèi)存的區(qū)域,即棧頂?shù)牡刂泛蜅5淖畲笕萘渴窍到y(tǒng)預(yù)先規(guī)定好的胎署。
-- 優(yōu)點(diǎn):由系統(tǒng)自動(dòng)分配吆录,速度較快。
-- 缺點(diǎn):不夠靈活硝拧,但程序員是無法控制的径筏。
-- 存放基本數(shù)據(jù)類型、開發(fā)過程中就創(chuàng)建的對象(而不是運(yùn)行過程中) - 堆
-- 是向高地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu)障陶,是不連續(xù)的內(nèi)存區(qū)域
-- 在堆中滋恬,沒有堆棧指針,為此也就無法直接從處理器那邊獲得支持
-- 堆的好處是有很大的靈活性抱究。如Java編譯器不需要知道從堆里需要分配多少存儲區(qū)域恢氯,也不必知道存儲的數(shù)據(jù)在堆里會(huì)存活多長時(shí)間。 - 靜態(tài)存儲區(qū)域與常量存儲區(qū)域
-- 靜態(tài)存儲區(qū)用來存放static類型的變量
-- 常量存儲區(qū)用來存放常量類型(final)類型的值鼓寺,一般在只讀存儲器中 - 非RAM存儲
-- 如流對象勋拟,是要發(fā)送到另外一臺機(jī)器上的
-- 持久化的對象,存放在磁盤上
2妈候、 java內(nèi)存分配
- 基礎(chǔ)數(shù)據(jù)類型直接在椄颐遥空間分配;
- 方法的形式參數(shù),直接在椏嘁空間分配啸胧,當(dāng)方法調(diào)用完成后從棧空間回收;
- 引用數(shù)據(jù)類型幔虏,需要用new來創(chuàng)建纺念,既在棧空間分配一個(gè)地址空間想括,又在堆空間分配對象的類變量;
- 方法的引用參數(shù)陷谱,在棧空間分配一個(gè)地址空間瑟蜈,并指向堆空間的對象區(qū)烟逊,當(dāng)方法調(diào)用完后從椩埽空間回收;
- 局部變量 new 出來時(shí),在棻焊瘢空間和堆空間中分配空間图毕,當(dāng)局部變量生命周期結(jié)束后夷都,椌彀Γ空間立刻被回收,堆空間區(qū)域等待GC回收;
- 方法調(diào)用時(shí)傳入的 literal 參數(shù)囤官,先在棧空間分配,在方法調(diào)用完成后從椘飞剑空間釋放;
- 字符串常量在 DATA 區(qū)域分配 友酱,this 在堆空間分配;
- 數(shù)組既在棧空間分配數(shù)組名稱刑顺, 又在堆空間分配數(shù)組實(shí)際的大新惹稀!
3蹲堂、Java內(nèi)存模型
Java虛擬機(jī)將其管轄的內(nèi)存大致分三個(gè)邏輯部分:方法區(qū)(Method Area)狼讨、Java棧和Java堆。
- 方法區(qū)是靜態(tài)分配的柒竞,編譯器將變量在綁定在某個(gè)存儲位置上政供,而且這些綁定不會(huì)在運(yùn)行時(shí)改變。
常數(shù)池朽基,源代碼中的命名常量布隔、String常量和static 變量保存在方法區(qū)。 - Java Stack是一個(gè)邏輯概念稼虎,特點(diǎn)是后進(jìn)先出衅檀。一個(gè)棧的空間可能是連續(xù)的,也可能是不連續(xù)的霎俩。
最典型的Stack應(yīng)用是方法的調(diào)用哀军,Java虛擬機(jī)每調(diào)用一次方法就創(chuàng)建一個(gè)方法幀(frame),退出該方法則對應(yīng)的 方法幀被彈出(pop)茸苇。棧中存儲的數(shù)據(jù)也是運(yùn)行時(shí)確定的排苍? - Java堆分配(heap allocation)意味著以隨意的順序,在運(yùn)行時(shí)進(jìn)行存儲空間分配和收回的內(nèi)存管理模型学密。
堆中存儲的數(shù)據(jù)常常是大小淘衙、數(shù)量和生命期在編譯時(shí)無法確定的。Java對象的內(nèi)存總是在heap中分配腻暮。
4彤守、Java內(nèi)存分配實(shí)例解析
常量池(constant pool)指的是在編譯期被確定毯侦,并被保存在已編譯的.class文件中的一些數(shù)據(jù)。它包括了關(guān)于類具垫、方法侈离、接口等中的常量,也包括字符串常量筝蚕。
常量池在運(yùn)行期被JVM裝載卦碾,并且可以擴(kuò)充。String的intern()方法就是擴(kuò)充常量池的一個(gè)方法起宽;當(dāng)一個(gè)String實(shí)例str調(diào)用intern()方法時(shí)洲胖,Java查找常量池中是否有相同Unicode的字符串常量,如果有坯沪,則返回其引用绿映,如果沒有,則在常量池中增加一個(gè)Unicode等于str的字符串并返回它的引用腐晾。
例:
String s1=new String("kvill");
String s2=s1.intern();
System.out.println( s1==s1.intern() );//false
System.out.println( s1+" "+s2 );// kvill kvill
System.out.println( s2==s1.intern() );//true
這個(gè)類中事先沒有聲名”kvill”常量叉弦,所以常量池中一開始是沒有”kvill”的,當(dāng)調(diào)用s1.intern()后就在常量池中新添加了一個(gè) ”kvill”常量藻糖,原來的不在常量池中的”kvill”仍然存在淹冰。s1==s1.intern()為false說明原來的“kvill”仍然存在;s2 現(xiàn)在為常量池中“kvill”的地址颖御,所以有s2==s1.intern()為true榄棵。
String 常量池問題
(1) 字符串常量的"+"號連接,在編譯期字符串常量的值就確定下來, 拿"a" + 1來說潘拱,編譯器優(yōu)化后在class中就已經(jīng)是a1疹鳄。
String a = "a1";
String b = "a" + 1;
System.out.println((a == b)); //result = true
String a = "atrue";
String b = "a" + "true";
System.out.println((a == b)); //result = true
String a = "a3.4";
String b = "a" + 3.4;
System.out.println((a == b)); //result = true
(2) 對于含有字符串引用的"+"連接,無法被編譯器優(yōu)化芦岂。
String a = "ab";
String bb = "b";
String b = "a" + bb;
System.out.println((a == b)); //result = false
由于引用的值在程序編譯期是無法確定的瘪弓,即"a" + bb,只有在運(yùn)行期來動(dòng)態(tài)分配并將連接后的新地址賦給b禽最。
(3) 對于final修飾的變量腺怯,它在編譯時(shí)被解析為常量值的一個(gè)本地拷貝并存儲到自己的常量池中或嵌入到它的字節(jié)碼流中。所以此時(shí)的"a" + bb和"a" + "b"效果是一樣的川无。
String a = "ab";
final String bb = "b";
String b = "a" + bb;
System.out.println((a == b)); //result = true
(4) jvm對于字符串引用bb呛占,它的值在編譯期無法確定,只有在程序運(yùn)行期調(diào)用方法后懦趋,將方法的返回值和"a"來動(dòng)態(tài)連接并分配地址為b晾虑。
String a = "ab";
final String bb = getbb();
String b = "a" + bb;
System.out.println((a == b)); //result = false
private static string getbb() {
return "b";
}
(5) String 變量采用連接運(yùn)算符(+)效率低下。
String s = "a" + "b" + "c"; 就等價(jià)于String s = "abc";
String a = "a";
String b = "b";
String c = "c";
String s = a + b + c;
這個(gè)就不一樣了,最終結(jié)果等于:
Stringbuffer temp = new Stringbuffer();
temp.append(a).append(b).append(c);
String s = temp.toString();
(6) Integer帜篇、Double等包裝類和String有著同樣的特性:不變類糙捺。
String str = "abc"的內(nèi)部工作機(jī)制很有代表性,以Boolean為例笙隙,說明同樣的問題洪灯。
不變類的屬性一般定義為final,一旦構(gòu)造完畢就不能再改變了竟痰。
Boolean對象只有有限的兩種狀態(tài):true和false签钩,將這兩個(gè)Boolean對象定義為命名常量:
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
這兩個(gè)命名常量和字符串常量一樣,在常數(shù)池中分配空間凯亮。 Boolean.TRUE是一個(gè)引用边臼,Boolean.FALSE是一個(gè)引用,而"abc"也是一個(gè)引用假消!由于Boolean.TRUE是類變量(static)將靜態(tài)地分配內(nèi)存,所以需要很多Boolean對象時(shí)岭接,并不需要用new表達(dá)式創(chuàng)建各個(gè)實(shí)例富拗,完全可以共享這兩個(gè)靜態(tài)變量。其JDK中源代碼是:
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
基本數(shù)據(jù)(Primitive)類型的自動(dòng)裝箱(autoboxing)鸣戴、拆箱(unboxing)是JSE 5.0提供的新功能啃沪。 Boolean b1 = 5>3; 等價(jià)于Boolean b1 = Boolean.valueOf(5>3); //優(yōu)于Boolean b1 = new Boolean (5>3);
static void foo(){
boolean isTrue = 5>3; //基本類型
Boolean b1 = Boolean.TRUE; //靜態(tài)變量創(chuàng)建的對象
Boolean b2 = Boolean.valueOf(isTrue);//靜態(tài)工廠
Boolean b3 = 5>3;//自動(dòng)裝箱(autoboxing)
System.out.println("b1 == b2 ?" +(b1 == b2));
System.out.println("b1 == b3 ?" +(b1 == b3));
Boolean b4 = new Boolean(isTrue);////不宜使用
System.out.println("b1 == b4 ?" +(b1 == b4));//浪費(fèi)內(nèi)存、有創(chuàng)建實(shí)例的時(shí)間開銷
} //這里b1窄锅、b2创千、b3指向同一個(gè)Boolean對象。
(7) 如果問你:String x ="abc";創(chuàng)建了幾個(gè)對象入偷?
準(zhǔn)確的答案是:0或者1個(gè)追驴。如果存在"abc",則變量x持有"abc"這個(gè)引用疏之,而不創(chuàng)建任何對象殿雪。
如果問你:String str1 = new String("abc"); 創(chuàng)建了幾個(gè)對象?
準(zhǔn)確的答案是:1或者2個(gè)锋爪。(至少1個(gè)在heap中)
(8) 對于int a = 3; int b = 3丙曙;
編譯器先處理int a = 3;首先它會(huì)在棧中創(chuàng)建一個(gè)變量為a的引用其骄,然后查找有沒有字面值為3的地址亏镰,沒找到,就開辟一個(gè)存放3這個(gè)字面值的地址拯爽,然后將a指向3的地址索抓。接著處理int b = 3;在創(chuàng)建完b的引用變量后,由于在棧中已經(jīng)有3這個(gè)字面值纸兔,便將b直接指向3的地址惰瓜。這樣,就出現(xiàn)了a與b同時(shí)均指向3的情況汉矿。
5崎坊、堆(Heap)和非堆(Non-heap)內(nèi)存
按照官方的說法:“Java 虛擬機(jī)具有一個(gè)堆,堆是運(yùn)行時(shí)數(shù)據(jù)區(qū)域洲拇,所有類實(shí)例和數(shù)組的內(nèi)存均從此處分配奈揍。堆是在 Java 虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建的「承”
可以看出JVM主要管理兩種類型的內(nèi)存:堆和非堆男翰。
簡單來說堆就是Java代碼可及的內(nèi)存,是留給開發(fā)人員使用的纽乱;
非堆就是JVM留給自己用的蛾绎,所以方法區(qū)、JVM內(nèi)部處理或優(yōu)化所需的內(nèi)存(如JIT編譯后的代碼緩存)鸦列、每個(gè)類結(jié)構(gòu)(如運(yùn)行時(shí)常數(shù)池租冠、字段和方法數(shù)據(jù))以及方法和構(gòu)造方法的代碼都在非堆內(nèi)存中。
堆內(nèi)存分配
JVM初始分配的內(nèi)存由-Xms指定薯嗤,默認(rèn)是物理內(nèi)存的1/64顽爹;
JVM最大分配的內(nèi)存由-Xmx指定,默認(rèn)是物理內(nèi)存的1/4骆姐。
默認(rèn)空余堆內(nèi)存小于40%時(shí)镜粤,JVM就會(huì)增大堆直到-Xmx的最大限制;空余堆內(nèi)存大于70%時(shí)玻褪,JVM會(huì)減少堆直到-Xms的最小限制肉渴。
因此服務(wù)器一般設(shè)置-Xms、-Xmx相等以避免在每次GC 后調(diào)整堆的大小归园。
非堆內(nèi)存分配
JVM使用-XX:PermSize設(shè)置非堆內(nèi)存初始值黄虱,默認(rèn)是物理內(nèi)存的1/64;
由XX:MaxPermSize設(shè)置最大非堆內(nèi)存的大小庸诱,默認(rèn)是物理內(nèi)存的1/4捻浦。
例子
-Xms256m
-Xmx1024m
-XX:PermSize=128M
-XX:MaxPermSize=256M
4.java內(nèi)存機(jī)制--堆與棧
問題的引入:
問題一:
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
問題二:
String str1 =new String ("abc");
String str2 =new String ("abc");
System.out.println(str1==str2); // false
問題三:
String s1 = "ja";
String s2 = "va";
String s3 = "java";
String s4 = s1 + s2;
System.out.println(s3 == s4);//false
System.out.println(s3.equals(s4));//true
一、Java 中的堆和棧
Java把內(nèi)存劃分成兩種:一種是棧內(nèi)存桥爽,一種是堆內(nèi)存朱灿。
在函數(shù)中定義的一些基本類型的變量和對象的引用變量都在函數(shù)的棧內(nèi)存中分配。
當(dāng)在一段代碼塊定義一個(gè)變量時(shí)钠四,Java就在棧中為這個(gè)變量分配內(nèi)存空間盗扒,當(dāng)超過變量的作用域后跪楞,Java會(huì)自動(dòng)釋放掉為該變量所分配的內(nèi)存空間,該內(nèi)存空間可以立即被另作他用侣灶。
堆內(nèi)存用來存放由new創(chuàng)建的對象和數(shù)組甸祭。
在堆中分配的內(nèi)存,由Java虛擬機(jī)的自動(dòng)垃圾回收器來管理褥影。
在堆中產(chǎn)生了一個(gè)數(shù)組或?qū)ο蠛蟪鼗В€可以在棧中定義一個(gè)特殊的變量,讓棧中這個(gè)變量的取值等于數(shù)組或?qū)ο笤诙褍?nèi)存中的首地址凡怎,棧中的這個(gè)變量就成了數(shù)組或?qū)ο蟮囊米兞俊?/p>
引用變量就相當(dāng)于是為數(shù)組或?qū)ο笃鸬囊粋€(gè)名稱校焦,以后就可以在程序中使用棧中的引用變量來訪問堆中的數(shù)組或?qū)ο蟆?/p>
具體的說:
棧與堆都是Java用來在Ram中存放數(shù)據(jù)的地方。與C++不同统倒,Java自動(dòng)管理?xiàng):投颜洌绦騿T不能直接地設(shè)置棧或堆房匆。
Java的堆是一個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū),類的(對象從中分配空間耸成。這些對象通過new、newarray坛缕、anewarray和multianewarray等指令建立墓猎,它們不需要程序代碼來顯式的釋放。堆是由垃圾回收來負(fù)責(zé)的赚楚,堆的優(yōu)勢是可以動(dòng)態(tài)地分配內(nèi)存大小,生存期也不必事先告訴編譯器骗卜,因?yàn)樗窃谶\(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存的宠页,Java的垃圾收集器會(huì)自動(dòng)收走這些不再使用的數(shù)據(jù)。但缺點(diǎn)是寇仓,由于要在運(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存举户,存取速度較慢。
棧的優(yōu)勢是遍烦,存取速度比堆要快俭嘁,僅次于寄存器,棧數(shù)據(jù)可以共享服猪。但缺點(diǎn)是供填,存在棧中的數(shù)據(jù)大小與生存期必須是確定的,缺乏靈活性罢猪。棧中主要存放一些基本類型的變量(int, short, long, byte, float, double, boolean, char)和對象句柄近她。
棧有一個(gè)很重要的特殊性,就是存在棧中的數(shù)據(jù)可以共享膳帕。假設(shè)我們同時(shí)定義:
int a = 3;
int b = 3粘捎;
編譯器先處理int a = 3;首先它會(huì)在棧中創(chuàng)建一個(gè)變量為a的引用,然后查找棧中是否有3這個(gè)值攒磨,如果沒找到泳桦,就將3存放進(jìn)來,然后將a指向3娩缰。接著處理int b = 3灸撰;在創(chuàng)建完b的引用變量后,因?yàn)樵跅V幸呀?jīng)有3這個(gè)值漆羔,便將b直接指向3梧奢。這樣,就出現(xiàn)了a與b同時(shí)均指向3的情況演痒。這時(shí)亲轨,如果再令a=4;那么編譯器會(huì)重新搜索棧中是否有4值鸟顺,如果沒有惦蚊,則將4存放進(jìn)來,并令a指向4讯嫂;如果已經(jīng)有了蹦锋,則直接將a指向這個(gè)地址。因此a值的改變不會(huì)影響到b的值欧芽。要注意這種數(shù)據(jù)的共享與兩個(gè)對象的引用同時(shí)指向一個(gè)對象的這種共享是不同的莉掂,因?yàn)檫@種情況a的修改并不會(huì)影響到b, 它是由編譯器完成的,它有利于節(jié)省空間千扔。而一個(gè)對象引用變量修改了這個(gè)對象的內(nèi)部狀態(tài)憎妙,會(huì)影響到另一個(gè)對象引用變量。
String是一個(gè)特殊的包裝類數(shù)據(jù)曲楚±逋伲可以用:
String str = new String("abc");
String str = "abc";
兩種的形式來創(chuàng)建,第一種是用new()來新建對象的龙誊,它會(huì)在存放于堆中抚垃。每調(diào)用一次就會(huì)創(chuàng)建一個(gè)新的對象。
而第二種是先在棧中創(chuàng)建一個(gè)對String類的對象引用變量str趟大,然后查找棧中有沒有存放"abc"鹤树,如果沒有,則將"abc"存放進(jìn)棧护昧,并令str指向”abc”魂迄,如果已經(jīng)有”abc” 則直接令str指向“abc”。
比較類里面的數(shù)值是否相等時(shí)惋耙,用equals()方法捣炬;當(dāng)測試兩個(gè)包裝類的引用是否指向同一個(gè)對象時(shí)熊昌,用==,下面用例子說明上面的理論湿酸。
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
可以看出str1和str2是指向同一個(gè)對象的婿屹。
String str1 =new String ("abc");
String str2 =new String ("abc");
System.out.println(str1==str2); // false
用new的方式是生成不同的對象。每一次生成一個(gè)推溃。
因此用第二種方式創(chuàng)建多個(gè)”abc”字符串,在內(nèi)存中其實(shí)只存在一個(gè)對象而已. 這種寫法有利與節(jié)省內(nèi)存空間. 同時(shí)它可以在一定程度上提高程序的運(yùn)行速度昂利,因?yàn)镴VM會(huì)自動(dòng)根據(jù)棧中數(shù)據(jù)的實(shí)際情況來決定是否有必要?jiǎng)?chuàng)建新對象。而對于String str = new String("abc")铁坎;的代碼蜂奸,則一概在堆中創(chuàng)建新對象,而不管其字符串值是否相等硬萍,是否有必要?jiǎng)?chuàng)建新對象扩所,從而加重了程序的負(fù)擔(dān)。
另一方面, 要注意: 我們在使用諸如String str = "abc"朴乖;的格式定義類時(shí)祖屏,總是想當(dāng)然地認(rèn)為,創(chuàng)建了String類的對象str买羞。擔(dān)心陷阱袁勺!對象可能并沒有被創(chuàng)建!而可能只是指向一個(gè)先前已經(jīng)創(chuàng)建的對象畜普。只有通過new()方法才能保證每次都創(chuàng)建一個(gè)新的對象期丰。由于String類的immutable性質(zhì),當(dāng)String變量需要經(jīng)常變換其值時(shí)吃挑,應(yīng)該考慮使用StringBuffer類咐汞,以提高程序效率。
二儒鹿、java中內(nèi)存分配策略及堆和棧的比較
2.1 內(nèi)存分配策略
按照編譯原理的觀點(diǎn),程序運(yùn)行時(shí)的內(nèi)存分配有三種策略,分別是靜態(tài)的,棧式的,和堆式的。
靜態(tài)存儲分配是指在編譯時(shí)就能確定每個(gè)數(shù)據(jù)目標(biāo)在運(yùn)行時(shí)刻的存儲空間需求,因而在編譯時(shí)就可以給他們分配固定的內(nèi)存空間.這種分配策略要求程序代碼中不允許有可變數(shù)據(jù)結(jié)構(gòu)(比如可變數(shù)組)的存在,也不允許有嵌套或者遞歸的結(jié)構(gòu)出現(xiàn),因?yàn)樗鼈兌紩?huì)導(dǎo)致編譯程序無法計(jì)算準(zhǔn)確的存儲空間需求.
棧式存儲分配也可稱為動(dòng)態(tài)存儲分配,是由一個(gè)類似于堆棧的運(yùn)行棧來實(shí)現(xiàn)的.和靜態(tài)存儲分配相反,在棧式存儲方案中,程序?qū)?shù)據(jù)區(qū)的需求在編譯時(shí)是完全未知的,只有到運(yùn)行的時(shí)候才能夠知道,但是規(guī)定在運(yùn)行中進(jìn)入一個(gè)程序模塊時(shí),必須知道該程序模塊所需的數(shù)據(jù)區(qū)大小才能夠?yàn)槠浞峙鋬?nèi)存.和我們在數(shù)據(jù)結(jié)構(gòu)所熟知的棧一樣,棧式存儲分配按照先進(jìn)后出的原則進(jìn)行分配几晤。
靜態(tài)存儲分配要求在編譯時(shí)能知道所有變量的存儲要求,棧式存儲分配要求在過程的入口處必須知道所有的存儲要求,而堆式存儲分配則專門負(fù)責(zé)在編譯時(shí)或運(yùn)行時(shí)模塊入口處都無法確定存儲要求的數(shù)據(jù)結(jié)構(gòu)的內(nèi)存分配,比如可變長度串和對象實(shí)例.堆由大片的可利用塊或空閑塊組成,堆中的內(nèi)存可以按照任意順序分配和釋放.
2.2 堆和棧的比較
上面的定義從編譯原理的教材中總結(jié)而來,除靜態(tài)存儲分配之外,都顯得很呆板和難以理解,下面撇開靜態(tài)存儲分配,集中比較堆和棧:
從堆和棧的功能和作用來通俗的比較,堆主要用來存放對象的约炎,棧主要是用來執(zhí)行程序的.而這種不同又主要是由于堆和棧的特點(diǎn)決定的:
在編程中,例如C/C++中蟹瘾,所有的方法調(diào)用都是通過棧來進(jìn)行的,所有的局部變量,形式參數(shù)都是從棧中分配內(nèi)存空間的圾浅。實(shí)際上也不是什么分配,只是從棧頂向上用就行,就好像工廠中的傳送帶(conveyor belt)一樣,Stack Pointer會(huì)自動(dòng)指引你到放東西的位置,你所要做的只是把東西放下來就行.退出函數(shù)的時(shí)候,修改棧指針就可以把棧中的內(nèi)容銷毀.這樣的模式速度最快, 當(dāng)然要用來運(yùn)行程序了.需要注意的是,在分配的時(shí)候,比如為一個(gè)即將要調(diào)用的程序模塊分配數(shù)據(jù)區(qū)時(shí),應(yīng)事先知道這個(gè)數(shù)據(jù)區(qū)的大小,也就說是雖然分配是在程序運(yùn)行時(shí)進(jìn)行的,但是分配的大小多少是確定的,不變的,而這個(gè)"大小多少"是在編譯時(shí)確定的,不是在運(yùn)行時(shí).
堆是應(yīng)用程序在運(yùn)行的時(shí)候請求操作系統(tǒng)分配給自己內(nèi)存憾朴,由于從操作系統(tǒng)管理的內(nèi)存分配,所以在分配和銷毀時(shí)都要占用時(shí)間狸捕,因此用堆的效率非常低.但是堆的優(yōu)點(diǎn)在于,編譯器不必知道要從堆里分配多少存儲空間,也不必知道存儲的數(shù)據(jù)要在堆里停留多長的時(shí)間,因此,用堆保存數(shù)據(jù)時(shí)會(huì)得到更大的靈活性众雷。事實(shí)上,面向?qū)ο蟮亩鄳B(tài)性,堆內(nèi)存分配是必不可少的,因?yàn)槎鄳B(tài)變量所需的存儲空間只有在運(yùn)行時(shí)創(chuàng)建了對象之后才能確定.在C++中灸拍,要求創(chuàng)建一個(gè)對象時(shí)做祝,只需用 new命令編制相關(guān)的代碼即可。執(zhí)行這些代碼時(shí)鸡岗,會(huì)在堆里自動(dòng)進(jìn)行數(shù)據(jù)的保存.當(dāng)然混槐,為達(dá)到這種靈活性,必然會(huì)付出一定的代價(jià):在堆里分配存儲空間時(shí)會(huì)花掉更長的時(shí)間轩性!這也正是導(dǎo)致我們剛才所說的效率低的原因,看來列寧同志說的好,人的優(yōu)點(diǎn)往往也是人的缺點(diǎn),人的缺點(diǎn)往往也是人的優(yōu)點(diǎn).
2.3 JVM中的堆和棧
JVM是基于堆棧的虛擬機(jī).JVM為每個(gè)新創(chuàng)建的線程都分配一個(gè)堆棧.也就是說,對于一個(gè)Java程序來說声登,它的運(yùn)行就是通過對堆棧的操作來完成的。堆棧以幀為單位保存線程的狀態(tài)揣苏。JVM對堆棧只進(jìn)行兩種操作:以幀為單位的壓棧和出棧操作悯嗓。
我們知道,某個(gè)線程正在執(zhí)行的方法稱為此線程的當(dāng)前方法.我們可能不知道,當(dāng)前方法使用的幀稱為當(dāng)前幀。當(dāng)線程激活一個(gè)Java方法,JVM就會(huì)在線程的 Java堆棧里新壓入一個(gè)幀脯厨。這個(gè)幀自然成為了當(dāng)前幀.在此方法執(zhí)行期間,這個(gè)幀將用來保存參數(shù),局部變量,中間計(jì)算過程和其他數(shù)據(jù).這個(gè)幀在這里和編譯原理中的活動(dòng)紀(jì)錄的概念是差不多的.
從Java的這種分配機(jī)制來看,堆棧又可以這樣理解:堆棧(Stack)是操作系統(tǒng)在建立某個(gè)進(jìn)程時(shí)或者線程(在支持多線程的操作系統(tǒng)中是線程)為這個(gè)線程建立的存儲區(qū)域蛾派,該區(qū)域具有先進(jìn)后出的特性。
每一個(gè)Java應(yīng)用都唯一對應(yīng)一個(gè)JVM實(shí)例洪乍,每一個(gè)實(shí)例唯一對應(yīng)一個(gè)堆眯杏。應(yīng)用程序在運(yùn)行中所創(chuàng)建的所有類實(shí)例或數(shù)組都放在這個(gè)堆中,并由應(yīng)用所有的線程共享.跟C/C++不同,Java中分配堆內(nèi)存是自動(dòng)初始化的岂贩。Java中所有對象的存儲空間都是在堆中分配的巷波,但是這個(gè)對象的引用卻是在堆棧中分配,也就是說在建立一個(gè)對象時(shí)從兩個(gè)地方都分配內(nèi)存萎津,在堆中分配的內(nèi)存實(shí)際建立這個(gè)對象,而在堆棧中分配的內(nèi)存只是一個(gè)指向這個(gè)堆對象的指針(引用)而已抹镊。