在瀏覽器輸入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 直接排除。
程式碼: