深入Java aqs源碼:cancelacquire方法中node.next = node; 的gc優化
在深入研究Java并發包中的AQS(AbstractQueuedSynchronizer)源碼時,我們常常會遇到cancelAcquire方法中的一句代碼:node.next = node; // help GC。這行代碼看似簡單,卻引出了一個關于垃圾回收機制的優化問題:為什么將節點的next指針指向自身能夠幫助垃圾回收?
許多開發者初看這段代碼會感到困惑,因為cancelAcquire方法并沒有直接負責取消節點的內存回收。事實上,真正移除取消的節點的工作是由其他方法,例如acquireQueued完成的。那么,node.next = node;究竟有何作用呢?
問題的關鍵在于jvm的垃圾回收機制以及跨代引用問題。即使一個節點已經被從AQS隊列中移除,使其不可達,但如果該節點已經晉升到老年代,minor GC仍然無法回收它。更重要的是,如果這個老年代節點還持有對年輕代中其他節點的引用(例如,通過next指針),那么這些年輕代節點也無法被回收,即使它們本身也已經被從隊列中移除。這便是跨代引用的問題,它會造成內存泄漏,并最終導致頻繁的Full GC,影響程序性能。
node.next = node; 正是為了解決這個問題而設計的。通過將next指針指向自身,我們有效地切斷了該節點與年輕代其他節點的引用鏈。即使該節點在老年代,也不會再阻止年輕代中其他已取消節點的回收。雖然節點本身仍然不可達,需要等待Full GC回收,但至少避免了跨代引用導致的年輕代內存堆積。
立即學習“Java免費學習筆記(深入)”;
值得注意的是,將next指針設為NULL并非最佳選擇,因為在AQS中,next指針指向null具有特殊含義——表示隊列尾部。
此外,AQS是雙向鏈表,理想情況下應該同時處理prev指針。然而,在其他移除取消節點的方法中,例如acquireQueued,并沒有對prev指針進行類似處理。這說明,雖然node.next = node;能夠有效優化GC,但仍存在潛在的跨代引用問題,一個取消的節點可能由于prev指針的跨代引用,影響其前驅節點的回收。
據了解,在JDK17及更高版本中,AQS的cancelAcquire方法已經移除了node.next = node;這行代碼。這暗示著JDK17及更高版本的GC機制可能已經能夠更好地處理跨代引用問題,不再需要這種顯式的優化手段。 但理解這一優化策略背后的原因,仍然有助于我們深入理解Java并發編程和JVM垃圾回收機制的細節。