0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

深入淺出解析JVM中的Safepoint

OSC開源社區(qū) ? 來源:得物技術(shù) ? 2023-06-01 09:25 ? 次閱讀

1

初識Safepoint-GC中的Safepoint

最早接觸JVM中的安全點概念是在讀《深入理解Java虛擬機》那本書垃圾回收器章節(jié)的內(nèi)容時。相信大部分人也一樣,都是通過這樣的方式第一次對安全點有了初步認識。不妨,先復習一下《深入理解Java虛擬機》書中安全點那一章節(jié)的內(nèi)容。

書中是在講解垃圾收集器-垃圾收集算法的章節(jié)引入安全點的介紹,為了快速準確地完成GC Roots枚舉,避免為每條指令都生成對應(yīng)的OopMap造成大量存儲空間的浪費,只在“特定的位置”生成對應(yīng)的OopMap,這些位置被稱為安全點。然后,書中提到了安全點位置的選擇標準是:是否能讓程序長時間執(zhí)行;所以會在方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等處才會產(chǎn)生安全點。

書中還提到了JVM如何在GC時讓用戶線程在最近的安全點處停頓下來:搶先式中斷和主動式中斷。搶先式中斷不需要線程的執(zhí)行代碼主動去配合,在GC發(fā)生時,系統(tǒng)首先把所有用戶線程全部中斷,如果發(fā)現(xiàn)有用戶線程中斷的地方不在安全點上,就恢復這條線程執(zhí)行,讓它一會再重新中斷,直到跑到安全點上。而主動式中斷的思想是當GC需要中斷線程時,不直接對線程操作,僅僅簡單地設(shè)置一個標志位,各個線程執(zhí)行過程時不停地主動去輪詢這個標志,一旦發(fā)現(xiàn)中斷標志為真就自己在最近的安全點上主動中斷掛起?,F(xiàn)在基本上所有虛擬機實現(xiàn)都采用主動式中斷方式來暫停線程響應(yīng)GC事件。

總結(jié)一下初識安全點學到的知識點:

JVM GC時需要讓用戶線程在安全點處停頓下來(Stop The World)

JVM會在方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等處放置安全點

JVM通過主動中斷方式到達全局STW:設(shè)置一個標志位,各個線程執(zhí)行過程時不停地主動去輪詢這個標志,一旦發(fā)現(xiàn)中斷標志為真就自己在最近的安全點上主動中斷掛起。

以上基本上就是《深入理解Java虛擬機》這本書對JVM安全點的所有介紹了,當時覺得安全點還是很好理解,認為安全點就是在垃圾回收時為了STW而設(shè)計的。

后來發(fā)現(xiàn),經(jīng)過一些線上問題和網(wǎng)上看到有關(guān)安全點有趣的示例,發(fā)現(xiàn)安全點其實也不簡單,不只有GC才會用到安全點;簡單的代碼如果寫的不當,安全點也會帶來一些莫名其妙的問題;其在JVM內(nèi)部的實現(xiàn)以及JIT對它的優(yōu)化,也經(jīng)常讓人摸不著頭腦。本文嘗試在初識安全點后已知知識點的基礎(chǔ)上,通過一段簡單的示例代碼,多問幾個為什么,來進一步更全面的了解一下安全點。

2

通過一段示例代碼深入剖析Safepoint

2.1 示例代碼

這段示例代碼可直接復制到本地運行,本文所有對示例代碼的運行環(huán)境都是jdk 1.8。

public class SafePointTest {


    public static AtomicInteger counter = new AtomicInteger(0);


    public static void main(String[] args) throws Exception{
        long startTime = System.currentTimeMillis();
        Runnable runnable = () -> {
            System.out.println(interval(startTime) + "ms后," + Thread.currentThread().getName() + "子線程開始運行");
            for(int i = 0; i < 100000000; i++) {
                counter.getAndAdd(1);
            }
            System.out.println(interval(startTime) + "ms后," + Thread.currentThread().getName() + "子線程結(jié)束運行, counter=" + counter);
        };


        Thread t1 = new Thread(runnable, "zz-t1");
        Thread t2 = new Thread(runnable, "zz-t2");


        t1.start();
        t2.start();


        System.out.println(interval(startTime) + "ms后,主線程開始sleep.");


        Thread.sleep(1000L);


        System.out.println(interval(startTime) + "ms后,主線程結(jié)束sleep.");
        System.out.println(interval(startTime) + "ms后,主線程結(jié)束,counter:" + counter);
    }


    private static long interval(Long startTime) {
        return System.currentTimeMillis() - startTime;
    }
}
示例代碼中主線程啟動兩個子線程,然后主線程睡眠1s,通過打印時間來觀察主線程和子線程的執(zhí)行情況。 按道理來說這里主線程和兩個子線程獨立并發(fā),沒有任何顯性的依賴,主線程的執(zhí)行是不會受子線程影響的:主線程睡眠結(jié)束后會直接結(jié)束。但是執(zhí)行結(jié)果卻和期望不一樣。 執(zhí)行結(jié)果如下方動圖展示:

b01002f2-000a-11ee-90ce-dac502259ad0.png

從執(zhí)行結(jié)果看,主線程在啟動兩個線程后進入睡眠狀態(tài),代碼中指定睡眠時間為1s,但是主線程卻在3s多之后才睡眠結(jié)束。是什么導致了主線程睡過頭了呢,從結(jié)果來看主線程睡覺結(jié)束時間和子線程結(jié)束時間是一致的。所以,我們有理由懷疑主線程沒有按時提前結(jié)束應(yīng)該是被兩個子線程阻塞了。

2.2 先給結(jié)論

由于VMThread的某些操作需要STW,主線程在sleep結(jié)束前進入了JVM全局安全點,然后主線程要等待其他線程全部進入安全點,所以主線程被長時間沒有進入安全點的其他線程給阻塞了。

2.3 驗證結(jié)論

添加JVM打印安全點日志參數(shù)-XX:+PrintSafepointStatistics后再執(zhí)行上面的實例代碼,結(jié)果如下截圖:

b024f176-000a-11ee-90ce-dac502259ad0.png

可以從安全點日志中看到,JVM想要執(zhí)行no vm operation,這個操作需要線程進入安全點,整個期間有12個線程,正在運行的線程有兩個,需要等待這兩個線程進入安全點,等待耗時2251ms。

加上 -XX:+SafepointTimeout 和-XX:SafepointTimeoutDelay=2000 參數(shù)后執(zhí)行代碼可以進一步看等待哪兩個線程進入安全點。

b05281e0-000a-11ee-90ce-dac502259ad0.png

果然和猜測的一樣,沒有到達安全點的兩個線程正是示例代碼中定義的zz-t1和zz-t2線程。

2.4 為什么

到這里這個示例的執(zhí)行結(jié)果的原因已經(jīng)有了結(jié)論并且得到了驗證,基本上已經(jīng)知其然了。但是如果深入思考一下,初識安全點時學到的知識點還不能解釋,所以為了知其所以然,這里提了幾個為什么。

(1)為什么會進入安全點

換句話問,是什么觸發(fā)了進入安全點?

由初識安全點得到的基礎(chǔ)知識知道進入安全點需要兩個條件:

JVM操作設(shè)置了主動中斷標志

運行的代碼中存在安全點

首先想到的是GC觸發(fā)JVM設(shè)置主動中斷標志,加上 -XX:-PrintGC再執(zhí)行示例代碼并沒有打印 GC 日志,可以排除掉GC。

既然不是GC,還是再回到安全點日志上尋找線索吧,發(fā)現(xiàn)有個vmop(虛擬機操作類型):no vm operation關(guān)于no vm operation,網(wǎng)上有大神通過解析JVM源碼得到了結(jié)論,這里不對JVM源碼展開做詳細解讀,直接給結(jié)論:

在 JVM 正常運行的時候,如果設(shè)置了進入安全點的間隔,就會隔一段時間判斷是否有代碼緩存要清理,如果有,會進入安全點。這個觸發(fā)條件不是 VM 操作,所以會將 _vmop_type 設(shè)置成-1,輸出日志的時候打印對應(yīng)的 「no vm operation」,也就是我們看到的安全點日志。

在 VM 操作為空的情況下,只要滿足以下 3 個條件,也是會進入安全點的:

1、VMThread 處于正常運行狀態(tài)

2、設(shè)置了進入安全點的間隔時間

3、SafepointALot 是否為 true 或者是否需要清理

用 Java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal 2>&1 | grep Safepoint 命令查看 JVM 關(guān)于安全點的默認參數(shù):

b0805d72-000a-11ee-90ce-dac502259ad0.png

發(fā)現(xiàn) GuaranteedSafepointInterval 默認設(shè)置成了 1 秒,每隔1s就會嘗試進入安全點。

那么,修改GuaranteedSafepointInterval參數(shù)值,看看是否能阻止進入安全點。

GuaranteedSafepointInterval參數(shù)是JVM診斷參數(shù),修改這個參數(shù)的值,需要配合-XX:+UnlockDiagnosticVMOptions一起使用。

另外不建議在線上對這個參數(shù)的值做修改。

關(guān)閉定時進入安全點

通過 -XX:GuaranteedSafepointInterval = 0 關(guān)閉定時進入安全點,看看代碼運行結(jié)果是怎么樣的

b0a86696-000a-11ee-90ce-dac502259ad0.png

由運行結(jié)果可以看出,關(guān)閉定時進入安全點后,主線程睡眠1s后正常結(jié)束,不受其他線程阻塞。從安全點日志看,之前等待進入安全點的兩個線程也沒有了。

調(diào)大定時進入安全點間隔時間

由打印的執(zhí)行結(jié)果可以看到子線程運行時間是3s多,如果把進入安全點間隔時間調(diào)整為5s,即在子線程結(jié)束之后再嘗試進入安全點是不是也能避免等待子線程進入安全點呢? 修改參數(shù)-XX:GuaranteedSafepointInterval = 5000 調(diào)整安全點間隔時間再次執(zhí)行結(jié)果:

b0d3f2c0-000a-11ee-90ce-dac502259ad0.png

從執(zhí)行結(jié)果可以看出,調(diào)大安全點間隔時間和關(guān)閉定時進入安全點的效果是一樣的,也可以避免等待子線程進入安全點的。

(2)主線程是在哪里進入的安全點

從示例代碼在默認JVM參數(shù)執(zhí)行結(jié)果看,主線程睡眠時間超過了3s,事實上主線程是在Thread.sleep()方法內(nèi)部進入安全點。這里對JVM 安全點實現(xiàn)的源碼簡單做一下分析:

Safepoint實現(xiàn)源代碼:Safepoint.cpp

b1138e4e-000a-11ee-90ce-dac502259ad0.png

讀源碼太費勁,看注釋吧,所幸從注釋中也能找到答案。上面截圖的注釋說在程序進入 Safepoint 的時候,Java 線程可能正處于的五種不同的狀態(tài),針對不同的狀態(tài)的不同處理機制。假設(shè)現(xiàn)在有一個操作觸發(fā)了某個 VM 線程所有線程需要進入 SafePoint,如果其他線程現(xiàn)在:

運行字節(jié)碼:運行字節(jié)碼時,解釋器會看線程是否被標記為 poll armed,如果是,VM 線程調(diào)用 SafepointSynchronize::block(JavaThread *thread)進行 block。

運行 native 代碼:當運行 native 代碼時,VM 線程略過這個線程,但是給這個線程設(shè)置 poll armed,讓它在執(zhí)行完 native 代碼之后,它會檢查是否 poll armed,如果還需要停在 SafePoint,則直接 block。

運行 JIT 編譯好的代碼:由于運行的是編譯好的機器碼,直接查看本地 local polling page 是否為臟,如果為臟則需要 block。這個特性是在 Java 10 引入的 JEP 312: Thread-Local Handshakes 之后,才是只用檢查本地 local polling page 是否為臟就可以了。

處于 BLOCK 狀態(tài):在需要所有線程需要進入 SafePoint 的操作完成之前,不許離開 BLOCK 狀態(tài)

處于線程切換狀態(tài)或者處于 VM 運行狀態(tài):會一直輪詢線程狀態(tài)直到線程處于阻塞狀態(tài)(線程肯定會變成上面說的那四種狀態(tài),變成哪個都會 block 住)。

再看一下Thread.sleep方法的聲明,就和上面Safepoint.cpp源碼注釋截圖紅框?qū)ι狭?,Thread.sleep正是一個native方法。

b15f3650-000a-11ee-90ce-dac502259ad0.png

Thread.sleep(0)在RocketMQ中的妙用

b1aa21c4-000a-11ee-90ce-dac502259ad0.png

上面這段代碼是RocketMQ的一段代碼,16年最早版本的實現(xiàn)for循環(huán)內(nèi)每循環(huán)1000次會調(diào)用一次Thread.sleep(0),這貌似是一段無用的代碼,作者真實的目的是為了在這里放置一個安全點,避免for循環(huán)運行時間過長導致系統(tǒng)長時間SWT。從代碼的變更記錄看,22年9月份有人對這段代碼換了一種寫法:把for循環(huán)變量類型定義成long型,同時注釋掉了循環(huán)內(nèi)部Thread.sleep(0)代碼,為什么可以這樣寫以及為什么要這樣寫這里先按下不表。

(3)子線程為什么無法進入安全點

現(xiàn)在已經(jīng)知道了主線程為什么進入會進入安全點,以及主線程在哪里進入的安全點,按照已知知識點JVM會在循環(huán)跳轉(zhuǎn)處和方法調(diào)用處放置安全點,為什么子線程沒有進入安全點?

可數(shù)循環(huán)和不可數(shù)循環(huán)

JVM為了避免安全點過多帶來過重的負擔,對循環(huán)有一項優(yōu)化措施,認為循環(huán)次數(shù)較少的話,執(zhí)行時間應(yīng)該不會太長,所以使用int類型和范圍更小的數(shù)據(jù)類型作為索引值的循環(huán)默認是不會被放置安全點的。這種循環(huán)被稱為可數(shù)循環(huán),相對應(yīng)的,使用long或者范圍更大的數(shù)據(jù)類型作為索引值的循環(huán)就被稱為不可數(shù)循環(huán),將被放置安全點。

在示例代碼中,子線程的循環(huán)索引值數(shù)據(jù)類型是int,也就是可數(shù)循環(huán),所以JVM沒有在循環(huán)跳轉(zhuǎn)處放置安全點。

把循環(huán)索引值數(shù)據(jù)類型改成long型,循環(huán)成為不可數(shù)循環(huán),就能夠成功在循環(huán)跳轉(zhuǎn)處放置安全點,避免子線程長時間無法進入安全點阻塞主線程。

b1f92990-000a-11ee-90ce-dac502259ad0.png

b23fa528-000a-11ee-90ce-dac502259ad0.png

從上面的執(zhí)行結(jié)果可以看到,把循環(huán)索引值數(shù)據(jù)類型改成long型,主線程在睡眠1s之后立即結(jié)束了睡眠,并沒有等待子線程的執(zhí)行。

到這里,也就知道為什么上面貼的RocketMQ大那段代碼,把循環(huán)索引值數(shù)據(jù)類型改成long型可以替換循環(huán)內(nèi)部Thread.Sleep(0)達到放置安全點的目的了。

其實,還可以通過-XX:+UseCountedLoopSafepoints參數(shù)關(guān)閉JVM 對可數(shù)循環(huán)放置安全點的優(yōu)化。下面的執(zhí)行結(jié)果可以看出,添加了-XX:+UseCountedLoopSafepoints參數(shù)后,也能讓運行結(jié)果到達預期。

b2692ae2-000a-11ee-90ce-dac502259ad0.png

還有一個疑惑

b29a15a8-000a-11ee-90ce-dac502259ad0.png

仔細看實例代碼,發(fā)現(xiàn)子線程循環(huán)體內(nèi)調(diào)用了AtomicInteger類的getAndAdd方法,再深入看jdk getAndAdd方法的實現(xiàn),發(fā)現(xiàn)底層是調(diào)用了sun.misc.Unsafe#getIntVolatile 這個方法和Thread.sleep方法一樣,也是一個native方法,為什么這里沒有進入像Thread.sleep方法一樣進入安全點?

是的,好可怕,確實被優(yōu)化了,被 JIT給優(yōu)化了。為了驗證是被JIT優(yōu)化了,可以用

-Djava.compiler=NONE關(guān)閉JIT然后看一下運行結(jié)果。

b3033790-000a-11ee-90ce-dac502259ad0.png

從運行結(jié)果看,關(guān)閉了JIT優(yōu)化后,主線程確實在睡眠1s后立即結(jié)束了,不過子線程運行的時間比JIT優(yōu)化開啟時多了不少。所以,JIT還是能夠帶來一定的性能優(yōu)化的,有時也會帶來一些奇怪的現(xiàn)象。

3

更全面的安全點定義

區(qū)別于初識安全點的時候局限于GC中的安全點概念,這里給安全點一個比較全面的定義:

Safepoint 可以理解成是在代碼執(zhí)行過程中的一些特殊位置,當線程執(zhí)行到這些位置的時候,線程可以暫停。在 SafePoint 保存了其他位置沒有的一些當前線程的運行信息,供其他線程讀取。這些信息包括:線程上下文的任何信息,例如對象或者非對象的內(nèi)部指針等等。我們一般這么理解 SafePoint,就是線程只有運行到了 SafePoint 的位置,他的一切狀態(tài)信息,才是確定的,也只有這個時候,才知道這個線程用了哪些內(nèi)存,沒有用哪些;并且,只有線程處于 SafePoint 位置,這時候?qū)?JVM的堆棧信息進行修改,例如回收某一部分不用的內(nèi)存,線程才會感知到,之后繼續(xù)運行,每個線程都有一份自己的內(nèi)存使用快照,這時候其他線程對于內(nèi)存使用的修改,線程就不知道了,只有再進行到 SafePoint 的時候,才會感知。

4

什么時候會進入Safepoint

當VM Thread需要做vm 操作時會讓線程進入安全點,vm操作類型有很多,可以參考VM_OP_ENUM源碼 vmOperations.hpp。下面是幾種經(jīng)常發(fā)生的進入Safepoint的情形:

(1)GC:由于需要每個線程的對象使用信息,以及回收一些對象,釋放某些堆內(nèi)存或者直接內(nèi)存,所以需要 進入Safepoint來 Stop the world;

(2)定時進入 SafePoint:每經(jīng)過-XX:GuaranteedSafepointInterval 配置的時間,都會讓所有線程進入 Safepoint,一旦所有線程都進入,立刻從 Safepoint 恢復。這個定時主要是為了一些沒必要立刻 Stop the world 的任務(wù)執(zhí)行,可以設(shè)置-XX:GuaranteedSafepointInterval=0關(guān)閉這個定時。

(3)由于 jstack,jmap 和 jstat 等命令,會導致 Stop the world:這種命令都需要采集堆棧信息,所以需要所有線程進入 Safepoint 并暫停。

(4)偏向鎖取消:鎖大部分情況是沒有競爭的(某個同步塊大多數(shù)情況都不會出現(xiàn)多線程同時競爭鎖),所以可以通過偏向來提高性能。即在無競爭時,之前獲得鎖的線程再次獲得鎖時,會判斷是否偏向鎖指向我,那么該線程將不用再次獲得鎖,直接就可以進入同步塊。但是高并發(fā)的情況下,偏向鎖會經(jīng)常失效,導致需要取消偏向鎖,取消偏向鎖的時候,需要 Stop the world,因為要獲取每個線程使用鎖的狀態(tài)以及運行狀態(tài)。

(5)Java Instrument 導致的 Agent 加載以及類的重定義:由于涉及到類重定義,需要修改棧上和這個類相關(guān)的信息,所以需要 Stop the world

(6)Java Code Cache相關(guān):當發(fā)生 JIT 編譯優(yōu)化或者去優(yōu)化,需要 OSR 或者 Bailout 或者清理代碼緩存的時候,由于需要讀取線程執(zhí)行的方法以及改變線程執(zhí)行的方法,所以需要 Stop the world

5

避免Safepoint副作用

Safepoint在一定程度上是可以理解成是為了讓所有用戶線程停頓(Stop The World)而設(shè)計的。STW對應(yīng)用系統(tǒng)來說是一件很可怕的事情,JVM不論是在GC還是在其他的VM操作上都在努力避免STW和減少STW時間。

安全點最主要的副作用就是可能導致STW時間過長,應(yīng)該極力避免這點副作用。

對第一個進入安全點的線程來說,STW是從它進入安全點開始的,如果有某個線程一直無法進入安全點就會導致進入安全點的時間一直處于等待狀態(tài),進而導致STW的時間過長。所以,應(yīng)避免線程執(zhí)行過長無法進入安全點的情況。

可數(shù)循環(huán)體內(nèi)執(zhí)行時間過長以及JIT優(yōu)化導致無法進入安全點的問題是最常見的無法進入安全點的情況。在寫大循環(huán)的時候可以把循環(huán)索引值數(shù)據(jù)類型定義成long。

在高并發(fā)應(yīng)用中,偏向鎖并不能帶來性能提升,反而因為偏向鎖取消帶來了很多沒必要的某些線程進入安全點 。所以建議關(guān)閉:-XX:-UseBiasedLocking。

jstack,jmap 和 jstat 等命令,也會導致進入安全點。所以,生產(chǎn)環(huán)境應(yīng)該關(guān)閉Thead dump的開關(guān),避免dump時間過長導致應(yīng)用STW時間過長。





審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • JAVA語言
    +關(guān)注

    關(guān)注

    0

    文章

    138

    瀏覽量

    20056
  • STW
    STW
    +關(guān)注

    關(guān)注

    0

    文章

    3

    瀏覽量

    6973
  • JVM
    JVM
    +關(guān)注

    關(guān)注

    0

    文章

    157

    瀏覽量

    12188
  • 虛擬機
    +關(guān)注

    關(guān)注

    1

    文章

    899

    瀏覽量

    27978

原文標題:深入淺出解析JVM中的Safepoint

文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    深入淺出AVR(傻孩子)

    本帖最后由 eehome 于 2013-1-5 09:56 編輯 深入淺出AVR(傻孩子)
    發(fā)表于 06-29 15:43

    深入淺出AVR

    深入淺出AVR,一本書。
    發(fā)表于 07-15 12:02

    深入淺出玩轉(zhuǎn)FPGA

    深入淺出玩轉(zhuǎn)FPGA
    發(fā)表于 07-21 09:21

    深入淺出ARM7

    深入淺出ARM7
    發(fā)表于 08-18 10:12

    HDMI技術(shù)深入淺出

    HDMI技術(shù)深入淺出
    發(fā)表于 08-19 10:52

    深入淺出Android

    深入淺出Android
    發(fā)表于 08-20 10:14

    深入淺出Android

    深入淺出Android
    發(fā)表于 04-26 10:48

    深入淺出安防視頻監(jiān)控系統(tǒng)

    深入淺出安防視頻監(jiān)控系統(tǒng)深入淺出安防視頻監(jiān)控系統(tǒng)
    發(fā)表于 05-22 19:28

    深入淺出AVR

    深入淺出AVR
    發(fā)表于 08-23 10:10

    深入淺出matlab

    深入淺出matlab 本書介紹了MATLAB 7.X版本與其他語言混合編程的方法。內(nèi)容包括在MATLAB以文件方式導入、導
    發(fā)表于 06-18 09:13 ?261次下載
    <b class='flag-5'>深入淺出</b>matlab

    深入淺出數(shù)據(jù)分析

    深入淺出數(shù)據(jù)分析,有需要的朋友下來看看。
    發(fā)表于 01-15 14:22 ?0次下載

    深入淺出談多層面板布線技巧

    深入淺出談多層面板布線技巧
    發(fā)表于 12-13 22:20 ?0次下載

    深入淺出Android—Android開發(fā)經(jīng)典教材

    深入淺出Android—Android開發(fā)經(jīng)典教材
    發(fā)表于 10-24 08:52 ?15次下載
    <b class='flag-5'>深入淺出</b>Android—Android開發(fā)經(jīng)典教材

    深入淺出數(shù)字信號處理

    深入淺出數(shù)字信號處理
    發(fā)表于 12-07 20:14 ?508次閱讀

    深入淺出學習250個通信原理資源下載

    深入淺出學習250個通信原理資源下載
    發(fā)表于 04-12 09:16 ?28次下載