一些雜想:Java老矣,尚能飯否?
來源:阿里開發者
阿里妹導讀:本文就Java真的老了嗎展開講述,詮釋了作者作為一名Java開發者的所思所感。
“落寞”的Java
"Write Once, Run Everywhere"的平臺無關特性在當年確實是真香,但現在這種部署的便利性已經完全可以交由Docker為代表的的容器提供了(從某種意義上說,JVM也是位元組碼的容器),而且做得更好,可以將整個執行環境進行打包。想想Docker的口號也是:"Build Once, Run Anywhere"。
Java 總體上是面向大規模、長時間執行的服務端應用而設計的。在語法層面,Java+Spring框架寫出的程式碼一致性很高;在執行期,有JIT編譯、GC等元件保障應用穩定可靠。這些特性對於企業級應用十分關鍵,曾經是Java最大的優勢之一。但在微服務化甚至Serverless化的部署形態下,有了高可用的服務叢集,也無須追求單個服務要 7×24 小時不可間斷地執行,它們隨時可以中斷和更新,Java的這一優勢無形中被削弱了。
另一個廣為詬病的是Java的資源佔用問題,這主要包含兩方面:靜態的程式大小和動態的記憶體佔用。
不管多大的應用,都要隨身帶一個臃腫的JRE環境(這裡先不討論模組化改造),加上各種複雜的Jar包依賴,看了下我們團隊的每個Java應用的容器映象大小都輕鬆上G。
應用的執行期記憶體佔用居高不下,這個是Java天生的缺陷,很難克服。
JDK的演進
Java 9:難產的模組化
不可忽視的改造成本
雖然提供了未命名模組和自動模組,Oracle也提供了遷移指南和工具[4]供參考,但改造的成本依舊很大,特別是梳理模組之間的依賴關係,較為繁瑣。
小心使用內部API
模組化的最大賣點之一是強大的封裝性,它確保非public類以及非匯出包中的類無法從模組外部訪問。但在這之前,jar包中類的訪問是沒有限制的(即使是private也可以透過反射訪問)。比如JDK中的大部分com.sun.* 和 sun.*包是內部無法訪問的,但這之前被用得很多(出於效能/向前相容等等原因),雖然Oracle的建議是不要使用這些類:Why Developers Should Not Write Programs That Call 'sun' Packages[5]。
小心使用內部JAR
像lib/rt.jar和lib/tools.jar等內部 JAR不能再訪問了。不過正常來說,應該只有IDE或類似工具會直接依賴?
小心使用JAR中的資源
一些API會在執行期獲取JAR中的資原始檔(例如透過ClassLoader.getSystemResource),在Java9之前會拿到 jar:file:<path-to-jar>!<path-to-file-in-jar>這類格式的URL Schema,而Java9之後則變成了 jrt:/<module-name>/<path-to-file-in-module>
其他一些問題[6]
我已經分成不同jar包了,我感覺這樣就可以了,有必要更進一步嗎? 我又不是開發中介軟體和框架的,我開發業務應用,為什麼要關心這些? 就算我有二方包要開放出去,為二方包維護模組定義似乎也帶不來多少收益? 該如何分離每個模組,基於什麼原則?就跟DDD一樣,我知道這東西很美好,有最佳實踐可以參考嗎?
compact strings[8],透過對底層儲存的最佳化來減少String的記憶體佔用。String物件往往是堆記憶體的大頭(通常來說可以達到25%),compact string可以減少最多一倍的記憶體佔用;
AOT編譯[9],一個實驗性的AOT編譯工具jaotc[10]。它藉助了Graal編譯器,將所輸入的Java類檔案轉換為機器碼,並存放至生成的動態共享庫之中。jaotc的一大應用便是編譯java.base module(也就是模組化後Java核心類庫中最為基礎的類)。這些類很有可能會被應用程式所呼叫,但呼叫頻率未必高到能夠觸發即時編譯。
JVMCI[11]( JVM 編譯器介面),另一個experimental的編譯特性。用Java寫Java編譯器,Java也可以說我能自舉了!
JVMCIJIT編譯器與JVM的互動可以分為如下三個方面。
響應編譯請求; 獲取編譯所需的後設資料(如類、方法、欄位)和反映程式執行狀態的profile; 將生成的二進位制碼部署至程式碼快取(code cache)裡。 即時編譯器透過這三個功能組成了一個響應編譯請求、獲取編譯所需的資料,完成編譯並部署的完整編譯週期。 傳統情況下,即時編譯器是與Java虛擬機器緊耦合的。也就是說,對即時編譯器的更改需要重新編譯整個Java虛擬機器。這對於開發相對活躍的Graal來說顯然是不可接受的。 為了讓Java虛擬機器與Graal解耦合,引入 JVMCI 將上述三個功能抽象成一個Java層面的介面。這樣一來,在Graal所依賴的JVMCI版本不變的情況下,我們僅需要替換Graal編譯器相關的jar包(Java 9以後的jmod檔案),便可完成對Graal的升級。 其實JVMCI介面就長這樣: public interface JVMCICompiler {
/**
* Services a compilation request. This object should compile the method to machine code and
* install it in the code cache if the compilation is successful.
*/
CompilationRequestResult compileMethod(CompilationRequest request);
}
Java 10:小升級
G1的多執行緒併發mark-sweep-compact:這個feature的背景是G1垃圾回收器在Java9中引入,但那會還使用單執行緒做mark-sweep-compact。
Application Class-Data Sharing[12]:透過在不同Java程式間共享應用類的後設資料來降低啟動時間和記憶體佔用,算是對Java 5引入的CDS的擴充套件,在這之前只支援Bootstrap Classloader載入的系統類。
其實這個特性還挺有用的,因為Java啟動慢很大一部分時間耗在類載入上,CDS生成的存檔類似於一個快照,在執行時可以直接做記憶體對映,還可以在多個JVM之間共享存檔檔案來減少記憶體佔用。這個JEP中也提了一嘴:對Serverless雲服務的分析表明,其中許多在啟動時載入了數千個應用程式類,AppCDS 可以讓這些服務快速啟動並提高整體系統響應時間。
Docker的支援[13]更好了,能認出Docker環境了。
Java 11:ZGC閃亮登場
Java 12:Shenandoah和記憶體返還
一方面,Java是一門有GC的語言,垃圾物件會持續佔用記憶體,直到下一次GC為止 另一方面,GC演算法也決定了更多的記憶體佔用,例如:
CMS的做法是在老年代達到指定的佔用率後(Java 6後預設為92%)開始GC,可以透過-XX:CMSInitiatingOccupancyFraction引數調高這個值,但調得太高又容易碰到Concurrent Mode Failure;
G1的解法則是為每一個Region設計了兩個名為TAMS(Top at Mark Start)的指標,把Region中的一部分空間劃分出來用於併發回收過程中的新物件分配,併發回收時新分配的物件地址都必須要在這兩個指標位置以上,並且預設不回收在這個地址以上的物件。
Java 13:小升級+1
ZGC的增強[20]:同G1和Shenandoah一樣,可以將未使用的記憶體返還給作業系統了
AppCDS的增強[21]:在Java10的AppCDS基礎上支援動態歸檔,可以在程式退出時自動建立
Java 14:小升級+2
ZGC支援Mac和Windows了(不過大部分生產環境應該不會用這倆?)
G1支援Numa-Aware的記憶體分配[22]:NUMA(Non-Uniform Memory Access,非統一記憶體訪問架構)的介紹可以參考下這篇文章:【計算機體系結構】NUMA架構詳解[23]。在NUMA架構下,G1收集器會優先嚐試在請求執行緒當前所處的處理器的本地記憶體上分配物件,以保證高效記憶體訪問。在G1之前的收集器就只有針對吞吐量設計的Parallel Scavenge支援NUMA記憶體分配,如今G1也成為另外一個選擇。
Java 15:ZGC和Shenandoah轉正
Java 16:Alipine Linux的支援
ZGC支援併發執行緒堆疊處理[24]
彈性元空間[25]:一般Java程式裡元空間(metaspace)的記憶體佔用相比起堆來說不算高,但也很容易出現出現記憶體浪費。Java 16最佳化了元空間的記憶體分配機制來減少記憶體佔用。
Java 17:最新的LTS版本
Project X
Project Amber[30]:旨在探索和孵化更小的、以生產力為導向的 Java 語言功能,每個提案的特性都不大,很多已經落地到不同JDK版本中了,像是Records[31]、Sealed Class[32]、Pattern Matching、Text Blocks[33]等等。
Project Leyden[34]:旨在解決Java的啟動時間、TTP(Time to Peak)效能、記憶體佔用等頑疾。一個特性即是AOT編譯,但難度太大,短期內指望不上,先寄希望於GraalVM。
Project Loom[35]:Java的協程和結構化併發[36]。
Project Valhalla[37]:旨在探索和孵化高階Java VM和語言特性,例如值型別(Value types)[38]和基於值型別的泛型[39]。
Project Portola[40]:將 OpenJDK 向 Alpine Linux 移植,在Java 16中已經得到了落地。
Project Panama[41]: 更好地跟原生程式碼(主要是C程式碼)互動。
Project Lilliput[42]:將物件頭縮減到64bit來降低記憶體佔用。
提前編譯-AOT
啟動慢,Java啟動需要初始化虛擬機器,載入大量的類
預熱慢,在JIT編譯器介入前,需要在解釋模式下執行
Java是一門跨平臺語言,但JVM並不是跨平臺的,Java將原始碼編譯成位元組碼,交給JVM執行,這中間裝載的開銷很高。
一段程式想要被載入需要經過的流程:
new 位元組碼或者 static 相關位元組碼觸發類載入 從一系列 jar 包中找到感興趣的 class 檔案 將 class 檔案的讀取到記憶體裡的 byte 陣列 defineClass,包括了 class 檔案的解析、校驗、連結 類初始化(static 塊,或者靜態變數初始化) 開始解釋執行 2000 次解釋後被 client compiler JIT 編譯,隨後 15000 次執行後被 server compiler JIT 編譯
峰值效能:AOT編譯不像JIT編譯一樣能收集程式執行時的資訊,因此也無法進行一些更激進的最佳化,例如基於類層次分析的完全虛方法內聯,或者基於程式profile的投機性最佳化(不過這並非硬性限制,我們可以透過限制執行範圍,或者利用上一次執行的程式profile來繞開這些限制)。
構建時長:從目前的實測資料看,像Graal編譯器花的構建時間都比正常編譯時間要長。不過這個也在情理之中,畢竟一個只需要把程式碼編譯成位元組碼,一個則需要掃描然後分析程式所有的依賴做靜態編譯。
在生產的本地映象(Native Image)中使用Java agents,JMX,JVMTI,JFR等元件會有一些限制。
(最關鍵的)動態特性的支援:AOT編譯很美好,但是在Java中實現起來卻很困難,主要的原因在於Java雖然是一門靜態語言,但是也包含了很多動態特性,比如反射、動態代理、動態類載入、位元組碼Instrument (BCI) 等等,而提前編譯要求滿足封閉世界假設( closed world assumption),在編譯期就確定程式用到的類。
這是一個很簡單的取捨問題,因為動態特性在Java中用得實在是太普遍了,不管是Spring、Hibernate這些應用框架還是CGLib這類位元組碼生成庫,大部分生產力工具都依賴這些動態特性,所以Java的提前編譯至今還是Experimental狀態。
目前來看使用AOT難免需要有一些折中,例如後面要講到的Substrate VM就要求以配置的方式明確告知編譯器程式程式碼中有哪些方法是隻透過反射來訪問的,哪些類會被動態載入等等。然而另一些功能可能只能妥協或者放棄了,就像動態生成位元組碼這類十分常用的功能,我們熟知的Spring預設就會使用CGLib生成動態代理。從 Spring Framework 5.2 開始增加了@proxyBeanMethods註解來排除對 CGLib 的依賴,僅使用標準的動態代理去增強類,但這也就限制了動態代理的能力。
協程(虛擬執行緒)
協程是協作式的,執行緒是搶佔式;
協程在使用者模式下,由應用程式排程管理,而執行緒則由作業系統核心管理;
(有棧)協程擁有自己的暫存器上下文和棧,但比執行緒要小得多(MB和KB級別的差距),切換也快得多;
一個執行緒可以包含一個或多個協程,即不同的協程可以在一個執行緒上被排程。協程也被稱為輕量級執行緒,有意思的是執行緒有時候也被成為輕量級程式;
1:1的模型對於計算密集型任務這很合適,既不用自己去做排程,也利於一條執行緒跑滿整個處理器核心;但對於 I/O 密集型任務,譬如訪問磁碟、訪問資料庫佔主要時間的任務,這種模型就顯得成本高昂,主要在於記憶體消耗和上下文切換上:64 位 Linux 上 HotSpot 的執行緒棧容量預設是 1MB,執行緒的核心後設資料(Kernel Metadata)還要額外消耗 2-16KB 記憶體,所以單個虛擬機器的最大執行緒數量一般只會設定到 200 至 400 條,當程式設計師把數以百萬計的請求往執行緒池裡面灌時,系統即便能處理得過來,其中的切換損耗也是相當可觀的。
在此之前,Java中已經有一些三方的實現支援協程,比如Quasar[49]和Coroutines[50],貌似都是需要掛載agent利用位元組碼注入的方式實現,我沒有細看,有興趣的可以瞭解下。
併發任務的數量很高(超過幾千個)
工作負載不受 CPU 限制,換句話說是I/O密集型的任務。如果是計算密集型任務,擁有比處理器核心多得多的執行緒並不能提高吞吐量
舉個例子,假設有這樣一個場景,需要同時啟動10000個任務做一些事情:
// 建立一個虛擬執行緒的Executor,該Executor每執行一個任務就會建立一個新的虛擬執行緒try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 10_000).forEach(i -> { executor.submit(() -> { doSomething(); return i; }); });} // executor.close() is called implicitly, and waits
把Executors.newVirtualThreadPerTaskExecutor()換成Executors.newCachedThreadPool()。結果是程式會崩潰,因為大多數作業系統和硬體不支援這種規模的執行緒數。 換成Executors.newFixedThreadPool(200)或者其他自定義的執行緒池,那這10000個任務將會共享200個執行緒,許多工將按順序執行而不是同時執行,並且程式需要很長時間才能完成。
如果doSomething()裡執行的是某類計算任務,例如給一個大陣列排序,那麼虛擬執行緒還是平臺執行緒都無濟於事。JEP中提到了很關鍵的一點就是:虛擬執行緒不是更快的執行緒—它們執行程式碼的速度並不比平臺執行緒快。它們的存在是為了提供scale(更高的吞吐量),而不是speed(更低的延遲)。
虛擬執行緒會保持原有統一執行緒模型的互動方式,通俗地說就是原有的 Thread、Executor、Future、ForkJoinPool 等多執行緒工具都應該能以同樣的方式支援新的虛擬執行緒。使用虛擬執行緒的程式碼可能長這樣:
// 直接建立一個虛擬執行緒
Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello"));
// 透過builder建立一個虛擬執行緒
Thread virtualThread = Thread.builder().virtual().task(() -> {
System.out.println("Fiber Thread: " + Thread.currentThread().getName());
}).start();
// 建立一個基於虛擬執行緒的ExecutorService
ExecutorService executor = Executors.newVirtualThreadExecutor()
虛擬執行緒既便宜又量大管飽,因此永遠不應該被池化。大多數虛擬執行緒將是短暫的並且具有淺層呼叫棧,執行的任務像是單個 HTTP 客戶端呼叫或單個 JDBC 查詢這樣的I/O操作。相比之下,執行緒是重量級且昂貴的,因此通常必須被池化。 JDK的虛擬執行緒排程會藉助ForkJoinPool[52],以 FIFO 模式執行。
值型別
記憶體延遲與處理器執行效能之間的馮諾依曼瓶頸[54](Von Neumann Bottleneck)增加了100-2000倍(也就是說,如果以CPU算術計算的速度為基準看,讀記憶體的速度沒有變快反而更慢了);
指標的間接獲取對效能的影響變得更大,因為對指標的解引用是昂貴的操作,尤其是當指標或它指向的物件不在處理器的快取中時(沒辦法,只能讀記憶體了);
Java透過物件識別符號進行鏈式訪問,與之相對的是集中訪問模式,例如C/C++中的struct會將物件在記憶體中拍平。兩者的關鍵區別在於,鏈式訪問需要讀多次記憶體才能命中,而集中訪問一次就可以將相關資料全部取出。打個比方,類A中包含類B,類B中包含類C,從A->B->C,鏈式訪問在最壞情況下要讀3次記憶體;而集中訪問只需要讀一次。
final class Point { final int x; final int y;}
值型別的記憶體佈局可以像基礎型別一樣平坦緊湊,其他物件或陣列在引用值型別時更簡單;
同樣也不需要object header了,可以省去記憶體佔用和分配的開銷;
甚至JVM可以在棧上直接分配值型別,而不必在堆上分配它們;
可以使用inline關鍵詞定義一個值型別:
inline public class Point {
public int x;
public int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
可以有變數+方法
可以繼承介面,例如Point可以從某個Shape介面繼承而來
可以透過封裝來隱藏內部實現
可以作為泛型使用,可以有泛型引數
隊友的助攻
GraalVM
Graal - 用Java寫的編譯器,既可以作為 JIT 編譯器取代C2在傳統的OpenJDK JVM上執行,又可以當做AOT編譯器使用。
Substrate VM - 是一個構建在Graal編譯器之上的,支援AOT編譯的執行框架。它的設計初衷是提供一個快速啟動,低記憶體佔用,以及能無縫銜接C程式碼(與JNI相比)的runtime,並能完美適配Truffle[59]語言實現。
Truffle - 即下圖中的語言實現框架(Language Implementation Framework),用來支援多種語言跑在GraalVM上。
我們熟知的HotSpot有兩個JIT編譯器,C1和C2。Java 程式首先在解釋模式下啟動,執行一段時間後,經常被呼叫的方法會被識別出來,並使用 JIT 編譯器進行編譯——先是使用 C1,如果 HotSpot 檢測到這些方法有更多的呼叫,就使用 C2 重新編譯這些方法。這種策略被稱為“分層編譯”,是 HotSpot 預設採用的方式。經過這麼多年最佳化下來,C2編譯後的程式碼效率非常出色,可以與 C++ 相媲美(甚至更快)。不過,近年來 C2 並沒有帶來多少重大的改進。不僅如此,C2 中的程式碼變得越來越難以維護和擴充套件,新加入的工程師很難修改使用 C++ 特定方言編寫的程式碼。
native image builder:使用Graal編譯器做靜態編譯的工具,它處理應用程式的所有類和依賴項(包括來自JDK的部分),透過指標分析(Points-To Analysis)來確定在應用程式執行期間可以訪問哪些類和方法,然後提前將可訪問的程式碼和資料編譯為特定作業系統和架構的可執行檔案或者動態連結庫。
SubstrateVM Runtime:一個特殊的精簡過的VM Runtime,包括了deoptimizer、GC、執行緒排程等元件。因為已經做了AOT編譯,比傳統的Runtime少了類載入、直譯器、JIT等元件。
官網放了一張圖來展示Graal Native Image的兩大優勢:快速啟動和低記憶體佔用。不過我看到的其他一些資料上說在低時延和高吞吐(Latency/Throughput)場景下並不佔優。
動態類載入:對於像Class.forName("myClass”)一類動態按照類名載入的操作,必須在配置檔案裡配上myClass,否則執行期就是一個ClassNotFoundException;
反射:構建時會透過檢測對反射 API 的呼叫做靜態分析,對於無法透過靜態分析獲知的,那也只能配置了;
動態代理:這裡指的是使用了java.lang.reflect.Proxy API的動態代理。要求動態代理的介面列表在構建期就是已知的,構建時會簡單地攔截對java.lang.reflect.Proxy.newProxyInstance(ClassLoader, Class<?>[], InvocationHandler)和java.lang.reflect.Proxy.getProxyClass(ClassLoader, Class<?>[])的呼叫來確定介面列表。同樣,如果分析失敗,那也只能配置了;
JNI:本機程式碼可以按名稱訪問 Java 物件、類、方法和欄位,其方式類似於在 Java 程式碼中使用反射 API。一種替代的方式是可以考慮使用GraalVM提供的原生介面org.graalvm.nativeimage.c[65],更簡單開銷更低,缺點是不允許從 C 程式碼訪問 Java 資料結構;
序列化:Java 序列化需要類的後設資料資訊才能起作用,因此也需要提前配置(不過,你的程式碼裡還在用 Java 序列化嗎?);
還有一些限制條件,像是invokedynamic位元組碼和Security Manager,是直接無法相容的。還有一些功能跟HotSpot有區別,具體可以參考這篇文件[66]。
Truffle
完整的列表參考這裡[72]。
const express = require('express')
const app = express()
const BigInteger = Java.type('java.math.BigInteger')
app.get('/', function (req, res) {
var text = 'Hello World from Graal.js!<br> '
// Using Java standard library classes
text += BigInteger.valueOf(10).pow(100)
.add(BigInteger.valueOf(43)).toString() + '<br>'
// Using R methods to return arrays
text += Polyglot.eval('R',
'ifelse(1 > 2, "no", paste(1:42, c="|"))') + '<br>'
// Using R interoperability to create graphs
text += Polyglot.eval('R',
`svg();
require(lattice);
x <- 1:100
y <- sin(x/10)
z <- cos(x^1.3/(runif(1)*5+10))
print(cloud(x~y*z, main="cloud plot"))
grDevices:::svg.off()
`);
res.send(text)
})
app.listen(3001, function () {
console.log('Example app listening on port 3001!')
})
關於spring-native,ATA上已經有大佬們做過比較深入的分析了,比如:讓Spring啟動提速95.5倍,專案解讀之Spring-Graalvm-Native,也可以參考下官方的announcing-spring-native-beta[75]。
其他:Quarkus/Micronut/Helidon等等
Cloud Native Container First GraalVM Reactive Fast Boot And Low Memory Footprint
未來?
更具生產力的語法和API改進 以ZGC為代表的更先進的GC 在啟動速度、記憶體佔用等短板上的各種最佳化 以GraalVM為代表的新編譯器+Native Image+多語言程式設計 更好的雲原生支援
參考連結:
[1]
[2]
[3]projects/jigsaw
[4]
[5]
[6]https://nipafx.dev/java-9-migration-guide
[7]
[8]jeps/254
[9]
[10]
[11]
[12]jeps/310
[13]https://www.docker.com/blog/improved-docker-container-integration-with-java-10
[14]jeps/333
[15]
[16]https://blogs.oracle.com/javamagazine/post/understanding-the-jdks-new-superfast-garbage-collectors
[17]jeps/318
[18]jeps/189
[19]jeps/346
[20]jeps/351
[21]jeps/350
[22]jeps/345
[23]
[24]jeps/376
[25]jeps/387
[26]jeps/386
[27]
[28]
[29]projects
[30]projects/amber
[31]jeps/395
[32]jeps/409
[33]jeps/378
[34]https://mail.openjdk.java.net/pipermail/discuss/2020-April/005429.html
[35]
[36]jeps/8277129
[37]projects/valhalla
[38]jeps/169
[39]jeps/218
[40]projects/portola
[41]projects/panama
[42]
[43]
[44]協程
[45](computing)
[46](computing)
[47]jeps/425
[48]
[49]
[50]
[51]jeps/425
[52]https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/util/concurrent/ForkJoinPool.html
[53]
[54]
[55](object-oriented_programming)
[56]jeps/218
[57]
[58]
[59]
[60]
[61]
[62]
[63]/native-image
[64]https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/BuildConfiguration.md
[65]
[66]https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/Limitations.md
[67]
[68]
[69]
[70]
[71]
[72]/22.0/graalvm-as-a-platform/language-implementation-framework/Languages
[73]
[74]
[75]https://spring.io/blog/2021/03/11/announcing-spring-native-beta
[76]
[77]
[78]
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2950392/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- JQuery老矣,尚能飯否jQuery
- ASP已老,尚能飯否?
- 2023年的“大航海”遊戲,尚能飯否?遊戲
- 押注社群電商,王興的中年美團尚能“飯否”?
- 一些雜感雜想(一)談談加班、團隊
- 我對微服務、SpringCloud、k8s、Istio的一些雜想微服務SpringGCCloudK8S
- DevOps - DevOps隨想雜談dev
- 遊戲想自個兒恰飯有多難?遊戲
- 知否知否,Java 面試的年貨到了Java面試
- 遊戲記:網咖想搶電影院的飯碗遊戲
- 一些“最短路”雜燴
- 一些“並查集”雜燴並查集
- python爬蟲遠比我們想的複雜Python爬蟲
- netty應用架構的一些設想Netty應用架構
- 一些CTF雜項MISC解題指令碼指令碼
- JAVA 實現 - 雜湊表Java
- 知否?知否?Vue之MVVMVueMVVM
- 開始寫部落格前一些想說的
- 公告:關於精準測試一些雜事
- 關於定時任務的一些雜談
- 想學Java要啥基礎?Java
- JAVA 解析複雜的json字串JavaJSON字串
- Java雜記17—String全面解析Java
- Java IO的一些思考Java
- 大佬老矣,企二代繼承者們如何再續輝煌?繼承
- Test Cases(或者說Corner Cases),如何想的更全面一些?
- 知否?知否?情人眼裡出程式碼
- 關於 Elasticsearch nested field /script 的一些複雜查詢Elasticsearch
- 做飯裝備
- java多執行緒的雜談Java執行緒
- 明叔雜談:對聯想的批評應該實事求是
- 【Java小疑問】java變數儲存的位置(雜)Java變數
- Hadoop老矣,為什麼騰訊還要花精力在其開源釋出上?Hadoop
- Java淺Copy的一些事Java
- java裡的一些hash方法Java
- 寫給那些想自學java的同伴Java
- JAVA資料結構之雜湊表Java資料結構
- Java雜記2—運算子和表示式Java