java必備知識進階

1.java的內存管理

a.主要包括:內存分配和內存回收

b.注意點:java的垃圾回收是不能保證一定發生的,除非jvm內存耗盡,合理的管理對象還是有必要的

c.java程序執行過程:java源文件-》java字節碼文件.class-》類加載器-》直接到執行引擎或者先到運行時數據區(內存)再到執行引擎

其中類加載過程有:加載-》鏈接(驗證【爲了確保Class文件中的字節流包含的信息符合當前虛擬機的要求】、準備【爲類變量分配內存並設置初始值】、【解析虛擬機將常量池的符號引用轉爲直接引用】)-》初始化(初始化類變量和其它資源、執行類構造器方法的過程)

其中類加載順序:啓動類加載器-》擴展類加載器-》應用程序類加載器

其中運行時內存區域劃分圖:

圖

其中類成員初始化順序:先靜態後普通再構造,先父類後子類

1、父類靜態變量和代碼塊-》子類靜態變量和代碼塊
2、父類普通變量和代碼塊-》執行父類構造器
3、子類普通變量和代碼塊-》子類構造器
4、static方法初始化優先於普通方法,靜態初始化只有在必要時刻進行且只初始化一次

注意: 子類的構造方法,不管這個構造方法帶不帶參數,默認的它都會先去尋找父類的不帶參數的構造方法。如果父類沒有不帶參數的構造方法,那麼子類必須用supper關鍵子來調用父類帶參數的構造方法,否則編譯不能通過。

d.四種垃圾回收器和finalize方法:

java中垃圾回收器可以幫助程序猿自動回收無用對象佔據的內存,但它只負責釋放java中創建的對象所佔據的所有內存,通過某種創建對象之外的方式爲對象分配的內存空間則無法被垃圾回收器回收;而且垃圾回收本身也有開銷,GC的優先級比較低,所以如果JVM沒有面臨內存耗盡,它是不會去浪費資源進行垃圾回收以恢復內存的。最後我們會發現,只要程序沒有瀕臨存儲空間用完那一刻,對象佔用的空間就總也得不到釋放。我們可以通過代碼System.gc()來主動啓動一個垃圾回收器(雖然JVM不會立刻去回收),在釋放new分配內存空間之前,將會通過finalize()釋放用其他方法分配的內存空間。

四種收集器:Serial收集器(單線程的新生代收集器,必須暫停其它所有工作線程直到收集結束)、Parallel收集器(多線程串行收集器)、CMS收集器(基於標記-清除算法實現,容易產生大量碎片,cpu資源佔用大)、G1收集器(標記整理算法實現,不會產生空間碎片,高吞吐量)

finalize工作原理:一旦垃圾回收器準備好釋放對象佔用的存儲空間,將首先調用並且只能調用一次該對象的finalize()方法(通過代碼System.gc()實現),並且在下一次垃圾回收動作發生時,纔會真正回收對象佔用的內存。所以如果我們重載finalize()方法就能在垃圾回收時刻做一些重要的清理工作或者自救該對象一次(只要在finalize()方法中讓該對象重新和引用鏈上的任何一個對象建立關聯即可)。finalize()方法用於釋放用特殊方式分配的內存空間,這是因爲我們可能在java中調用非java代碼來分配內存,比如Android開發中調用NDK。那麼,當我們調用C中的malloc()函數分配了存儲空間,我們就只能用free()函數來釋放這些內存,這樣就需要我們在finalize()函數中用本地方法調用它。

e.如何判斷java對象需要被回收

引用計數,計數爲0表示不可用,引用計數記錄着每個對象被其它對象所持有的引用數,被引用一次加1,時效減1;當一個對象被回收後,該對象所引用的其它對象的引用計數都會減少,它很難解決對象之間的循環引用實例

可達性分析算法,從GC ROOT對象向下搜索其走過的路徑稱爲引用鏈,當一個對象不再被任何的GC ROOT對象引用鏈相連時說明該對象不再可用;GC ROOT對象包括四種:方法區中常量和靜態變量的引用對象,虛擬機棧中變量引用的對象,本地方法棧中引用的對象;解決循環引用是因爲GC ROOT是一組特別管理的指針,他們不是對象圖裏的對象,對象也不可能引用到這些指針

比較,可達性分析避免了循環引用的問題,引用計數算法只需要在每個實例對象之初,通過計數器來記錄所有的引用次數即可,而可達性分析需要遍歷整個GC根節點來判斷是否回收

f.java對象的四種引用

強引用 :創建一個對象並把這個對象直接賦給一個變量,eg :Person person = new Person(“sunny”); 不管系統     資源有麼的緊張,強引用的對象都絕對不會被回收,即使他以後不會再用到。
          軟引用 :通過SoftReference類實現,eg : SoftReference p = new SoftReference(new Person(“Rain”));內存非常緊張的時候會被回收,其他時候不會被回收,所以在使用之前要判斷是否爲null從而判斷他是否已經被回收了。
          弱引用 :通過WeakReference類實現,eg : WeakReference p = new WeakReference(new Person(“Rain”));不管內存是否足夠,系統垃圾回收時必定會回收
         虛引用 :不能單獨使用,主要是用於追蹤對象被垃圾回收的狀態,爲一個對象設置虛引用關聯的唯一目的是希望能在這個對象被收集器回收時收到一個系統通知。通過PhantomReference類和引用隊列ReferenceQueue類聯合使用實現

g.垃圾回收算法

停止-複製算法
這是一種非後臺回收算法,將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊,內存浪費嚴重.它先暫停程序的運行,然後將所有存活的對象從當前堆複製到另外一個堆,沒被複制的死對象則全部是垃圾,存活對象被複制到新堆之後全部緊密排列,就可以直接分配新空間了。此方法耗費空間且效率低,適用於存活對象少。

標記-清掃算法
同樣是非後臺回收算法,該算法從堆棧區和靜態域出發,遍歷每一個引用去尋找所有需要回收的對象,對每個找到需要回收對象都進行標記。標記結束之後,開始清理工作,被標記的對象都會被釋放掉,如果需要連續堆空間,則還需要對剩下的存貨對象進行整理;否則會產生大量內存碎片

標記-整理算法
先標記需要回收的對象,但是不會直接清理那些可回收的對象,而是將存活對象向內存區域的一端移動,然後清理掉端以外的內存。適用於存活對象多。

分代算法
在新生代中,每次垃圾收集時都會發現有大量對象死去,只有少量存活,因此可選用停止複製算法來完成收集,而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記—清除算法或標記—整理算法來進行回收。

        h.內存相關問題

內存泄露是指分配出去的內存沒有被回收回來,由於失去了對該內存區域的控制(例如你把它的地址給弄丟了),因而造成了資源的浪費。Java 中一般不會產生內存泄露,因爲有垃圾回收器自動回收垃圾,但這也不絕對,Java堆內也可能發生內存泄露(Memory Leak; 當我們 new 了對象,並保存了其引用,但是後面一直沒用它,而垃圾回收器又不會去回收它,這邊會造成內存泄露

內存溢出是指程序所需要的內存超出了系統所能分配的內存(包括動態擴展)的上限

符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不一定已經加載到了內存中。

直接引用:直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存佈局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那說明引用的目標必定已經存在於內存之中了。

雙親委派模型:表示類加載器之間的加載順序從頂至下的層次關係,加載器之間的父子關係一般都是通過組合來實現,而不是繼承。可以防止內存中出現多份同樣的字節碼,並確保加載順序

雙親委派模型的工作過程是:在loadClass函數中,首先會判斷該類是否被加載過,加載過則進行下一步—-解析,否則進行加載;如果一個類加載器收到了類加載器的請求,先不會自己嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父類加載器反饋自己無法完成這個加載請求(它的搜說範圍中沒有找到所需的類時,子加載類纔會嘗試自己去加載)

靜態分派和動態分派:靜態分派發生在編譯階段,是指依據靜態類型(變量聲明時定義的變量類型)來決定方法的執行版本,例如方法重載中依據參數的定義類型來定位具體應該執行的方法;動態分派發生在運行期,根據變量實例化時的實際類型來決定方法的執行版本,例如方法重寫;目前的 Java 語言(JDK1.6)是一門靜態多分派、動態單分派的語言。

動態分派具體實現Java虛擬機是通過在方法區中建立一個虛方法表,通過使用方法表的索引來代替元數據查找以提高性能。虛方法表中存放着各個方法的實際入口地址,如果子類沒有覆蓋父類的方法,那麼子類的虛方法表裏面的地址入口與父類是一致的;如果重寫父類的方法,那麼子類的方法表的地址將會替換爲子類實現版本的地址。方法表是在類加載的連接階段(驗證、準備、解析)進行初始化,準備了子類的初始化值後,虛擬機會把該類的虛方法表也進行初始化。

JDK7和8中內存模型變化:JDK7中把String常量池從永久代移到了堆中,並通過intern方法來保證不在堆中重複創建一個對象;JDK7開始使用G1收集器替代CMS收集器。JDK8使用元空間來替代原來的方法區,並且提供了字符串去重功能,也就是G1收集器可以識別出堆中那些重複出現的字符串並讓他們指向同一個內部char[]數組,而不是在堆中存在多份拷貝

 


2.堆和棧的區別

1、空間分配的不同:棧是由操作系統自動分配釋放,存放函數的參數值,局部變量的值等,而堆一般是由程序員分配釋放,弱不手動釋放,程序結束時可能由os回收

2、緩存方式不同:棧使用的是一級緩存,他們通常都是被調用時處於存儲空間中,調用完立即釋放,而堆是存放在二級緩存中,生命週期由虛擬機的GC算法決定,並不是一旦成爲孤立對象就能被回收

3、數據結構的不同:堆可以被看成是一棵樹 ,棧是一種先進後出的數據結構

 


3.字符串hash方法

•現在我們希望找到一個hash函數,使得每一個字符串都能夠映射到一個整數上
•比如hash[i]=(hash[i-1]*p+idx(s[i]))%mod
•字符串:abc,bbc,aba,aadaabac
•字符串下標從0開始
•先把a映射爲1,b映射爲2,c->3,d->4,即idx(a)=1, idx(b)=2, idx(c)=3,idx(d)=4;
•好!開始對字符串進行hash
假設我們取p=13 ,mod=101
先把abc映射爲一個整數
hash[0]=1,表示 a 映射爲1
hash[1]=(hash[0]*p+idx(b))%mod=15,表示 ab 映射爲 15
hash[2]=(hash[1]*p+idx(c))%mod=97
這樣,我們就把 abc 映射爲 97 這個數字了。
•用同樣的方法,我們可以把bbc,aba,aadaabac都映射到一個整數
•用同樣的hash函數,得到如下結果
• abc  ->  97
• bbc  ->  64
• aba  ->  95
• aadaabac  ->  35
•那麼,我們發現,這是一個字符串到整數的映射
•這樣子,我們就可以記錄下每個字符串對應的整數,當下一次出現了一個已經出現的字符串時,查詢整數是否出現過,就可以知道 字符串是否重複出現。
•現在要判斷兩個字符串是否一致,怎麼辦呢?直接用它們的hash值判斷即可,若hash值一致,則認爲字符串一致;若hash值不一致,則認爲是不同的字符串。
•我們要判斷兩個字符串是否一致,沒有那麼麻煩,直接先判斷長度是否一致,然後再判斷每個對應的字符是否一致即可。
•但,如果要判斷多個字符串裏有多少個不同的字符串,怎麼辦呢?
•兩兩字符串都進行比較?時間複雜度太高
•把每個字符串hash成一個整數,然後把所有整數進行一個去重操作,即可知道答案了。
當遇到衝突時,我們可以想辦法調整p和mod,使得衝突概率減小之又小。我們一般認爲p和mod一般取素數,p取一個較大的素數即可(6位到8位),mod取一個大素數,比如1e9+7,或者1e9+9。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章