1、既然有GC機(jī)制,為什么還會有內(nèi)存泄露的情況?
理論上Java因為有垃圾回收機(jī)制(GC)不會存在內(nèi)存泄露問題(這也是Java被廣泛使用于服務(wù)器端編程的一個重要原因)。然而在實際開發(fā)中,可能會存在無用但可達(dá)的對象,這些對象不能被GC回收,因此也會導(dǎo)致內(nèi)存泄露的發(fā)生。
例如hibernate的Session(一級緩存)中的對象屬于持久態(tài),垃圾回收器是不會回收這些對象的,然而這些對象中可能存在無用的垃圾對象,如果不及時關(guān)閉(close)或清空(flush)一級緩存就可能導(dǎo)致內(nèi)存泄露。
下面例子中的代碼也會導(dǎo)致內(nèi)存泄露。
import java.util.Arrays; import java.util.EmptyStackException; public class MyStack<T> { private T[] elements; private int size = 0; private static final int INIT_CAPACITY = 16; public MyStack() { elements = (T[]) new Object[INIT_CAPACITY]; } public void push(T elem) { ensureCapacity(); elements[size++] = elem; } public T pop() { if (size == 0) throw new EmptyStackException(); return elements[--size]; } private void ensureCapacity() { if (elements.length == size) { elements = Arrays.copyOf(elements,2 * size + 1); } } }
上面的代碼實現(xiàn)了一個棧(先進(jìn)后出(FILO))結(jié)構(gòu),乍看之下似乎沒有什么明顯的問題,它甚至可以通過你編寫的各種單元測試。
然而其中的pop方法卻存在內(nèi)存泄露的問題,當(dāng)我們用pop方法彈出棧中的對象時,該對象不會被當(dāng)作垃圾回收,即使使用棧的程序不再引用這些對象,因為棧內(nèi)部維護(hù)著對這些對象的過期引用(obsolete reference)。在支持垃圾回收的語言中,內(nèi)存泄露是很隱蔽的,這種內(nèi)存泄露其實就是無意識的對象保持。
如果一個對象引用被無意識的保留起來了,那么垃圾回收器不會處理這個對象,也不會處理該對象引用的其他對象,即使這樣的對象只有少數(shù)幾個,也可能會導(dǎo)致很多的對象被排除在垃圾回收之外,從而對性能造成重大影響,極端情況下會引發(fā)Disk Paging(物理內(nèi)存與硬盤的虛擬內(nèi)存交換數(shù)據(jù)),甚至造成OutOfMemoryError。
2、Java中為什么會有GC機(jī)制呢?
·安全性考慮;–for security.
·減少內(nèi)存泄露;–erase memory leak in some degree.
·減少程序員工作量。–Programmers dont worry about memory releasing.
3、對于Java的GC哪些內(nèi)存需要回收?
內(nèi)存運(yùn)行時JVM會有一個運(yùn)行時數(shù)據(jù)區(qū)來管理內(nèi)存。
它主要包括5大部分:
程序計數(shù)器(Program CounterRegister);
虛擬機(jī)棧(VM Stack);
本地方法棧(Native Method Stack);
方法區(qū)(Method Area);
堆(Heap)。
而其中程序計數(shù)器、虛擬機(jī)棧、本地方法棧是每個線程私有的內(nèi)存空間,隨線程而生,隨線程而亡。例如棧中每一個棧幀中分配多少內(nèi)存基本上在類結(jié)構(gòu)確定是哪個時就已知了,因此這3個區(qū)域的內(nèi)存分配和回收都是確定的,無需考慮內(nèi)存回收的問題。
但方法區(qū)和堆就不同了,一個接口的多個實現(xiàn)類需要的內(nèi)存可能不一樣,我們只有在程序運(yùn)行期間才會知道會創(chuàng)建哪些對象,這部分內(nèi)存的分配和回收都是動態(tài)的,GC主要關(guān)注的是這部分內(nèi)存??偠灾?,GC主要進(jìn)行回收的內(nèi)存是JVM中的方法區(qū)和堆。
4、Java的GC什么時候回收垃圾?
在面試中經(jīng)常會碰到這樣一個問題(事實上筆者也碰到過):如何判斷一個對象已經(jīng)死去?
很容易想到的一個答案是:對一個對象添加引用計數(shù)器。每當(dāng)有地方引用它時,計數(shù)器值加1;當(dāng)引用失效時,計數(shù)器值減1.而當(dāng)計數(shù)器的值為0時這個對象就不會再被使用,判斷為已死。是不是簡單又直觀。
然而,很遺憾。這種做法是錯誤的!為什么是錯的呢?事實上,用引用計數(shù)法確實在大部分情況下是一個不錯的解決方案,而在實際的應(yīng)用中也有不少案例,但它卻無法解決對象之間的循環(huán)引用問題。
比如對象A中有一個字段指向了對象B,而對象B中也有一個字段指向了對象A,而事實上他們倆都不再使用,但計數(shù)器的值永遠(yuǎn)都不可能為0,也就不會被回收,然后就發(fā)生了內(nèi)存泄露。
正確的做法應(yīng)該是怎樣呢?
在Java,C#等語言中,比較主流的判定一個對象已死的方法是:可達(dá)性分析(Reachability Analysis).所有生成的對象都是一個稱為"GC Roots"的根的子樹。
從GC Roots開始向下搜索,搜索所經(jīng)過的路徑稱為引用鏈(Reference Chain),當(dāng)一個對象到GC Roots沒有任何引用鏈可以到達(dá)時,就稱這個對象是不可達(dá)的(不可引用的),也就是可以被GC回收了。
無論是引用計數(shù)器還是可達(dá)性分析,判定對象是否存活都與引用有關(guān)!那么,如何定義對象的引用呢?
我們希望給出這樣一類描述:當(dāng)內(nèi)存空間還夠時,能夠保存在內(nèi)存中;如果進(jìn)行了垃圾回收之后內(nèi)存空間仍舊非常緊張,則可以拋棄這些對象。所以根據(jù)不同的需求,給出如下四種引用,根據(jù)引用類型的不同,GC回收時也會有不同的操作:
強(qiáng)引用(Strong Reference):Object obj=new Object();只要強(qiáng)引用還存在,GC永遠(yuǎn)不會回收掉被引用的對象。
軟引用(Soft Reference):描述一些還有用但非必需的對象。在系統(tǒng)將會發(fā)生內(nèi)存溢出之前,會把這些對象列入回收范圍進(jìn)行二次回收(即系統(tǒng)將會發(fā)生內(nèi)存溢出了,才會對他們進(jìn)行回收)
弱引用(Weak Reference):程度比軟引用還要弱一些。這些對象只能生存到下次GC之前。當(dāng)GC工作時,無論內(nèi)存是否足夠都會將其回收(即只要進(jìn)行GC,就會對他們進(jìn)行回收。)
虛引用(Phantom Reference):一個對象是否存在虛引用,完全不會對其生存時間構(gòu)成影響。關(guān)于方法區(qū)中需要回收的是一些廢棄的常量和無用的類。
1.廢棄的常量的回收。這里看引用計數(shù)就可以了。沒有對象引用該常量就可以放心的回收了。
2.無用的類的回收。什么是無用的類呢?
A.該類所有的實例都已經(jīng)被回收。也就是Java堆中不存在該類的任何實例;
B加載該類的ClassLoader已經(jīng)被回收;
C.該類對應(yīng)的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。
總而言之:對于堆中的對象,主要用可達(dá)性分析判斷一個對象是否還存在引用,如果該對象沒有任何引用就應(yīng)該被回收。而根據(jù)我們實際對引用的不同需求,又分成了4種引用,每種引用的回收機(jī)制也是不同的。
對于方法區(qū)中的常量和類,當(dāng)一個常量沒有任何對象引用它,它就可以被回收了。而對于類,如果可以判定它為無用類,就可以被回收了。
5、通過10個示例來初步認(rèn)識Java8中的lambda表達(dá)式
用lambda表達(dá)式實現(xiàn)Runnable
// Java 8 之前: new Thread(new Runnable(){ @Override public void run(){ System.out.println("Before Java8, too much code for too little to do"); }}).start(); //Java 8 方式: new Thread(()->System.out.println("In Java8, Lambda expression rocks !!")).start();
輸出:
too much code,for too little to do Lambda expression rocks!!
這個例子向我們展示了Java 8 lambda表達(dá)式的語法。你可以使用lambda寫出如下代碼:
(params) -> expression (params) -> statement (params) -> { statements }
例如,如果你的方法不對參數(shù)進(jìn)行修改、重寫,只是在控制臺打印點東西的話,那么可以這樣寫:
() -> System.out.println("Hello Lambda Expressions");
如果你的方法接收兩個參數(shù),那么可以寫成如下這樣:
(int even, int odd) -> even + odd
順便提一句,通常都會把lambda表達(dá)式內(nèi)部變量的名字起得短一些。這樣能使代碼更簡短,放在同一行。所以,在上述代碼中,變量名選用a、b或者x、y會比even、odd要好。
使用Java 8 lambda表達(dá)式進(jìn)行事件處理
如果你用過Swing API編程,你就會記得怎樣寫事件監(jiān)聽代碼。這又是一個舊版本簡單匿名類的經(jīng)典用例,但現(xiàn)在可以不這樣了。你可以用lambda表達(dá)式寫出更好的事件監(jiān)聽代碼,如下所示:
// Java 8 之前: JButton show = new JButton("Show"); show.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("Event handling without lambda expression is boring"); } }); // Java 8 方式: show.addActionListener((e) -> { System.out.println("Light, Camera, Action !! Lambda expressions Rocks"); });
使用Java 8 lambda表達(dá)式進(jìn)行事件處理 使用lambda表達(dá)式對列表進(jìn)行迭代
如果你使過幾年Java,你就知道針對集合類,最常見的操作就是進(jìn)行迭代,并將業(yè)務(wù)邏輯應(yīng)用于各個元素,例如處理訂單、交易和事件的列表。
由于Java是命令式語言,Java 8之前的所有循環(huán)代碼都是順序的,即可以對其元素進(jìn)行并行化處理。如果你想做并行過濾,就需要自己寫代碼,這并不是那么容易。
通過引入lambda表達(dá)式和默認(rèn)方法,將做什么和怎么做的問題分開了,這意味著Java集合現(xiàn)在知道怎樣做迭代,并可以在API層面對集合元素進(jìn)行并行處理。
下面的例子里,我將介紹如何在使用lambda或不使用lambda表達(dá)式的情況下迭代列表。你可以看到列表現(xiàn)在有了一個forEach()方法,它可以迭代所有對象,并將你的lambda代碼應(yīng)用在其中。
// Java 8 之前: List features = Arrays.asList("Lambdas", "Default Method", "Stream API","Date and Time API"); for (String feature : features) { System.out.println(feature); } // Java 8 之后: List features = Arrays.asList("Lambdas", "Default Method", "Stream API","Date and Time API"); features.forEach(n -> System.out.println(n)); // 使用 Java 8 的方法引用更方便,方法引用由::雙冒號操作符標(biāo)示, // 看起來像 C++的作用域解析運(yùn)算符 features.forEach(System.out::println);
輸出:
Lambdas Default Method Stream API Date and Time API
列表循環(huán)的最后一個例子展示了如何在Java 8中使用方法引用(method reference)。你可以看到C++里面的雙冒號、范圍解析操作符現(xiàn)在在Java 8中用來表示方法引用。
使用lambda表達(dá)式和函數(shù)式接口Predicate
除了在語言層面支持函數(shù)式編程風(fēng)格,Java 8也添加了一個包,叫做java.util.function。它包含了很多類,用來支持Java的函數(shù)式編程。其中一個便是Predicate,使用java.util.function.Predicate函數(shù)式接口以及l(fā)ambda表達(dá)式,可以向API方法添加邏輯,用更少的代碼支持