- 多線程
- 問:進程和線程的區別?
- 問:多線程和多進程的區別?
- 問:並行和並發?
- 問:CPU內存模型?
- 問:說一下並發編程中的3個概念?(原子、可見、有序)
- 問:Java的內存模型以及如何保證三種特性?
- 問:說一下volatile關鍵字?
- 問:說一下Synchronized關鍵字?
- 問:Java中確保線程安全的方法?(Synchronized和Lock、thradlocal和同步,悲觀鎖和樂觀鎖CAS)
- 問:什么是自旋鎖?
- 問:線程的5種狀態?
- 問:什么是守護線程?
- 問:Java實現多線程的方式(有程序/兩種實現的區別)?
- 問:進程間的通信?
- 問:線程間通信方式(wait和notify)?
- 問:說一下線程池?
- 問:sleep和wait異同?
- JVM
多線程
問:進程和線程的區別?
- 進程是運行着的程序,是資源分配的最小單位,進程之間是相互獨立的;
- 線程是進程的單位,一個進程可以有多個線程,像是工廠里的流水線,是一條執行路徑; 線程之間並不完全獨立,共享運行棧和程序計數器,有自己獨立的堆和方法區;
問:多線程和多進程的區別?
- 多進程中占用內存多,而且切換復雜,CPU利用率相對較低;多進程中編程是比較簡單的,調試也比較方便;每個進程之間不會相互影響;
- 多線程中占用內存少,切換簡單,CPU利用率高;多線程的話編程復雜,調試也復雜;一個線程掛掉會導致整個線程掛掉;
問:並行和並發?
- 並發是一段時間內宏觀上多個程序同時運行,主要是多個事情,在同一個時間段內同時發生了;但並不是真正意義上的同時發生,因為CPU速度很快,采用時間切片,任務來回切換,所以看上去好像是同時在運行;
- 並行是在某一個時刻,真正有多個程序在運行,指的是多個事情,在同一時間發生了;並發的多個任務互相搶奪資源,並行的多個任務不會相互搶奪資源;只有多個CPU或一個CPU多核的情況下,才會發生並行,否則,看上去同時發生的,其實都是並發執行;
問:CPU內存模型?
CPU的內存模型:因為CPU的執行速度很快,但是內存的讀寫速度和CPU比起來就慢很多,所以就在CPU里面設計了一個高速緩存;程序在運行的時候,把數據從內存里復制一份到CPU的高速緩存里,然后CPU就直接與高速緩存進行數據讀寫,運算結束后,再將高速緩存中的數據刷到主存中;
這樣在單線程的時候自然是沒有問題,但是當有多個線程操作同樣的數據時,都會把數據復制到自己的高速緩存里,比如線程1把變量+1了,然后把修改后的值刷進主存,線程2的高速緩存里還是原來未修改的,這就造成了數據的臟讀,和數據庫里事務的臟讀差不多;這就是緩存不一致問題:解決的方法主要有兩個:
- 在總線加Lock鎖的方式;CPU和其他部件通信是通過總線進行的,所以可以在總線上加lock鎖,每次只允許有1個CPU對變量操作;但是這樣在鎖的時候其他CPU沒有辦法執行,效率低;
- 緩存一致性協議(MESI協議):保證每個緩存中的使用的共享變量的副本是一致的,當CPU寫數據的時候,if操作的是共享變量(在其他CPU中存在該變量的副本),會發出信號通知其他CPU將該變量的緩存行置為無效,當其他CPU需要讀這個變量的時候,因為緩存行是無效的,所以就會從主存中重新讀;
問:說一下並發編程中的3個概念?(原子、可見、有序)
- 原子性:就是說一組操作要么都執行,要么都不執行;而且在執行的過程中不會被任何因素打斷;
- 可見性;可見性就是說當多個線程訪問共享變量的時候,一個線程修改了變量的值后,其他線程能夠立馬看到;
- 有序性:程序的執行順序是按照代碼的先后的順序執行;
多線程中要保證這三個特性才能使程序不出錯;
指令重排序:在JVM里,真正執行的時候並不是完全按照代碼的先后順序來進行的,它可能會考慮到代碼的優化,為了提高程序運行效率,會發生指令重排序,處理器在執行的時候,會考慮指令之間的數據依賴性,如果指令b會用到指令a的結果,那a一定會在b之前進行;if沒有依賴,那就可以發生指令的重排序;這種重排序在單線程的時候不會影響結果,但是多線程的時候就不一定了;比如下面的指令:
線程A中
{
context = loadContext(); //語句1;
inited = true; //語句2;
}
線程B中
{
if (inited)
fun(context);
}
線程A中的語句1和語句2沒有相互依賴,可以指令重排,if語句2先執行了,這時候線程B開始了,就會拿到一個還沒有初始化的context,這就錯誤了;
問:Java的內存模型以及如何保證三種特性?
在Java中定義了一種內存模型(Java Memory Model,JMM),和整個CPU內存模型基本一致,所有的變量都存在主存中(類似內存),每個線程都有自己的工作內存(類似前面的高速緩存),線程對變量的所有操作都必須在工作內存中,不能直接對主存操作,而且每個線程不能訪問其他線程的工作內存;
- 原子性:在java中對基本數據類型的讀取和賦值是原子性操作,也就是這些操作是不可中斷的,要么執行要么不執行;比如x=10,是原子性操作,但是x++就不是原子性操作,這個語句包括3個操作:先讀取x的值,然后進行
加1,再寫入新值;在java中可以通過synchronized和lock來實現,保證任一時刻只有一個線程執行一段代碼塊,也就保證了原子性; - 可見性:java中是用volatile關鍵字來保證可見性的,if變量被volatile關鍵字修飾,那對這個變量進行操作后值會被立馬更新到主存,同時其他線程讀取到的這個變量的值也設置為無效,重新從內存中讀取,這樣多個線程讀取的都是一個值;synchronized和lock也可以保證可見性,因為每次只能有一個線程拿到鎖然后執行同步代碼,在釋放鎖之前會把修改后的變量刷新到主存中,所以可以保證原子性;
- 有序性:在java中可以通過volatile關鍵字來保證有序性,這其中最典型的就是雙重檢驗鎖的單例模式:我們把instance設置為volitile,因為instance = new Singleton();這並不是一個原子操作,這里面包含3個階段:
memory =allocate(); //1:分配對象的內存空間
ctorInstance(memory); //2:初始化對象
instance =memory; //3:設置instance指向剛分配的內存地址
這3個過程,2和3都依賴1,但是2,3之間沒有依賴,所以可以執行指令重排,if執行過程中變為了1,3,2;線程A可能執行完3,進行了賦值,但是還沒有初始化,那這時候線程B過來了,判斷instance不為null,那就返回使用了,這樣就出錯了;所以使用volatile修飾后就會插入內存屏障,不會執行重排序;
問:說一下volatile關鍵字?
if一個變量被volatile關鍵字修飾了,那其實是有兩個作用:
- 1.保證可見性:不同的線程對這個變量的操作都是可見的,當這個變量的值發生修改后,其他線程能立即看到;
- 2.保證有序性:禁止指令的重排序;
- 需要注意的點是volatile關鍵字並不保證原子性;所以也就不能保證多線程安全
volatile是一種輕量級的同步機制,但是一定注意,volatile不能修飾寫入操作依賴當前值的變量,比如i++,i=i+1;因為不保證原子性;此外,因為volatile屏蔽了JVM中的代碼優化,所以在效率上比較低;
原理:
總之,volatile關鍵字可以保證可見性和有序性,但是不保證原子性,在JVM底層是通過“內存屏障”來實現的,加入volatile關鍵字后,會多出一個lock前綴指令,就相當於一個內存屏障,主要有3個功能:
1.能夠確保指令重排的時候不會把后面的指令排到內存屏障之前,也不會把前面的指令排到內存屏障之后,也就是當執行到內存屏障中這句指令的時候,前面的操作都已經完成了;
2.它會強制將對緩存的修改立即刷入內存;
3.會導致其他cpu對應的緩存行是無效的;
為什么volatile關鍵字不能保證原子性?
java中只有對基本類型變量的賦值和讀取是原子操作,比如i++這個指令,其實是有3個步驟:讀取i的值,執行i+1操作,把i+1賦值給i刷到主存中,比如說AB兩個線程都取出來i,然后A進行了i+1操作,之后被阻塞了,B又進行了i+1的操作,並且將新值賦給i后刷回主存,然后由於緩存一致性會將線程A中的i值變為新值,但是這並不影響A還要把剛才的i+1得到的新值賦給i的過程,所以導致了最后結果出錯;
問:說一下Synchronized關鍵字?
Synchronized關鍵字是用來同步的,主要解決線程安全的問題,能夠保證並發編程中的3個特性:
- 原子性:被Synchronized關鍵字修飾的語句塊內的操作都是原子的;
- 可見性:在釋放鎖之前,能夠把變量刷回主內存中去,保證可見性;
- 有序性:一個變量在一個時刻只允許一個線程對其進行操作;
主要可以用來修飾方法和代碼塊,使它們都變成同步方法和同步代碼塊。JVM底層都是基於進入(enter)和退出(exit)monitor對象來實現的;
- 同步方法:同步方法是隱式的,不需要通過字節碼指令來控制,在方法調用時,JVM從方法常量池中的方法表結構中的ACC_SYNCHRONIZED訪問標志位來區分一個方法是否是同步方法,if這個標志位被設置了,那就先去獲取monitor,if獲取成功了,那再去執行方法,執行結束后再釋放monitor;if獲取不成功,那證明monitor對象被其他線程獲取,然后當前線程阻塞;
- 同步代碼塊:代碼塊是利用monitorenter和monitorexit這兩個字節碼指令實現的,位於同步代碼塊的開始和結束標志位,當執行到monitorenter時,嘗試獲取monitor對象,if沒有加鎖或者被當前線程持有,就把鎖計數器+1,執行到monitorexit時,鎖計數器-1;當鎖計數器為0時,釋放鎖;if獲取monitor對象失敗,線程進入阻塞狀態,直到其他線程釋放鎖;
通過javap 反編譯查看可以發現同步方法是得到ACC_SYNCHRONIZED標志,而對於同步代碼塊則是得到monitorenter和monitorexit指令;
問:Java中確保線程安全的方法?(Synchronized和Lock、thradlocal和同步,悲觀鎖和樂觀鎖CAS)
java中主要有兩種思路來解決:
- 1.發生線程不安全因為有共享資源,所以可以讓線程也有資源,不用去共享資源,可以使用ThreadLocal類,為每一個線程維護一個副本變量,從而讓線程不再去競爭共享資源;
在每個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這就是用來存儲實際變量的副本的,key就是當前實例變量,value就是變量副本;
-
2.在java里處理線程安全最為常見的手段就是同步:也就是說在多線程訪問共享數據的時候,要保證數據在同一時刻只能被一個線程占用;這里又分為兩種方法;
- 阻塞同步,也叫互斥同步或悲觀鎖,就是說我們總是假設的認為每次對數據操作時,其他線程都會修改這個數據,所以無論共享數據是否真正競爭,這種處理都去加鎖,比如最常見的使用synchronized關鍵字,可以將操作共享數據的代碼或者方法聲明為synchronized,這樣每次就只有一個線程能拿到鎖;此外也可以用lock來加鎖,也能夠保證同步,這種都稱為阻塞同步,這樣的缺點是其他線程想要訪問數據都會阻塞掛起,這樣每次阻塞和喚醒都有性能上的消耗; 悲觀鎖適合寫操作多的場景,可以保證寫操作時數據正確;
- 非阻塞同步,或者說是一種樂觀鎖,這種方法就是說總是認為不會發生線程不安全,也就是認為每次取數據的時候,其他線程不會修改共享的數據,所以不用上鎖,在執行完需要更新數據的時候再比較判斷其他線程是否修改過數據,一種常見的方法就是CAS(compare and swap),假設不會沖突所有不上鎖,if沖突了那就重新來,直到成功為止;在CAS中,有3個操作數,當前值V,期望值E,還有新值new,當操作一個共享變量的時候,比較V和E,if相等,那說明這個過程中沒有其他線程修改了這個變量,就把V置為新值new;if不相等,那說明這個過程中有其他線程修改了這個變量,那就改變期望值E重新進行比較。 樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其性能提升;
舉個例子,比如兩個線程操作共享變量a=0;要執行a++;線程A先對變量a進行a++,V=0,E=0,新值new=1;但是在執行的過程中,線程B執行了a++將值改為1了,所以在比較的時候V=1,不等於E了,所以就需要重新比較,將3個操作數更新為V=1,E=1,new=2,這時候if沒有其他線程操作了,那就將新值賦給V,修改成功;
Synchronized和Lock的區別?
Synchronized和Lock都可以解決線程安全問題,兩者的不同就是Synchronized在執行完相應的同步代碼之后,會自動釋放同步監視器,也就是鎖;而Lock需要手動啟動同步(lock())和手動關閉(unlock());
問:什么是自旋鎖?
當阻塞或者喚醒一個線程的時候需要去切換CPU的狀態,這種狀態的轉換是很耗費時間的,比如有時候可能狀態轉換消耗的時間有可能比用戶執行代碼的時間都要長。所以有時候把一個線程去掛起然后再喚醒的話有點得不償失,所以可以不必讓當前線程去阻塞,而是讓其自旋,if在自旋完成后前面鎖定同步資源的線程已經釋放了鎖,那當前線程就可以不必阻塞而是直接獲取同步資源,這樣就可以避免線程切換的開銷。
缺點:自旋等待的過程是占用CPU的,if鎖被占用的時間很短,自旋等待的效果就會很好,但是,if鎖被占用的時間很長,那就會浪費處理器的資源。所以需要給自旋等待設置一定的限度,超過了以后還沒有獲得鎖,那就把當前線程掛起;
問:線程的5種狀態?
- 新建:當一個Thread類或子類被聲明被創建時,就會處於新建狀態;
- 就緒:新建狀態的線程start()后,將進入線程隊列等待CPU時間片,這時候已經具備了運行的條件,但是沒有分配到CPU資源;
- 運行:當就緒的狀態被調度並獲得CPU資源后,進入運行狀態;
- 阻塞:在某些情況下,當前線程被掛起,讓出了CPU並且臨時終止了自己的執行,就進入了阻塞狀態;
- 死亡:線程完成了全部工作,或者被強制終止或出現異常,當前線程結束,就是死亡狀態;
問:什么是守護線程?
在java中主要有兩類線程,一個是user thread(用戶線程),一個是daemon thread(守護線程)
只要JVM實例中存在任何一個非守護線程沒有結束,那守護線程就在工作,當所有的非守護線程結束時,守護線程就會隨着JVM一同結束工作;比如說最常見的守護線程就是GC垃圾回收器。當程序里沒有運行着的其他線程時,程序就不再產生垃圾,垃圾回收器也就無事可做,就會自動離開,它始終在低級別的狀態下運行,用於實時監控和管理系統中的可回收資源;
問:Java實現多線程的方式(有程序/兩種實現的區別)?
在java里主要是有兩種方法實現多線程:
- 繼承Thread類:定義一個子類繼承Thread類,然后在子類中重寫Thread類的run方法,然后創建子類對象,也就是創建一個線程對象,調用這個線程對象的start方法,啟動線程,調用run方法;
注意不能直接調用run方法,當new一個thread的時候,線程進入了新建狀態,調用start()方法,會啟動一個線程並且使線程進入就緒狀態,然后當分配的時間片到了以后就可以開始運行了,start會執行相應的准備工作,然后自動執行run方法,這是多線程的工作;if直接調用run方法,會把run方法當成一個main線程下的普通方法去執行,不會在某個線程下執行,這不是多線程工作;
- 實現Runnable接口:首先定義一個子類,然后該子類實現Runnable接口,並且重寫run方法,然后創建該子類實現類的對象,然后將該對象作為參數傳遞到Thread類的構造器中,創建Thread類的對象,然后調用Thread類的start方法,開啟線程,調用剛才那個子類的的run方法;
一般更常用runnable接口,可以避免單繼承的局限性,同時,多個線程可以共享同一個接口實現類的對象,所以很適合多個相同線程來處理同一份資源的情況;
在新的JDK中,又新增了創建線程的方法:
- 實現callable接口:首先創建一個實現了callable接口的子類,重寫call方法,然后創建該接口實現類的對象,將此對象作為參數傳遞到FutureTask構造器中,創建FutureTask對象,然后再將futuretask對象作為參數傳遞到Thread類的構造器中,創建Thread對象,然后調用start;與Runnable接口最大的區別就在於callable接口中call方法可以有返回值,可以通過futuretask.get()來獲得;此外也可以拋出異常;並且支持泛型;
- 使用線程池:提前創建好多個線程,放入線程池中,使用時直接獲取,使用完放回去,能夠避免頻繁的創建銷毀、實現一個資源的重復利用。相關API有ExecutorService和Executors;
//方法一:繼承Thread類;
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
//方法二:實現Runnable接口:
class MThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
MThread mThread = new MThread();
//線程共享接口實現類;
Thread t1 = new Thread(mThread);
t1.setName("線程1");
t1.start();
Thread t2 = new Thread(mThread);
t2.setName("線程2");
t2.start();
}
}
問:進程間的通信?
每個進程之間彼此相互獨立,擁有各自不同的地址空間,所以在一個進程中的全局變量在其他進程里是看不到的,所以進程之間交換數據需要內核,也就是在內核中開辟一塊緩沖區,然后把數據從用戶空間拷貝到緩沖區,其他線程再從緩沖區把數據讀走,這就是進程間的通信;
- 管道pipe:是一種半雙工的通信,也就是說數據只能在一個方向上流動,並且只能用於親緣關系的進程通信(比如父子進程或兄弟進程);實質上就是管道一端的進程順序的將進程寫入緩沖區,然后另一端的進程再依次順序讀取數據;
- 命名管道FIFO:和前面基本一樣,但是允許無親緣關系的進程進行通信;
- 消息隊列MessageQueue:就是一個消息的鏈表,與管道進行相比,就是為每個消息指定了特定的消息類型,接收的時候根據自定義條件接收特定類型的消息;
- 共享內存:也就是允許多個線程共享一個指定的存儲區,這樣的好處是效率高,因為進程可以直接讀寫內存,不需要進行數據的拷貝;
- 信號量:信號量是一個計數器,可以用來控制多個進程對共享資源的訪問,實現進程之間的互斥和同步,經常是把它作為一種鎖機制,防止當一個進程正在訪問共享資源的時候,其他進程也訪問該資源;比如當某個進程來訪問內存1的時候,就把信號量的值設置為0,這時候其他的進程再訪問的時候看到信號量為0就知道已經有進程在訪問了;
- 套接字socket:可以實現不同計算機上的進程之間的通信,比如說客戶端和服務端;
- 客戶端:1創建socket。根據ip地址和端口號構造socket對象;2打開輸入輸出流:使用getInputStream和getOutStream;3進行讀寫操作;4關閉socket;
- 服務端:1創建ServerSocket並綁定指定端口,用於監聽客戶端請求,2調用accept。3調用socket的getOutputStream和getInputStream,用於數據的收發;4福安比socket對象;
問:線程間通信方式(wait和notify)?
線程通信就是說當多個線程共同操作共享資源的時候,為了避免資源爭奪,互相告知自己的狀態;這其中最常用的就是wait和notify方法;
- wait方法就是當前線程必須等待,放棄擁有的鎖,然后等待其他線程來喚醒;
- notify方法就是用來喚醒的,能夠喚醒一個等待當前對象鎖的線程; 這兩個方法都是獲得鎖的線程調用的,所以需要放在synchronized修飾的方法或者代碼塊里;
問:說一下線程池?
線程池是一種池化技術,除了線程池以外,其實還有很多,比如說數據庫連接池等,它的底層是維護了一個線程隊列,隊列里保存着等待狀態的線程;主要的目的就是減少了每次都去獲取資源的消耗,可以重復利用創建的線程,不用再每次創建和銷毀,提高資源的利用率;其次,能夠提高響應的速度,任務到的時候不需要再去創建了;還有就是提高線程的可管理性,線程不能無限制的創建,這樣既浪費系統資源,又降低系統穩定性,使用線程池就可以統一分配管理;
問:sleep和wait異同?
首先兩者都能夠使線程進入阻塞狀態;
- sleep是Thread類的方法,可以在任何需要的時候進行調用,調用后線程進入睡眠,休眠結束后進入就緒狀態;
- wait是Object類的方法,調用它的線程必須獲得鎖,所以wait是在synchronized關鍵字修飾的代碼塊或函數中調用的,當調用wait后,會釋放鎖,開始等待,直到被喚醒;
- if兩個方法都在同步代碼塊或者同步方法中,sleep是不會釋放鎖,而wait會釋放鎖;
JVM
問:介紹一下JVM?
JVM是java的核心部分,是java虛擬機的意思,有人也稱為世界上最強大的虛擬機,java的一次編譯,到處運行正是靠虛擬機來實現的。
Java虛擬機運行時數據區包括很多部分:
- 程序計數器:是一塊較小的內存空間,可以看做是當前線程執行的字節碼文件的行號,主要有2個作用,1是通過計數器的值來讀取指令,實現流程控制,比如循環、跳轉等;2是在多線程的情況下,記錄當前線程執行的位置,當線程被切換回來的時候知道運行到哪了;
- java虛擬機棧:同樣也是線程私有的,由一個個棧幀組成,每個方法在執行的時候就會創建棧幀,存儲局部變量、方法出口等信息,然后把棧幀壓棧,當方法結束調用時,棧幀出棧;
- 本地方法棧:基本和虛擬機棧一樣,區別在於java虛擬機棧為虛擬機執行字節碼文件,而本地方法棧為使用到的native方法服務;(native方法就是java調用非java代碼比如c、c++的接口);
- 堆:這是java虛擬機里內存最大的一塊區域,是線程共享的,主要用來存放實例化的對象和數組;同時這也是垃圾回收的主要區域,所以有時候也叫GC堆;細分的話堆空間又可以分為老生代和新生代;主要是為了划分空間,為了方面對象和垃圾回收;
- 老生代主要是存放應用程序中生命周期長的存活對象;
- 新生代主要存放存活周期短的對象;
- 方法區:方法區也是共享內存,主要用來存儲被虛擬機加載的類信息、常量、靜態變量等;
問:堆內存和棧內存?
首先是數據結構中:
- 棧是一種連續存儲的數據結構,具有先進后出的性質;
- 堆是一種非連續的樹型存儲結構,每個節點都有一個值,整棵樹是經過排序的,特點是根節點的值最小(或最大),而且根節點的子樹也是一個堆。經常用來實現優先隊列;
其次是在內存中的堆區和棧區:
- 棧內存空間是由操作系統自動分配和釋放的,一般情況下空間有限;主要是用來存放一些基本類型的變量、指令代碼還有局部變量;棧內存的存取速度比較快,此外,棧中的數據可以共享;
- 堆區的話內存空間是手動申請和釋放的,比如經常用new關鍵字來進行分配,可以動態的分配內存大小,堆內存的空間很大,幾乎沒有限制;堆內存在使用完畢后,是由垃圾回收器進行回收的,
問:類加載機制了解嗎?
JVM 把描述類的數據從 Class 文件加載到內存,並對數據進行驗證、解析和初始化,最終形成可以被虛擬機直接使用的 Java 類型。
加載:通過一個類的全限定類名獲取對應的二進制流,在內存中生成對應的 Class 實例,作為方法區中這個類的訪問入口。
驗證:確保 Class 文件符合約束,防止因載入有錯字節流而遭受攻擊。包含:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。
准備:為類靜態變量分配內存並設置零值,該階段進行的內存分配僅包括類變量,不包括實例變量。
解析:將常量池內的符號引用替換為直接引用。
初始化:直到該階段 JVM 才開始執行類中編寫的代碼,根據程序員的編碼去初始化類變量和其他資源。
問:java的類加載器?
加載指的是將類的class文件讀入內存,然后為其創建一個java.lang.class對象;
JDK有3個自帶的類加載器:bootstrap classloader,ExtClassLoader,AppClassLoader
- bootstrapClassLoader:根類加載器:是extclassLoader的父類加載器,默認會加載java環境變量%JAVA_HOME%lib下的jar包和class文件;
- extclassloader:擴展類加載器:是appclassloader的父類加載器,復雜加載%JAVA_HOME%lib/ext下的jar包和class類;
- appclassLoader是自定義類加載器的父類,負責加載classpath下的類文件
問:雙親委派機制?
雙親委派的意思是說 如果一個類加載器收到了類加載請求,它並不會自己去加載,而是先把這個請求委托給父類的加載器去執行,if父類加載器還存在父類加載器,那就向上進一步委托,依次遞歸,請求最終到達頂層的啟動類加載器,if父類加載器能夠完成類加載任務,就成功返回,if無法完成,子類加載器就會嘗試自己去加載;(每個兒子都很懶,有活了先去找父親,直到父親也干不了時,兒子再自己干)
優勢
- 這種層次結構可以避免類的重復加載,在JVM中區分不同類,不僅是根據類名,相同的class文件被不同的類加載器加載也是不同的類,所以當父類加載了該類時,子類的加載器就沒必要再加載一次了;
- 安全性的考慮:可以避免用戶自己編寫的類替換了java中的一些核心類,比如String、Integer等;
問:說一下對象創建過程?
- 遇到new指令時,首先檢查這個指令是否能在方法區里定位到一個類,檢查這個類是否已經被加載解析過,如果沒有的話,就執行相應的類加載;
- 類加載完之后,在堆空間為新對象分配內存;
- 內存空間分配完之后初始化為0,然后填充對象頭,把對象是哪個類實例、對象的哈希碼、等信息存入對象頭;
- 上面完成之后對虛擬機層面對象已經創建完成,但是對java程序來說,還要執行init方法,按照程序進行初始化;
問:說一下垃圾回收機制?
在java虛擬機的內存中,程序計數器還有棧結構都是跟着線程開始和結束的,所以不太需要考慮回收的問題,主要考慮的是內存中的堆;if不進行回收,那內存遲早被耗完,就沒辦法運行了;
- 垃圾標記階段:需要判斷哪些對象是垃圾,需要被回收,總體上就是當一個對象不被任何對象引用時,就判斷為垃圾;主要有兩種方法:
- 引用計數法:為每個對象添加一個計數器,當引用一次就+1;引用失效時計數器-1;計數器為0,對象就不用了,簡單高效,但是不能解決循環引用的問題;比如兩個對象互相引用,始終為1,都不能被回收,其實是一種內存泄漏;
- 可達性分析:通過“GC roots”的對象作為起始點,然后往下搜索,走過的路徑叫做引用鏈,內存中存活的對象都會直接或間接的與GC root相連,if不相連那就可以被回收了;可以被作為gc root的對象要是活躍的引用,所以可以選用棧和方法區中的引用(因為其不被GC管理),所以不會被GC回收;比如虛擬機棧中的引用對象、本地方法棧中的引用對象、(線程私有,只要線程沒終止,就活着),方法區中靜態引用對象、方法區中的常量引用對象;
- 垃圾清除階段:標記好垃圾后,就需要清除垃圾釋放緩存;
- 標記-清除算法:對堆內存進行遍歷,if發現哪個對象沒有被標記為可達對象,那就將其回收;這樣的話1是效率不高,2是會產生許多不連續的內存碎片,以后如果要分配連續的大內存時,可能就找不到這樣的空間了;
- 復制算法:將內存分為大小相等的兩塊,每次只用一塊,回收的時候把正在使用的那一塊里面活着的對象復制到另一塊,然后把這塊內存回收,兩個內存塊這樣交替;這樣子解決了碎片問題,但是將可使用的內存其實就減小了一半;而且還需要頻繁的進行復制操作;
- 標記-整理算法:標記還和原來一樣,然后把存活的對象都整理壓縮到內存的一端,然后清理所有的其他內存;主要用於老生代;
- 分代收集算法:目前大部分都采用的這種算法,主要是根據對象的存活周期將內存分為幾塊並采用不同的垃圾收集算法;一般都是將內存分為新生代(每次垃圾收集都有很多對象死去,只有少量活着,所以用復制算法),老生代(對象的存活率比較高,所以一般使用標記-整理算法)
問:java中內存溢出(outOfMemoryError)和棧溢出(StackOverflowError)?
在java虛擬機規范中描述了兩種異常:
- if線程請求的棧深度大於虛擬機所允許的最大深度,就會拋出stackoverflowError;比如常見的就是方法調用的層次太深,內存不夠新建棧幀;比如遞歸的時候
- if虛擬機在申請內存的時候無法申請到足夠的內存空間,那拋出OutOfMemoryError,這里面可能包括棧內存溢出、堆內存溢出等問題;
問:說一下內存溢出和內存泄漏?
- 內存溢出(out of memory,OOM)是指堆中沒有足夠的內存空間了;比如很常見的有棧溢出,像遞歸這個過程,if沒有遞歸出口那就會棧溢出;
- 內存泄漏,就是說申請完內存后沒辦法釋放,比如對象不用了,然后也沒有被回收,這是一個逐漸的過程,內存被逐步蠶食最后就會導致內存溢出;比如說單例模式,單例的對象和應用程序一樣長,如果單例對象引用了另一個對象,那即使這個對象不用了,由於單例仍然引用着它,所以也不能回收,就發生了內存泄漏;再比如說一些資源沒有關閉,比如數據庫連接,必須手動關閉;