在瀏覽器輸入url後發生了什麼

1.講一下Java的虛擬機器

2.說說怎麼能讓虛擬機器中的方法區直接爆滿

3.講一下Java的垃圾回收機制

4.把Java中的容器類都講一下

5.Java中的鎖是怎麼實現的?

6.引用計數法有啥缺點呢

7.說一下TCP的三次握手和四次揮手

8.為什麼揮手時有個time_wait?即2MSL

9.說一下瀏覽器輸入URL都發生了什麼,到頁面出來的流程

10.作業系統中的死鎖怎麼形成的,怎麼預防死鎖

11.程序和執行緒有什麼區別

12.執行緒的幾種狀態

13.執行緒池用過沒,怎麼使用,流程是啥

14.建立執行緒有哪些方法,有啥區別,你一般怎麼建立


1. 講一下Java的虛擬機器

6. 引用計數法有啥缺點呢

1. 引用計數法(Java沒有采用)

2. 標記-清除法 (jvm老年代回收)

3. 標記-壓縮法 (jvm老年代回收)

4. 複製演算法 (jvm新生代回收)

引用計數演算法

給物件中新增一個引用計數器,每當有一個地方引用它時,計數器的值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的物件就是不可能再被使用的。這也就是需要回收的物件。

引用計數演算法是物件記錄自己被多少程式引用,引用計數為零的物件將被清除。

計數器表示的是有多少程式引用了這個物件(被引用數)。計數器是無符號整數。

計數器的增減

引用計數法沒有明確啟動 GC 的語句,它與程式的執行密切相關,在程式的處理過程中通過增減計數器的值來進行記憶體管理。

new_obj() 函式

與GC標記-清除演算法相同,程式在生成新物件的時候會呼叫 new_obj()函式。

這裡 pickup_chunk()函式的用法與GC標記-清除演算法中的用法大致相同。不同的是這裡返回 NULL 時,分配就失敗了。這裡 ref_cnt 域代表的是 obj 的計數器。

在引用計數演算法中,除了連線到空閒連結串列的物件,其他物件都是活躍物件。所以如果 pickup_chunk()返回 NULL,堆中也就沒有其它大小合適的塊了。

update_ptr() 函式

update_ptr() 函式用於更新指標 ptr,使其指向物件 obj,同時進行計數器值的增減。

這裡 update_ptr 為什麼需要先呼叫 inc_ref_cnt,再呼叫dec_ref_cnt呢?

是因為有可能 *ptr和 obj 可能是同一個物件,如果先呼叫dec_ref_cnt可能會誤傷。

inc_ref_cnt()函式

這裡inc_ref_cnt函式只對物件 obj 引用計數 1

func inc_ref_cnt(obj){

obj.ref_cnt

}

dec_ref_cnt() 函式

這裡 dec_ref_cnt 函式會把之前引用的物件進行-1 操作,如果這時物件的計數器變為0,說明這個物件是一個垃圾物件,需要銷燬,那麼被它引用的物件的計數器值都需要相應的-1。

func dec_ref_cnt(obj){

obj_ref_cnt--

if(obj.ref_cnt == 0)

for(child : children(obj))

dec_ref_cnt(*child) // 遞迴將被需要銷燬物件引用的物件計數-1

reclaim(obj)

}

上圖這裡開始時,A 指向 B,第二步 A 指向了 C。可以看到通過更新,B 的計數器值變為了0,因此 B 被回收(連線到空閒連結串列),C 的計數器值由1變成了2。

通過上邊的介紹,應該可以看出引用計數垃圾回收的特點。

在變更陣列元素的時候會進行指標更新

通過更新執行計數可能會產生沒有被任何程式引用的垃圾物件

引用計數演算法會時刻監控更新指標是否會產生垃圾物件,一旦生成會立刻被回收。

所以如果呼叫 pickup_chunk函式返回 NULL,說明堆中所有物件都是活躍物件。

引用計數演算法的優點

1.可立即回收垃圾

每個物件都知道自己的引用計數,當變為0時可以立即回收,將自己接到空閒連結串列

2.最大暫停時間短

因為只要程式更新指標時程式就會執行垃圾回收,也就是每次通過執行程式生成垃圾時,這些垃圾都會被回收,記憶體管理的開銷分佈於整個應用程式執行期間,無需掛起應用程式的執行來做,因此消減了最大暫停時間(但是增多了垃圾回收的次數)

最大暫停時間,因執行 GC 而暫停執行程式的最長時間。

3.不需要沿指標查詢

產生的垃圾立即就連線到了空閒連結串列,所以不需要查詢哪些物件是需要回收的

引用計數演算法的缺點

1.計數器值的增減處理頻繁

因為每次物件更新都需要對計數器進行增減,特別是被引用次數多的物件。

2.計數器需要佔用很多位

計數器的值最大必須要能數完堆中所有物件的引用數。比如我們用的機器是32位,那麼極端情況,可能需要讓2的32次方個物件同時引用一個物件。這就必須要確保各物件的計數器有32位大小。也就是對於所有物件,必須保留32位的空間。

假如物件只有兩個域,那麼其計數器就佔用了整體的1/3。

3.迴圈引用無法回收

這個比較好理解,迴圈引用會讓計數器最小值為1,不會變為0。

迴圈引用

像這樣,兩個物件相互引用,所以各個物件的計數器都為1,且這些物件沒有被其他物件引用。所以計數器最小值也為1,不可能為0。

延遲引用計數法

引用計數法雖然縮小了最大暫停時間,但是計數器的增減處理特別多。為了改善這個缺點,延遲引用計數法(Deferred Reference Counting)被研究了出來。

通過上邊的描述,可以知道之所以計數器增減處理特別繁重,是因為有些增減是根引用的變化,因此我們可以讓根引用的指標變化不反映在計數器上。比如我們把 update_ptr($ptr, obj)改寫成*$ptr = obj,這樣頻繁重寫對重物件中引用關係時,計數器也不需要修改。但是這有一個問題,那就是計數器並不能正確反映出物件被引用的次數,就有可能會出現,物件仍在活動,卻被回收。

在延遲引用計數法中使用ZCT(Zero Count Table),來修正這一錯誤。

ZCT 是一個表,它會事先記錄下計數器在 dec_ref_cnt()函式作用下變成 0 的物件。

ZCT

dec_ref_cnt 函式

在延遲引用計數法中,引用計數為0 的物件並不一定是垃圾,會先存入到 zct 中保留。

func dec_ref_cnt(obj){

obj_ref_cnt--

if(obj.ref_cnt == 0) //引用計數為0 先存入到 $zct 中保留

if(is_full($zct) == TRUE) // 如果 $zct 表已經滿了 先掃描 zct 表,清除真正的垃圾

scan_zct()

push($zct, obj)

}

scan_zct 函式

func scan_zct(){

for(r: $roots)

(*r).ref_cnt

for(obj : $zct)

if(obj.ref_cnt == 0)

remove($zct, obj)

delete(obj)

for(r: $roots)

(*).ref_cnt--

}

第二行和第三行,程式先把所有根直接引用的計數器都進行增量。這樣,來修正計數器的值。

接下來檢查 $zct 表中的物件,如果此時計數器還為0,則說明沒有任何引用,那麼將物件先從 $zct中清除,然後呼叫 delete()回收。

delete() 函式定義如下:

func delete(obj){

for(child : children(obj)) // 遞迴清理物件的子物件

(*child).ref_cnt--

if (*child).ref_cnt == 0

delete(*child)

reclaim(obj)

}

new_obj() 函式

除 dec_ref_cnt 函式需要調整,new_obj 函式也要做相應的修改。

func new_obj(size){

obj = pickup_chunk(size, $free_list)

if(obj == NULL) // 空間不足

scan_zct() // 掃描 zct 以便獲取空間

obj = pickup_chunk(size, $free_list) // 再次嘗試分配

if(obj == NULL)

allocation_fail() // 提示失敗

obj.ref_cnt = 1

return obj

}

如果第一次分配空間不足,需要掃描 $zct,以便再次分配,如果這時空間還不足,就提示失敗

在延遲引用計數法中,程式延遲了根引用的計數,通過延遲,減輕了因根引用頻繁變化而導致的計數器增減所帶來的額外的負擔。

但是,延遲引用計數卻不能馬上將垃圾進行回收,可立即回收垃圾這一優點也就不存在了。scan_zct函式也會增加程式的最大暫停時間。

Sticky 引用計數法

對於引用計數法,有一個不能忽略的部分是計數器位寬的設定。假設為了反映所有引用,計數器需要1個字(32位機器就是32位)的空間。但是這會大量的消耗記憶體空間。比如,2個字的物件就需要一個字的計數器。也就是計數器會使物件所佔的空間增大1.5倍。

sticky 引用計數法就是用來減少位寬的。

如果我們為計數器的位數設為5,那麼計數器最大的引用數為31,如果有超過31個物件引用,就會爆表。對於爆表,我們怎麼處理呢?

1. 什麼都不做

這種處理方式對於計數器爆表的物件,再有新的引用也不在增加,當然,當計數器為0 的時候,也不能直接回收(因為可能還有物件在引用)。這樣其實是會產生殘留的物件佔用記憶體。

不過,研究表明,大部分物件其實只被引用了一次就被回收了,出現5位計數器溢位的情況少之又少。

爆表的物件大部分也都是重要的物件,不會輕易回收。

所以,什麼都不做也是一個不錯的辦法。

2. 使用GC 標記-清除演算法進行管理

這種方法是,對於爆表的物件,使用 GC 標記-清除演算法來管理。

func mark_sweep_for_counter_overflow(){

reset_all_ref_cnt()

mark_phase()

sweep_phase()

}

首先,把所有物件的計數器都設為0,然後進行標記和清除階段。

標記階段程式碼為:

func mark_phase(){

for (r: $roots) // 先把根引用的物件推到標記棧中

push(*r, $mark_stack)

while(is_empty($mark_stack) == False) // 如果堆不為空

obj = pop($mark_stack)

obj.ref_cnt

if(obj.ref_cnt == 1) // 這裡必須把各個物件及其子物件堆進行標記一次

for(child : children(obj))

push(*child, $mark_stack)

}

在標記階段,先把根引用的物件推到標記棧中

然後按順序從標記棧中取出物件,對計數器進行增量操作。

對於迴圈引用的物件來說,obj.ref_cnt > 1,為了避免無謂的 push 這裡需要進行 if(obj.ref_cnt == 1) 的判斷

清除階段程式碼為:

func sweep_phase(){

sweeping = $heap_top

while(sweeping < $heap_end) // 因為迴圈引用的所有物件都會被 push 到 head_end 所以也能被回收

if(sweeping.ref_cnt == 0)

reclaim(sweeping)

sweeping = sweeping.size

}

在清除階段,程式會搜尋整個堆,回收計數器仍為0的物件。

這裡的 GC 標記-清除演算法和GC 標記-清除演算法 主要不同點如下:

1.開始時將所有物件的計數器值設為0

2.不標記物件,而是對計數器進行增量操作

3.為了對計數器進行增量操作,演算法對活動物件進行了不止一次的搜尋。

這裡將 GC 標記-清除演算法和引用計數法結合起來,在計數器溢位後,物件稱為垃圾也不會漏掉清除。並且也能回收迴圈引用的垃圾。

因為在查詢物件時不是設定標誌位而是把計數器進行增量,所以需要多次查詢活動物件,所以這裡的標記處理比以往的標記清除花的時間更長,吞吐量會相應的降低。

7. 為什麼揮手時有個time_wait?即2MSL

TIME_WAIT主要是用來解決以下幾個問題:

1)上面解釋為什麼主動關閉方需要進入TIME_WAIT狀態中提到的: 主動關閉方需要進入TIME_WAIT以便能夠重發丟掉的被動關閉方FIN包的ACK。如果主動關閉方不進入TIME_WAIT,那麼在主動關閉方對被動關閉方FIN包的ACK丟失了的時候,被動關閉方由於沒收到自己FIN的ACK,會進行重傳FIN包,這個FIN包到主動關閉方後,由於這個連線已經不存在於主動關閉方了,這個時候主動關閉方無法識別這個FIN包,協議棧會認為對方瘋了,都還沒建立連線你給我來個FIN包?

於是回覆一個RST包給被動關閉方,被動關閉方就會收到一個錯誤(我們見的比較多的:connect reset by peer,這裡順便說下 Broken pipe,在收到RST包的時候,還往這個連線寫資料,就會收到 Broken pipe錯誤了),原本應該正常關閉的連線,給我來個錯誤,很難讓人接受;

2)防止已經斷開的連線1中在鏈路中殘留的FIN包終止掉新的連線2(重用了連線1的所有的5元素(源IP,目的IP,TCP,源埠,目的埠)),這個概率比較低,因為涉及到一個匹配問題,遲到的FIN分段的序列號必須落在連線2的一方的期望序列號範圍之內,雖然概率低,但是確實可能發生,因為初始序列號都是隨機產生的,並且這個序列號是32位的,會迴繞;

3)防止鏈路上已經關閉的連線的殘餘資料包(a lost duplicate packet or a wandering duplicate packet) 干擾正常的資料包,造成資料流的不正常。這個問題和2)類似。

MSL是Maximum Segment Lifetime英文的縮寫,中文可以譯為“報文最大生存時間”,他是任何報文在網路上存在的最長時間,超過這個時間報文將被丟棄。因為tcp報文(segment)是ip資料包(datagram)的資料部分,具體稱謂請參見《資料在網路各層中的稱呼》一文,

而ip頭中有一個TTL域,TTL是time to live的縮寫,中文可以譯為“生存時間”,這個生存時間是由源主機設定初始值但不是存的具體時間,而是儲存了一個ip資料包可以經過的最大路由數,每經過一個處理他的路由器此值就減1,當此值為0則資料包將被丟棄,同時傳送ICMP報文通知源主機。RFC 793中規定MSL為2分鐘,實際應用中常用的是30秒,1分鐘和2分鐘等。

2MSL即兩倍的MSL,TCP的TIME_WAIT狀態也稱為2MSL等待狀態,當TCP的一端發起主動關閉,在發出最後一個ACK包後,即第3次握手完成後傳送了第四次握手的ACK包後就進入了TIME_WAIT狀態,必須在此狀態上停留兩倍的MSL時間,等待2MSL時間主要目的是怕最後一個ACK包對方沒收到,那麼對方在超時後將重發第三次握手的FIN包,主動關閉端接到重發的FIN包後可以再發一個ACK應答包。

在TIME_WAIT狀態時兩端的埠不能使用,要等到2MSL時間結束才可繼續使用。當連線處於2MSL等待階段時任何遲到的報文段都將被丟棄。不過在實際應用中可以通過設定SO_REUSEADDR選項達到不必等待2MSL時間結束再使用此埠。

TTL與MSL是有關係的但不是簡單的相等的關係,MSL要大於等於TTL。

9.說一下瀏覽器輸入URL都發生了什麼,到頁面出來的流程

1、首先,在瀏覽器位址列中輸入url

2、瀏覽器先檢視瀏覽器快取-系統快取-路由器快取,如果快取中有,會直接在螢幕中顯示頁面內容。若沒有,則跳到第三步操作。

3、在傳送http請求前,需要域名解析(DNS解析),解析獲取相應的IP地址。

4、瀏覽器向伺服器發起tcp連線,與瀏覽器建立tcp三次握手。

5、握手成功後,瀏覽器向伺服器傳送http請求,請求資料包。

6、伺服器處理收到的請求,將資料返回至瀏覽器

7、瀏覽器收到HTTP響應

8、讀取頁面內容,瀏覽器渲染,解析html原始碼

9、生成Dom樹、解析css樣式、js互動

10、客戶端和伺服器互動

11、ajax查詢

其中,步驟2的具體過程是:

瀏覽器快取:瀏覽器會記錄DNS一段時間,因此,只是第一個地方解析DNS請求;

作業系統快取:如果在瀏覽器快取中不包含這個記錄,則會使系統呼叫作業系統,獲取作業系統的記錄(儲存最近的DNS查詢快取);

路由器快取:如果上述兩個步驟均不能成功獲取DNS記錄,繼續搜尋路由器快取;

ISP快取:若上述均失敗,繼續向ISP搜尋。

手撕程式碼:

思路:(可能不是很正規)

要使用盡可能少的硬幣兌換,肯定是儘可能使用大額的硬幣,但是有個問題是大額的硬幣可能找不開啊,舉個簡單的例子:現在有100,30,1共三種硬幣,我要湊120,儘可能使用大額硬幣的話方案應該是一個100加上二十個1,很明顯不對,沒有四個30使用硬幣少。 所以直接找還是不行,還是得遞迴或者回溯,我用的是直接遞迴,遞迴的過程就在程式碼裡面體現了就不說了 ,說一下可取的點,雖然儘可能使用大額的硬幣不能直接得到答案,但是我們可以得到一點有用的東西,我使用100的硬幣得到的答案雖然不是最小的,但是也算是比較小的了,在這裡是21枚硬幣,我們可以藉助這個21排除一些方案,比如我全選1的硬幣肯定不行,120/1==120遠大於21 直接排除。

程式碼: