雙非Java的學習之旅以及秋招路程

zjwoo 發表於 2021-09-19
Java
個人資訊:
趁著中秋寫個帖子記錄一下吧。渣渣本,無實習,無高質量證照,走了很多彎路,最後選擇的Java後端。現在算是半躺平了,接了幾個中小廠的offer保底,20w多的薪資,後面還有幾家公司接著面。不是大佬,還有很多比我厲害的雙非戰神!感謝很多前輩還有網友,讓我白嫖了那麼多的資源,趁著中秋假期,寫篇文章總結一下。
 
大一大二劃水:
大一學Unity3D做遊戲開發,大二學CTF、滲透測試挖洞,都是跟著社團一起學的。我是在大二的暑假,意識到了自己的一些問題,決定選擇Java後端的方向。這個時候我Java只會用eclipse寫寫for迴圈。
 
大三上學期:
大三上在b站看了黑馬的999集JavaSE、JavaWEB、狂神的SSM、尚矽谷的SSM、Redis等視訊。然後寒假投了一波小公司的簡歷,面試發現知識遠遠不夠。大三上的寒假繼續學了尚矽谷的SpringBoot、Dubbo、Spring註解驅動原理等,也是這個時候學長告訴我,春招對大三學生來說也非常重要,很多大佬都是大二就出去實習了,我還一直以為大四才用找工作。也是這個時候,我才知道了一個非常友好的平臺,牛客網。可以經常看看別人的面試經歷,調整自己的學習重點。
大三下學期:
過完年的寒假投實習崗,阿里、騰訊、位元組、京東、攜程各大廠的一面,沒有一家大廠能進二面的。演算法不會,專案沒含金量,我都替面試官尷尬。。意識到了演算法的重要性,開始刷劍指offer和leetcode,看部落格和視訊跟著做專案。今年5月份開始,繼續投簡歷,繼續面試,開始拿到了一些offer,拿到第一份月薪過萬的工作還挺激動。
但我很清楚自己的弱點,計算機網路、作業系統、jvm、juc、mysql原理,都不紮實,而這些又是大公司愛問的。就開始瘋狂補這些方面知識。黑馬的jvm和juc就挺好。學了後繼續面試,每一場面試我都會覆盤總結,記錄下來,形成自己的小題庫。不會就去學,學不會就去背!其實高頻被問的就那麼些。8月中旬開始,狀態越來越好,只要能進面試,至少不會一輪遊了,都能接著二面,面試過程也能侃侃而談,面試官提出的問題都能答個七七八八。
碎碎念:
最後,秋招算是落下帷幕,有點遺憾但要保持熱愛。學習Java剛好有一年的時間了,當然沒有一些幾個月零基礎上岸大佬的學習效率高,中間也三天打魚兩天曬網過,但總體保持著一個較好的學習狀態。秋招就是一個長跑的過程,投了近百家公司,最累的時候一天四場面試兩場筆試。找工作焦慮很正常,經常睡不著。但一直堅持,相信會有好的結果。麵包會有的,offer也會有的!
 
 
 
附上一點點自己經常被問到的題目,超高頻(答案不一定全對):
1、ArrayList和LinkedList的區別是什麼?
ArrayList的底層是陣列實現,初始化的時候資料量是零,當第一次add的時候預設變成10。擴容是每次到之前的1.5倍。特性是查詢速度快,增刪效率低。
擴容條件:每超出陣列長度就會進行擴容
擴容分為兩個步驟:1.把原來的陣列複製到一個更大的陣列中2.把新元素新增到擴容的陣列裡。
 
LinkedList的底層是帶有頭節點和尾節點的雙向連結串列,實現了Deque介面所以還可以當雙向佇列使用。特性是適合插入刪除,查詢速度慢。
執行緒都不安全。
 
如果想要執行緒安全,又要用List,會怎麼用?
古老的Vector類,底層結構和ArrayList一樣都是陣列。與ArrayList的區別是,大部分方法都被synchronized關鍵字修飾,所以是一個執行緒安全的。擴容和ArrayList有所區別,每次擴容為之前的2倍。
 
2、hashmap的資料結構是什麼?
hashmap在1.7和1.8版本底層資料結構不同:1.7是陣列加連結串列,1.8的資料結構是陣列加連結串列/紅黑樹的方式。
連結串列和紅黑樹之間的轉換:當連結串列長度大於等於閾值8,並且陣列長度大於等於64,將單鏈錶轉化為紅黑樹。紅黑樹節點數量小於等於6的時候,又會重新轉換為單連結串列。
擴容機制:hashmap初始化時建立一個空的陣列,在第一次put值時陣列大小預設變成16。hashmap的負載因子是0.75,這樣閾值就是16*0.75=12。
hashmap元素個數大於等於閾值時,呼叫resize()觸發擴容。
resize():建立新的陣列代替原有容量小的陣列,每次擴容為原來的2倍。擴容後的物件要麼放在原來位置,要麼移動到原偏移量的兩倍的位置。
執行緒不安全:jdk1.7,新增資料遇到hash碰撞,採用的是頭插法,在多執行緒環境下會造成迴圈連結串列死迴圈。所以jdk1.8改用了尾插法。雖然避免了死迴圈,但是在多執行緒情況下,有資料覆蓋或者多次擴容發生。
執行緒不安全的替代品:ConCurrentHashMap
 
簡述從hashmap中get元素的過程?
先對key進行hash計算,得到的hash值跟陣列長度-1進行與運算,得到陣列下標。如果命中了桶的第一個節點,直接返回;發生hash衝突,通過key.equals()去找到對應的值。
簡述從hashmap中put元素的過程?
實際呼叫了putval()方法:
①先呼叫hash()方法,對key進行hash計算,得到的hash值跟陣列長度-1進行與運算,得到陣列下標。
②如果桶裡面為null,直接新建節點進行新增;
③如果桶裡不為空(發生了hash碰撞),有兩種情況:
如果桶裡首個元素和key相同(equals),則直接覆蓋value;
如果key不同,判斷是否為treeNode紅黑樹,如果是則直接在樹中插入鍵值對;否則就是連結串列,遍歷連結串列判斷key是否存在,存在就直接覆蓋value,不存在就插入節點。連結串列插入節點後判斷連結串列長度是否大於8,大於8就連結串列轉換為紅黑樹。最後,++size,判斷實際大小是否大於閾值,大於就要resize()擴容。
hashmap的hashcode方法如何實現?
將物件的實體地址轉化為一個整數,將整數通過hash計算得到hashcode。
 
3、1)介紹JVM幾種垃圾收集演算法?
標記-清除演算法Mark Sweep:
分為“標記”和“清除”兩個階段,首先標記出所有存活的物件,標記完成後統一回收所有沒有被標記的物件。它是最基礎的收集演算法,後續的演算法都
是在對其不足進行改進得到。
產生問題:1.效率問題 2.空間問題(產生大量不連續碎片)
複製演算法Copy:
為了解決效率問題。將記憶體分為相同兩塊,每次使用其中一塊,使用完後將還存活的物件複製到另一個記憶體塊去,然後把原來的記憶體塊全部清理
掉。這樣每次都是對一半記憶體區的回收。
標記-整理演算法Mark Compact:
先標記存活的物件,然後讓存活物件向一端移動,然後直接清理掉存活物件區域外的記憶體。
分代收集演算法(當前虛擬機器都使用):
根據物件存活週期的不同,將記憶體分為新生代和老年代。比如在新生代,每次收集都會有大量物件死去,採用複製演算法,只需要付出少量物件的複製成本,就可以完成每次垃圾收集。而老年代物件存活機率比較高,選擇“標記-清除”或“標記-整理”演算法進行垃圾收集。
2)新生代和老年代的垃圾收集器有哪些?
Serial收集器:單執行緒,只使用一個垃圾收集的執行緒,而且還會stop the world直到它收集結束。
ParNew收集器:Serial的多執行緒版本。
Parallel Scavenge收集器:關注的是吞吐量(高效率的利用CPU)。jdk1.8預設。
CMS收集器:關注的是使用者執行緒的停頓時間短。四個步驟:
初始標記:暫停所有的其他執行緒,把直接與root相連的物件記錄下來,速度很快。
併發標記:同時開啟GC和使用者執行緒,。。。
重新標記:修正併發標記期間,因為使用者執行緒繼續執行導致標記變動的記錄。
併發清除:開啟使用者執行緒,同時GC執行緒對未標記區域進行清除。
G1收集器:面向伺服器的GC。併發與並行,分代收集、空間整合、可預測的停頓。大致四個步驟:
初始標記:
併發標記:
重新標記:
篩選回收:
 
3)總結一下發生full GC的條件有哪些?
 
4)JVM的記憶體區域(執行時資料區)?這幾個區有什麼作用?為什麼分割槽?堆的內部結構?
堆:
存放物件例項,幾乎所有的物件例項以及陣列都在這裡分配記憶體。(JDK1.7開始預設開啟了逃逸分析,如果物件引用沒有被外面使用,也就是未逃逸出去,那麼物件可以直接在棧上分配記憶體)
堆還可以細分為:
JDK1.7:新生代(Eden、From Survivor、To Survivor),老年代,永生代。
JDK1.8:永久代被移除,取而代之的是元空間,元空間使用的是直接記憶體。(物理上永久代或元空間屬於堆,邏輯上屬於方法區)
方法區(1.8不同):
儲存被虛擬機器載入的類資訊、常量、靜態變數、即使編譯器編譯後的程式碼等資料。為永久代或元空間的邏輯部分。
虛擬機器棧:
每次方法呼叫的資料,都是通過虛擬機器棧來傳遞。虛擬機器棧由一個個棧幀組成,每次呼叫方法都有一個對應的棧幀被壓入棧,方法結束棧幀被彈出。每個棧幀都有區域性變數表、運算元棧、動態連結、方法出口資訊。
本地方法棧:
和虛擬機器棧類似,區別是,虛擬機器使用Native本地方法,就會在本地方法棧建立一個棧幀。
程式計數器:
1.在位元組碼直譯器通過改變程式計數器來依次讀取指令,來實現流程控制,如:順序執行、迴圈、異常處理。
2.多執行緒情況下,會發生上下文切換,程式計數器用於記錄當前執行緒執行的位置,在切換回來的時候知道執行緒上次執行到哪兒了。
 
4、JAVA執行緒池實現原理?(不會,底層原理挺難的)
執行緒池作用:減少每次獲取資源的消耗,提高對資源的利用率;提高響應速度;更加方便進行管理。
阿里開發手冊強制執行緒池不允許使用 Executors 去建立,而是通過 ThreadPoolExecutor 的方式建立。
執行緒池四大方法:Executors.newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、ScheduledThreadPool。
Executors建立FixedThreedPool建立固定執行緒池和SingleThreadExecutor:LinkedBlockingQueue,允許請求的佇列長度為 Integer.MAX_VALUE ,可能堆積大量的請求,從而導致 OOM。
Executors建立CachedThreadPool 和 ScheduledThreadPool:允許建立的執行緒數量為 Integer.MAX_VALUE,可能會建立大量執行緒,從而導致 OOM。
七大引數:
建議通過ThreadPoolExecutor 的構造方法建立。
ThreadPoolExecutor(int corePoolSize,核心執行緒數,執行緒數定義了最小可以同時執行的執行緒數量。 int maximumPoolSize,當佇列中存放的任務達到佇列容量的時候,當前可以同時執行的執行緒數量變為最大執行緒數。 long keepAliveTime,當執行緒池中的執行緒數量大於 corePoolSize 的時候,如果這時沒有新的任務提交,核心執行緒外的救急執行緒不會立即銷燬,而是會等待,直到等待的時間超過了 keepAliveTime才會被回收銷燬; TimeUnit unit,keepAliveTime 引數的時間單位。 BlockingQueue<Runnable> workQueue,當新任務來的時候會先判斷當前執行的執行緒數量是否達到核心執行緒數,如果達到的話,新任務就會被存放在佇列中。 ThreadFactory threadFactory,executor 執行緒工廠,建立新執行緒可以起名字、是否守護執行緒。 RejectedExecutionHandler handler)拒絕策略。
四種拒絕策略:
雙非Java的學習之旅以及秋招路程
 
Redis的資料結構?
String:由多個位元組組成,每個位元組8個bit,也是bitmap的資料結構。
List列表:相當於Java的LinkedList,雙向連結串列(當棧和佇列使用),插入、刪除快,查詢(lindex)慢。
Hash:相當於Java的HashMap(陣列+連結串列),無序字典。
Set集合:相當於Java的HashSet,無序且唯一。
Sorted set有序集合:相比於set增加了一個權重引數score,使集合中的元素可以有序排列。有點像HashMap和TreeSet的結合。zadd,zcard,zscore,zrange,zrevrange,zrem。
持久化:
快照(RDB):可以通過建立快照來獲得儲存在記憶體裡面的資料在某個時間點上的副本。資料體積小,從硬碟恢復到記憶體速度快。因為是一下子把記憶體中的資料存到硬碟上,比較耗時,產生阻塞,對其他業務有影響。(不適合實時去做,時候幾個小時進行一次備份)
日誌(AOF):每執行一個命令,把redis命令儲存。可以實時存,不斷追加,體積比較大。從硬碟恢復到記憶體速度慢。
如何解決快取一致性問題?
對於快取和資料庫的操作,主要有兩種方式。
方式一:先刪除快取,再更新資料庫(較多)
有髒資料問題:執行緒1快取刪除後,在更新資料庫前。執行緒2來讀快取,快取不存在,讀資料庫,此時資料庫讀到的是舊值,然後把舊值寫入快取,所以快取不一致。
解決方案:延時雙刪:先刪除快取,在更新資料庫時,其他執行緒發現沒有快取會讀資料庫舊值,然後把舊值新增到快取。所以在更新完資料庫後,sleep一段時間(大於其他執行緒讀寫快取的時間),然後再次刪除快取。
方案缺點:影響效能,sleep時間短第二次刪除還是會失敗。
方式二:先更新資料庫,再刪除快取。保證了最終一致性。(專案)使用快取的策略:Cache Aside Pattern(旁路快取模式)
併發問題:更新資料庫成功,刪除快取失敗,其他執行緒從快取讀的是舊值。
解決方案:訊息佇列:先更新資料庫,成功後往訊息佇列發訊息,消費到訊息後再刪除快取,藉助訊息佇列的重試機制來實現,達到最終一致性的效果。
方案缺點:問題變得更復雜。而且怎麼保證訊息不丟失。