Android 面試之 Java 篇二

本文出自 Eddy Wiki ,轉載請注明出處:http://eddy.wiki/interview-java.html

本文收集整理了 Android 面試中會遇到與 Java 知識相關的簡述題。

容器

Java 集合框架

img
img

參考:

Java集合框架

列舉 Java 的集合和它們的繼承關系

Collection包結構,與Collections的區別。

Collection是一個接口,它是Set、List等容器的父接口;

Collections是一個工具類,提供了一系列的靜態方法來輔助容器操作,這些方法包括對容器的搜索、排序、線程安全化等等。

List, Set, Map是否繼承自Collection接口?

List和Set是,Map不是。

Set和List的區別

  1. Set接口存儲的是無序的、不重復的數據。
  2. List接口存儲的是有序的、可以重復的數據。
  3. Set檢索效率低下,刪除和插入效率高,插入和刪除不會引起元素位置改變 ,實現類有HashSet、TreeSet。
  4. List查找元素效率高,插入刪除效率低,因為會引起其他元素位置改變,實現類有ArrayList、LinkedList、Vector。List和數組類似,可以動態增長,根據實際存儲的數據的長度自動增長List的長度。

hashCode方法的作用

對于包含容器類型的程序設計語言來說,基本上都會涉及到hashCode。在Java中也一樣,hashCode方法的主要作用是為了配合基于散列的集合一起正常運行,這樣的散列集合包括HashSet、HashMap以及HashTable。

為什么這么說呢?考慮一種情況,當向集合中插入對象時,如何判別在集合中是否已經存在該對象了?(注意:集合中不允許重復的元素存在)

也許大多數人都會想到調用equals方法來逐個進行比較,這個方法確實可行。但是如果集合中已經存在一萬條數據或者更多的數據,如果采用equals方法去逐一比較,效率必然是一個問題。此時hashCode方法的作用就體現出來了,當集合要添加新的對象時,先調用這個對象的hashCode方法,得到對應的hashcode值,實際上在HashMap的具體實現中會用一個table保存已經存進去的對象的hashcode值,如果table中沒有該hashcode值,它就可以直接存進去,不用再進行任何比較了;如果存在該hashcode值, 就調用它的equals方法與新元素進行比較,相同的話就不存了,不相同就散列其它的地址,所以這里存在一個沖突解決的問題,這樣一來實際調用equals方法的次數就大大降低了,說通俗一點:Java中的hashCode方法就是根據一定的規則將與對象相關的信息(比如對象的存儲地址,對象的字段等)映射成一個數值,這個數值稱作為散列值。下面這段代碼是java.util.HashMap的中put方法的具體實現:

public V put(K key, V value) {
  if (key == null)
    return putForNullKey(value);
  int hash = hash(key.hashCode());
  int i = indexFor(hash, table.length);
  for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    Object k;
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
      V oldValue = e.value;
      e.value = value;
      e.recordAccess(this);
      return oldValue;
    }
  }

  modCount++;
  addEntry(hash, key, value, i);
  return null;
}

put方法是用來向HashMap中添加新的元素,從put方法的具體實現可知,會先調用hashCode方法得到該元素的hashCode值,然后查看table中是否存在該hashCode值,如果存在則調用equals方法重新確定是否存在該元素,如果存在,則更新value值,否則將新的元素添加到HashMap中。從這里可以看出,hashCode方法的存在是為了減少equals方法的調用次數,從而提高程序效率。

hashCode返回的就是對象的存儲地址,事實上這種看法是不全面的,確實有些JVM在實現時是直接返回對象的存儲地址,但是大多時候并不是這樣,只能說可能存儲地址有一定關聯。

因此有人會說,可以直接根據hashcode值判斷兩個對象是否相等嗎?肯定是不可以的,因為不同的對象可能會生成相同的hashcode值。雖然不能根據hashcode值判斷兩個對象是否相等,但是可以直接根據hashcode值判斷兩個對象不等,如果兩個對象的hashcode值不等,則必定是兩個不同的對象。如果要判斷兩個對象是否真正相等,必須通過equals方法。

  • 如果equals方法得到的結果為true,則兩個對象的hashcode值必定相等;
  • 如果equals方法得到的結果為false,則兩個對象的hashcode值不一定不同;
  • 如果兩個對象的hashcode值不等,則equals方法得到的結果必定為false;
  • 如果兩個對象的hashcode值相等,則equals方法得到的結果未知。

當x.equals(y)等于true時,x.hashCode()與y.hashCode()可以不相等,這句話對不對?

如果equals方法得到的結果為true,則兩個對象的hashcode值必定相等;

Java 中 == 和 equals 和 hashCode 的區別

  1. '=='是用來比較兩個變量(基本類型和對象類型)的值是否相等的, 如果兩個變量是基本類型的,那很容易,直接比較值就可以了。如果兩個變量是對象類型的,那么它還是比較值,只是它比較的是這兩個對象在棧中的引用(即地址)。 對象是放在堆中的,棧中存放的是對象的引用(地址)。由此可見'=='是對棧中的值進行比較的。如果要比較堆中對象的內容是否相同,那么就要重寫equals方法了。
  2. Object類中的equals方法就是用'=='來比較的,所以如果沒有重寫equals方法,equals和==是等價的。 通常我們會重寫equals方法,讓equals比較兩個對象的內容,而不是比較對象的引用(地址)因為往往我們覺得比較對象的內容是否相同比比較對象的引用(地址)更有意義。
  3. Object類中的hashCode是返回對象在內存中地址轉換成的一個int值(可以就當做地址看)。所以如果沒有重寫hashCode方法,任何對象的hashCode都是不相等的。通常在集合類的時候需要重寫hashCode方法和equals方法,因為如果需要給集合類(比如:HashSet)添加對象,那么在添加之前需要查看給集合里是否已經有了該對象,比較好的方式就是用hashCode。
  4. 注意的是String、Integer、Boolean、Double等這些類都重寫了equals和hashCode方法,這兩個方法是根據對象的內容來比較和計算hashCode的。(詳細可以查看jdk下的String.java源代碼),所以只要對象的基本類型值相同,那么hashcode就一定相同。
  5. equals()相等的兩個對象,hashcode()一般是相等的,最好在重寫equals()方法時,重寫hashcode()方法; equals()不相等的兩個對象,卻并不能證明他們的hashcode()不相等。換句話說,equals()方法不相等的兩個對象,hashcode()有可能相等。 反過來:hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。在object類中,hashcode()方法是本地方法,返回的是對象的引用(地址值),而object類中的equals()方法比較的也是兩個對象的引用(地址值),如果equals()相等,說明兩個對象地址值也相等,當然hashcode()也就相等了。

有許多人學了很長時間的Java,但一直不明白hashCode方法的作用,我來解釋一下吧。首先,想要明白hashCode的作用,你必須要先知道Java中的集合。

總的來說,Java中的集合(Collection)有兩類,一類是List,再有一類是Set。你知道它們的區別嗎?前者集合內的元素是有序的,元素可以重復;后者元素無序,但元素不可重復。

那么這里就有一個比較嚴重的問題了:要想保證元素不重復,可兩個元素是否重復應該依據什么來判斷呢?這就是Object.equals方法了。但是,如果每增加一個元素就檢查一次,那么當元素很多時,后添加到集合中的元素比較的次數就非常多了。也就是說,如果集合中現在已經有1000個元素,那么第1001個元素加入集合時,它就要調用1000次equals方法。這顯然會大大降低效率。

于是,Java采用了哈希表的原理。哈希(Hash)實際上是個人名,由于他提出一哈希算法的概念,所以就以他的名字命名了。哈希算法也稱為散列算法,是將數據依特定算法直接指定到一個地址上。如果詳細講解哈希算法,那需要更多的文章篇幅,我在這里就不介紹了。初學者可以這樣理解,hashCode方法實際上返回的就是對象存儲的物理地址(實際可能并不是)。

這樣一來,當集合要添加新的元素時,先調用這個元素的hashCode方法,就一下子能定位到它應該放置的物理位置上。如果這個位置上沒有元素,它就可以直接存儲在這個位置上,不用再進行任何比較了;如果這個位置上已經有元素了,就調用它的equals方法與新元素進行比較,相同的話就不存了,不相同就散列其它的地址。所以這里存在一個沖突解決的問題。這樣一來實際調用equals方法的次數就大大降低了,幾乎只需要一兩次。 所以,Java對于eqauls方法和hashCode方法是這樣規定的:

  1. 如果兩個對象相同,那么它們的hashCode值一定要相同;
  2. 如果兩個對象的hashCode相同,它們并不一定相同。

上面說的對象相同指的是用eqauls方法比較。 你當然可以不按要求去做了,但你會發現,相同的對象可以出現在Set集合中。同時,增加新元素的效率會大大下降。

Set里的元素是不能重復的,那么用什么方法來區分重復與否呢? 是用==還是equals()? 它們有何區別?

使用equals()區分。

==是用來判斷兩者是否是同一對象(同一事物),而equals是用來判斷是否引用同一個對象。

set里面存放的是對象的引用,所以當兩個元素只要滿足了equals()時就已經指向同一個對象,也就出現了重復元素。所以應該用equals()來判斷。

多線程環境中安全使用集合API

在集合API中,最初設計的Vector和Hashtable是多線程安全的。例如:對于Vector來說,用來添加和刪除元素的方法是同步的。如果只有一個線程與Vector的實例交互,那么,要求獲取和釋放對象鎖便是一種浪費,另外在不必要的時候如果濫用同步化,也有可能會帶來死鎖。因此,對于更改集合內容的方法,沒有一個是同步化的。集合本質上是非多線程安全的,當多個線程與集合交互時,為了使它多線程安全,必須采取額外的措施。

在Collections類中有多個靜態方法,它們可以獲取通過同步方法封裝非同步集合而得到的集合:

public static Collection synchronizedCollention(Collection c)

public static List synchronizedList(list l)

public static Map synchronizedMap(Map m)

public static Set synchronizedSet(Set s)

public static SortedMap synchronizedSortedMap(SortedMap sm)

public static SortedSet synchronizedSortedSet(SortedSet ss)

這些方法基本上返回具有同步集合方法版本的新類。比如,為了創建多線程安全且由ArrayList支持的List,可以使用如下代碼:

List list = Collection.synchronizedList(new ArrayList());

注意,ArrayList實例馬上封裝起來,不存在對未同步化ArrayList的直接引用(即直接封裝匿名實例)。這是一種最安全的途徑。如果另一個線程要直接引用ArrayList實例,它可以執行非同步修改。

下面給出一段多線程中安全遍歷集合元素的示例。我們使用Iterator逐個掃描List中的元素,在多線程環境中,當遍歷當前集合中的元素時,一般希望阻止其他線程添加或刪除元素。安全遍歷的實現方法如下:

import java.util.*;  

public class SafeCollectionIteration extends Object {  
    public static void main(String[] args) {  
        //為了安全起見,僅使用同步列表的一個引用,這樣可以確保控制了所有訪問  
        //集合必須同步化,這里是一個List  
        List wordList = Collections.synchronizedList(new ArrayList());  

        //wordList中的add方法是同步方法,會獲取wordList實例的對象鎖  
        wordList.add("Iterators");  
        wordList.add("require");  
        wordList.add("special");  
        wordList.add("handling");  

        //獲取wordList實例的對象鎖,  
        //迭代時,阻塞其他線程調用add或remove等方法修改元素  
        synchronized ( wordList ) {  
            Iterator iter = wordList.iterator();  
            while ( iter.hasNext() ) {  
                String s = (String) iter.next();  
                System.out.println("found string: " + s + ", length=" +s.length());  
            }  
        }  
    }  
}  

這里需要注意的是:在Java語言中,大部分的線程安全類都是相對線程安全的,它能保證對這個對象單獨的操作時線程安全的,我們在調用的時候不需要額外的保障措施,但是對于一些特定的連續調用,就可能需要在調用端使用額外的同步手段來保證調用的正確性。例如Vector、HashTable、Collections的synchronizedXxxx()方法包裝的集合等。

集合類的源碼分析

參考:

ArrayList源碼剖析

LinkedList源碼剖析

Vector源碼剖析

HashMap源碼剖析

LinkedHashMap源碼剖析

HashTable源碼剖析

HashMap的底層實現

參考:

https://github.com/GeniusVJR/LearningNotes/blob/master/Part2/JavaSE/HashMap%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90.md

HashMap 的實現原理

  • HashMap概述:HashMap是基于哈希表的Map接口的非同步實現。此實現提供所有可選的映射操作,并允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恒久不變。
  • HashMap的數據結構:在java編程語言中,最基本的結構就是兩種,一個是數組,另外一個是模擬指針(引用),所有的數據結構都可以用這兩個基本結構來構造的,HashMap也不例外。HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。

從上圖中可以看出,HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個HashMap的時候,就會初始化一個數組。

容器類之間的區別

HashMap 和 HashTable 的區別

  1. 繼承不同。
public class Hashtable extends Dictionary implements Map
public class HashMap  extends AbstractMap implements Map
  1. Hashtable 中的方法是同步的,而HashMap中的方法在缺省情況下是非同步的。在多線程并發的環境下,可以直接使用Hashtable,但是要使用HashMap的話就要自己增加同步處理了。
  2. Hashtable中,key和value都不允許出現null值。在HashMap中,null可以作為鍵,這樣的鍵只有一個;可以有一個或多個鍵所對應的值為null。當get()方法返回null值時,即可以表示 HashMap中沒有該鍵,也可以表示該鍵所對應的值為null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵, 而應該用containsKey()方法來判斷。
  3. 兩個遍歷方式的內部實現上不同。Hashtable、HashMap都使用了 Iterator。而由于歷史原因,Hashtable還使用了Enumeration的方式 。
  4. 哈希值的使用不同,HashTable直接使用對象的hashCode。而HashMap重新計算hash值。
  5. Hashtable和HashMap它們兩個內部實現方式的數組的初始大小和擴容的方式。HashTable中hash數組默認大小是11,增加的方式是 old*2+1。HashMap中hash數組的默認大小是16,而且一定是2的指數。

ArrayMap 和 HashMap 的區別

  • 存儲方式不一樣。ArrayMap 使用兩個數組來存儲,HashMap 則使用一個鏈表數組來存儲。
  • 擴容處理方法不一樣。ArrayMap 使用 System.arraycopy() copy 數組,而 HashMap 使用 new HashMapEntry 重新創建數組。
  • ArrayMap 提供了數組收縮的功能,在clear或remove后,會重新收縮數組,節省空間。
  • ArrayMap 采用二分法查找。

ArrayList Vector LinkedList 三者的區別

  • 三者都實現了List 接口;但 LinkedList 還實現了 Queue 接口。
  • ArrayList 和 Vector 使用數組存儲數據;LinkedList 使用雙向鏈表存儲數據。
  • ArrayList 和 LinkedList 是非線程安全的;Vector 是線程安全的。
  • ArrayList 數據增長時空間增長50%;而 Vector 是增長1倍;
  • LinkedList 在添加、刪除元素時具有更好的性能,但讀取性能要低一些。
  • LinkedList 與 ArrayList 最大的區別是 LinkedList 更加靈活,并且部分方法的效率比 ArrayList 對應方法的效率要高很多,對于數據頻繁出入的情況下,并且要求操作要足夠靈活,建議使用 LinkedList;對于數組變動不大,主要是用來查詢的情況下,可以使用 ArrayList。

TreeMap、HashMap、LinkedHashMap的區別。

Map主要用于存儲健值對,根據鍵得到值,因此不允許鍵重復(重復了覆蓋了),但允許值重復。

Hashmap 是一個最常用的Map,它根據鍵的HashCode 值存儲數據,根據鍵可以直接獲取它的值,具有很快的訪問速度,遍歷時,取得數據的順序是完全隨機的。HashMap最多只允許一條記錄的鍵為Null,允許多條記錄的值為Null。HashMap不支持線程的同步,即任一時刻可以有多個線程同時寫HashMap,可能會導致數據的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。

Hashtable與 HashMap類似,它繼承自Dictionary類,不同的是:它不允許記錄的鍵或者值為空;它支持線程的同步,即任一時刻只有一個線程能寫Hashtable,因此也導致了 Hashtable在寫入時會比較慢。

LinkedHashMap保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的。也可以在構造時用帶參數,按照應用次數排序。在遍歷的時候會比HashMap慢,不過有種情況例外,當HashMap容量很大,實際數據較少時,遍歷起來可能會比LinkedHashMap慢,因為LinkedHashMap的遍歷速度只和實際數據有關,和容量無關,而HashMap的遍歷速度和他的容量有關。

TreeMap實現SortMap接口,能夠把它保存的記錄根據鍵排序,默認是按鍵值的升序排序,也可以指定排序的比較器,當用Iterator 遍歷TreeMap時,得到的記錄是排過序的。

一般情況下,我們用的最多的是HashMap。HashMap里面存入的鍵值對在取出的時候是隨機的,它根據鍵的HashCode值存儲數據,根據鍵可以直接獲取它的值,具有很快的訪問速度。在Map 中插入、刪除和定位元素,HashMap 是最好的選擇。

TreeMap取出來的是排序后的鍵值對。但如果您要按自然順序或自定義順序遍歷鍵,那么TreeMap會更好。

LinkedHashMap 是HashMap的一個子類,如果需要輸出的順序和輸入的相同,那么用LinkedHashMap可以實現,它還可以按讀取順序來排列,像連接池中可以應用。

參考:

HashMap,LinkedHashMap,TreeMap的區別

Map、Set、List、Queue、Stack的特點與用法。

Collection 是對象集合

Collection 有兩個子接口 List 和 Set

List 可以通過下標 (1,2..) 來取得值,值可以重復

而 Set 只能通過游標來取值,并且值是不能重復的

ArrayList , Vector , LinkedList 是 List 的實現類

ArrayList 是線程不安全的, Vector 是線程安全的,這兩個類底層都是由數組實現的

LinkedList 是線程不安全的,底層是由鏈表實現的

Map 是鍵值對集合

HashTable 和 HashMap 是 Map 的實現類

HashTable 是線程安全的,不能存儲 null 值

HashMap 不是線程安全的,可以存儲 null 值

Stack類:繼承自Vector,實現一個后進先出的棧。提供了幾個基本方法,push、pop、peak、empty、search等。

Queue接口:提供了幾個基本方法,offer、poll、peek等。已知實現類有LinkedList、PriorityQueue等。

參考:

Map、Set、List、Queue、Stack的特點與用法

內存相關知識

參考:

Android內存泄漏總結

Java 內存分配策略

Java 程序運行時的內存分配策略有三種,分別是靜態分配、棧式分配和堆式分配,對應的,三種存儲策略使用的內存空間主要分別是靜態存儲區(也稱方法區)、棧區和堆區。

  • 靜態存儲區(方法區):主要存放靜態數據、全局 static 數據和常量。這塊內存在程序編譯時就已經分配好,并且在程序整個運行期間都存在。
  • 棧區 :當方法被執行時,方法體內的局部變量(其中包括基礎數據類型、對象的引用)都在棧上創建,并在方法執行結束時這些局部變量所持有的內存將會自動被釋放。因為棧內存分配運算內置于處理器的指令集中,效率很高,但是分配的內存容量有限。
  • 堆區 : 又稱動態內存分配,通常就是指在程序運行時直接 new 出來的內存,也就是對象的實例。這部分內存在不使用時將會由 Java 垃圾回收器來負責回收。

棧與堆的區別

在方法體內定義的(局部變量)一些基本類型的變量和對象的引用變量都是在方法的棧內存中分配的。當在一段方法塊中定義一個變量時,Java 就會在棧中為該變量分配內存空間,當超過該變量的作用域后,該變量也就無效了,分配給它的內存空間也將被釋放掉,該內存空間可以被重新使用。

堆內存用來存放所有由 new 創建的對象(包括該對象其中的所有成員變量)和數組。在堆中分配的內存,將由 Java 垃圾回收器來自動管理。在堆中產生了一個數組或者對象后,還可以在棧中定義一個特殊的變量,這個變量的取值等于數組或者對象在堆內存中的首地址,這個特殊的變量就是我們上面說的引用變量。我們可以通過這個引用變量來訪問堆中的對象或者數組。

舉個例子:

public class Sample {
    int s1 = 0;
    Sample mSample1 = new Sample();

    public void method() {
        int s2 = 1;
        Sample mSample2 = new Sample();
    }
}

Sample mSample3 = new Sample();

Sample 類的局部變量 s2 和引用變量 mSample2 都是存在于棧中,但 mSample2 指向的對象是存在于堆上的。mSample3 指向的對象實體存放在堆上,包括這個對象的所有成員變量 s1 和 mSample1,而它自己存在于棧中。

結論:

  1. 局部變量的基本數據類型和引用存儲于棧中,引用的對象實體存儲于堆中。因為它們屬于方法中的變量,生命周期隨方法而結束。
  2. 成員變量全部存儲于堆中(包括基本數據類型,引用和引用的對象實體)。因為它們屬于類,類對象終究是要被new出來使用的。

heap(堆) 和 stack(棧) 有什么區別。

Java 的堆是一個運行時數據區,類的(對象從中分配空間。這些對象通過new、newarray、anewarray和multianewarray等指令建立,它們不需要程序代碼來顯式的釋放。堆是由垃圾回收來負責的,堆的優勢是可以動態地分配內存大小,生存期也不必事先告訴編譯器,因為它是在運行時動態分配內存的,Java的垃圾收集器會自動收走這些不再使用的數據。但缺點是,由于要在運行時動態分配內存,存取速度較慢。

棧的優勢是,存取速度比堆要快,僅次于寄存器,棧數據可以共享。但缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。棧中主要存放一些基本類型的變量(,int, short, long, byte, float, double, boolean, char)和對象句柄。棧有一個很重要的特殊性,就是存在棧中的數據可以共享。

  • Stack存取速度僅次于寄存器,存儲效率比heap高,可共享存儲數據,但是其中數據的大小和生存期必須在運行前確定。
  • Heap是運行時可動態分配的數據區,從速度看比Stack慢,Heap里面的數據不共享,大小和生存期都可以在運行時再確定。

Java是如何管理內存

Java的內存管理就是對象的分配和釋放問題。在 Java 中,程序員需要通過關鍵字 new 為每個對象申請內存空間 (基本類型除外),所有的對象都在堆 (Heap)中分配空間。另外,對象的釋放是由 GC 決定和執行的。在 Java 中,內存的分配是由程序完成的,而內存的釋放是由 GC 完成的,這種收支兩條線的方法確實簡化了程序員的工作。但同時,它也加重了JVM的工作。這也是 Java 程序運行速度較慢的原因之一。因為,GC 為了能夠正確釋放對象,GC 必須監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC 都需要進行監控。監視對象狀態是為了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用。

為了更好理解 GC 的工作原理,我們可以將對象考慮為有向圖的頂點,將引用關系考慮為圖的有向邊,有向邊從引用者指向被引對象。另外,每個線程對象可以作為一個圖的起始頂點,例如大多程序從 main 進程開始執行,那么該圖就是以 main 進程頂點開始的一棵根樹。在這個有向圖中,根頂點可達的對象都是有效對象,GC將不回收這些對象。如果某個對象 (連通子圖)與這個根頂點不可達(注意,該圖為有向圖),那么我們認為這個(這些)對象不再被引用,可以被 GC 回收。以下,我們舉一個例子說明如何用有向圖表示內存管理。對于程序的每一個時刻,我們都有一個有向圖表示JVM的內存分配情況。以下右圖,就是左邊程序運行到第6行的示意圖。

Java使用有向圖的方式進行內存管理,可以消除引用循環的問題,例如有三個對象,相互引用,只要它們和根進程不可達的,那么GC也是可以回收它們的。這種方式的優點是管理內存的精度很高,但是效率較低。另外一種常用的內存管理技術是使用計數器,例如COM模型采用計數器方式管理構件,它與有向圖相比,精度行低(很難處理循環引用的問題),但執行效率很高。

GC引用計數法循環引用會發生什么情況

什么是Java中的內存泄露

在Java中,內存泄漏就是存在一些被分配的對象,這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連;其次,這些對象是無用的,即程序以后不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定為Java中的內存泄漏,這些對象不會被GC所回收,然而它卻占用內存。

在C++中,內存泄漏的范圍更大一些。有些對象被分配了內存空間,然后卻不可達,由于C++中沒有GC,這些內存將永遠收不回來。在Java中,這些不可達的對象都由GC負責回收,因此程序員不需要考慮這部分的內存泄露。

通過分析,我們得知,對于C++,程序員需要自己管理邊和頂點,而對于Java程序員只需要管理邊就可以了(不需要管理頂點的釋放)。通過這種方式,Java提高了編程的效率。

因此,通過以上分析,我們知道在Java中也有內存泄漏,但范圍比C++要小一些。因為Java從語言上保證,任何對象都是可達的,所有的不可達對象都由GC管理。

對于程序員來說,GC基本是透明的,不可見的。雖然,我們只有幾個函數可以訪問GC,例如運行GC的函數System.gc(),但是根據Java語言規范定義, 該函數不保證JVM的垃圾收集器一定會執行。因為,不同的JVM實現者可能使用不同的算法管理GC。通常,GC的線程的優先級別較低。JVM調用GC的策略也有很多種,有的是內存使用到達一定程度時,GC才開始工作,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但通常來說,我們不需要關心這些。除非在一些特定的場合,GC的執行影響應用程序的性能,例如對于基于Web的實時系統,如網絡游戲等,用戶不希望GC突然中斷應用程序執行而進行垃圾回收,那么我們需要調整GC的參數,讓GC能夠通過平緩的方式釋放內存,例如將垃圾回收分解為一系列的小步驟執行,Sun提供的HotSpot JVM就支持這一特性。

同樣給出一個 Java 內存泄漏的典型例子,

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;   
}

在這個例子中,我們循環申請Object對象,并將所申請的對象放入一個 Vector 中,如果我們僅僅釋放引用本身,那么 Vector 仍然引用該對象,所以這個對象對 GC 來說是不可回收的。因此,如果對象加入到Vector 后,還必須從 Vector 中刪除,最簡單的方法就是將 Vector 對象設置為 null。

詳解Java中的內存泄漏

Java內存回收機制

不論哪種語言的內存分配方式,都需要返回所分配內存的真實地址,也就是返回一個指針到內存塊的首地址。Java中對象是采用new或者反射的方法創建的,這些對象的創建都是在堆(Heap)中分配的,所有對象的回收都是由Java虛擬機通過垃圾回收機制完成的。GC為了能夠正確釋放對象,會監控每個對象的運行狀況,對他們的申請、引用、被引用、賦值等狀況進行監控,Java會使用有向圖的方法進行管理內存,實時監控對象是否可以達到,如果不可到達,則就將其回收,這樣也可以消除引用循環的問題。在Java語言中,判斷一個內存空間是否符合垃圾收集標準有兩個:一個是給對象賦予了空值null,以下再沒有調用過,另一個是給對象賦予了新值,這樣重新分配了內存空間。

Java內存泄漏引起的原因

內存泄漏是指無用對象(不再使用的對象)持續占有內存或無用對象的內存得不到及時釋放,從而造成內存空間的浪費稱為內存泄漏。內存泄露有時不嚴重且不易察覺,這樣開發者就不知道存在內存泄露,但有時也會很嚴重,會提示你Out of memory。

Java內存泄漏的根本原因是什么呢?長生命周期的對象持有短生命周期對象的引用就很可能發生內存泄漏,盡管短生命周期對象已經不再需要,但是因為長生命周期持有它的引用而導致不能被回收,這就是Java中內存泄漏的發生場景。具體主要有如下幾大類:

  1. 靜態集合類引起內存泄漏。

像HashMap、Vector等的使用最容易出現內存泄露,這些靜態變量的生命周期和應用程序一致,他們所引用的所有的對象Object也不能被釋放,因為他們也將一直被Vector等引用著。 例如:

Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
  Object o = new Object();
  v.add(o);
  o = null;
}

在這個例子中,循環申請Object 對象,并將所申請的對象放入一個Vector 中,如果僅僅釋放引用本身(o=null),那么Vector 仍然引用該對象,所以這個對象對GC 來說是不可回收的。因此,如果對象加入到Vector 后,還必須從Vector 中刪除,最簡單的方法就是將Vector對象設置為null。

  1. 當集合里面的對象屬性被修改后,再調用remove()方法時不起作用。例如:
public static void main(String[] args) {
  Set<Person> set = new HashSet<Person>();
  Person p1 = new Person("唐僧","pwd1",25);
  Person p2 = new Person("孫悟空","pwd2",26);
  Person p3 = new Person("豬八戒","pwd3",27);
  set.add(p1);
  set.add(p2);
  set.add(p3);
  System.out.println("總共有:"+set.size()+" 個元素!"); //結果:總共有:3 個元素!
  p3.setAge(2); //修改p3的年齡,此時p3元素對應的hashcode值發生改變

  set.remove(p3); //此時remove不掉,造成內存泄漏

  set.add(p3); //重新添加,居然添加成功
  System.out.println("總共有:"+set.size()+" 個元素!"); //結果:總共有:4 個元素!
  for (Person person : set) {
    System.out.println(person);
  }
}
  1. 監聽器

在java 編程中,我們都需要和監聽器打交道,通常一個應用當中會用到很多監聽器,我們會調用一個控件的諸如addXXXListener()等方法來增加監聽器,但往往在釋放對象的時候卻沒有記住去刪除這些監聽器,從而增加了內存泄漏的機會。

  1. 各種連接

比如數據庫連接(dataSourse.getConnection()),網絡連接(socket)和io連接,除非其顯式的調用了其close()方法將其連接關閉,否則是不會自動被GC 回收的。對于Resultset 和Statement 對象可以不進行顯式回收,但Connection 一定要顯式回收,因為Connection 在任何時候都無法自動回收,而Connection一旦回收,Resultset 和Statement 對象就會立即為NULL。但是如果使用連接池,情況就不一樣了,除了要顯式地關閉連接,還必須顯式地關閉Resultset Statement 對象(關閉其中一個,另外一個也會關閉),否則就會造成大量的Statement 對象無法釋放,從而引起內存泄漏。這種情況下一般都會在try里面去的連接,在finally里面釋放連接。

  1. 內部類和外部模塊的引用

內部類的引用是比較容易遺忘的一種,而且一旦沒釋放可能導致一系列的后繼類對象沒有釋放。此外程序員還要小心外部模塊不經意的引用,例如程序員A 負責A 模塊,調用了B 模塊的一個方法如:public void registerMsg(Object b);這種調用就要非常小心了,傳入了一個對象,很可能模塊B就保持了對該對象的引用,這時候就需要注意模塊B 是否提供相應的操作去除引用。

  1. 單例模式

不正確使用單例模式是引起內存泄漏的一個常見問題,單例對象在初始化后將在JVM的整個生命周期中存在(以靜態變量的方式),如果單例對象持有外部的引用,那么這個對象將不能被JVM正常回收,導致內存泄漏,考慮下面的例子:

class A {
  public A(){
    B.getInstance().setA(this);
  }
  // ...
}

//B類采用單例模式
class B {
  private A a;
  private static B instance=new B();
  public B(){
    
  }
  public static B getInstance(){
    return instance;
  }
  public void setA(A a){
    this.a=a;
  }
  // ...
} 

顯然B采用singleton模式,它持有一個A對象的引用,而這個A類的對象將不能被回收。想象下如果A是個比較復雜的對象或者集合類型會發生什么情況。

Java內存回收機制,GC 垃圾回收機制,垃圾回收的優點和原理,并說出3種回收機制。

  1. java語言最顯著的特點就是引入了垃圾回收機制,它使java程序員在編寫程序時不再考慮內存管理的問題。
  2. 由于有這個垃圾回收機制,java中的對象不再有“作用域”的概念,只有引用的對象才有“作用域”。
  3. 垃圾回收機制有效的防止了內存泄露,可以有效的使用可使用的內存。
  4. 垃圾回收器通常作為一個單獨的低級別的線程運行,在不可預知的情況下對內存堆中已經死亡的或很長時間沒有用過的對象進行清除和回收。
  5. 程序員不能實時的對某個對象或所有對象調用垃圾回收器進行垃圾回收。

垃圾回收機制有:分代復制垃圾回收、標記垃圾回收、增量垃圾回收。

垃圾回收算法

  1. 引用計數法:缺點是無法處理循環引用問題
  2. 標記-清除法:標記所有從根結點開始的可達對象,缺點是會造成內存空間不連續,不連續的內存空間的工作效率低于連續的內存空間,不容易分配內存
  3. 標記-壓縮算法:標記-清除的改進,清除未標記的對象時還將所有的存活對象壓縮到內存的一端,之后,清理邊界所有空間既避免碎片產生,又不需要兩塊同樣大小的內存快,性價比高。適用于老年代。
  4. 復制算法:將內存空間分成兩塊,每次將正在使用的內存中存活對象復制到未使用的內存塊中,之后清除正在使用的內存塊。算法效率高,但是代價是系統內存折半。適用于新生代(存活對象少,垃圾對象多)
  5. 分代

Java 虛擬機的特性

Java 語言的一個非常重要的特點就是與平臺的無關性,而 Java 虛擬機就是實現這一特點的關鍵。一般的高級語言如果要在不同的平臺上運行,至少需要編譯成不同的目標代碼。而引入Java語言虛擬機后,Java語言在不同平臺上運行時不需要重新編譯。Java語言使用模式Java虛擬機屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。Java虛擬機在執行字節碼時,把字節碼解釋成具體平臺上的機器指令執行。

參考:

JVM基礎知識

JVM類加載機制

Java內存區域與內存溢出

那些情況下的對象會被垃圾回收機制處理掉

???Java 垃圾回收機制最基本的做法是分代回收。內存中的區域被劃分成不同的世代,對象根據其存活的時間被保存在對應的世代的區域中。一般的實現是劃分為3個世代:年輕、年老和永久。內存的分配是發生在年輕世代中的。當一個對象存活時間足夠長的時候,它就會被復制到年老世代中。對于不同的世代可以使用不同的垃圾回收算法。進行世代劃分的出發點是對應用中對象存活時間進行研究之后得出的統計規律。一般來說,一個應用中的大部分對象的存活時間都很短。比如局部變量的存活時間就只在方法的執行過程中。基于這一點,對于年輕世代的垃圾回收算法就可以很有針對性。

狀態機

Java的四種引用,強弱軟虛,用到的場景。

JDK1.2之前只有強引用,其他幾種引用都是在JDK1.2之后引入的。

  1. 強引用(Strong Reference)最常用的引用類型,如Object obj = new Object(); 。只要強引用存在則GC時則必定不被回收。
  2. 軟引用(Soft Reference)用于描述還有用但非必須的對象,當堆將發生OOM(Out Of Memory)時則會回收軟引用所指向的內存空間,若回收后依然空間不足才會拋出OOM。一般用于實現內存敏感的高速緩存。當真正對象被標記finalizable以及的finalize()方法調用之后并且內存已經清理, 那么如果SoftReference object還存在就被加入到它的 ReferenceQueue.只有前面幾步完成后,Soft Reference和Weak Reference的get方法才會返回null
  3. 弱引用(Weak Reference)發生GC時必定回收弱引用指向的內存空間。和軟引用加入隊列的時機相同
  4. 虛引用(Phantom Reference)又稱為幽靈引用或幻影引用,虛引用既不會影響對象的生命周期,也無法通過虛引用來獲取對象實例,僅用于在發生GC時接收一個系統通知。當一個對象的finalize方法已經被調用了之后,這個對象的幽靈引用會被加入到隊列中。通過檢查該隊列里面的內容就知道一個對象是不是已經準備要被回收了。虛引用和軟引用、弱引用都不同,它會在內存沒有清理的時候被加入引用隊列。虛引用的建立必須要傳入引用隊列,其他可以沒有。

數組復制

請使用 System.arrayCopy 或 Arrays.copyOf 實現,且在 Java 中后者基于前者實現。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,765評論 18 399
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,366評論 11 349
  • Java8張圖 11、字符串不變性 12、equals()方法、hashCode()方法的區別 13、...
    Miley_MOJIE閱讀 3,731評論 0 11
  • (一)Java部分 1、列舉出JAVA中6個比較常用的包【天威誠信面試題】 【參考答案】 java.lang;ja...
    獨云閱讀 7,142評論 0 62