日本黄色一级经典视频|伊人久久精品视频|亚洲黄色色周成人视频九九九|av免费网址黄色小短片|黄色Av无码亚洲成年人|亚洲1区2区3区无码|真人黄片免费观看|无码一级小说欧美日免费三级|日韩中文字幕91在线看|精品久久久无码中文字幕边打电话

當前位置:首頁 > > 充電吧
[導讀]對象的創(chuàng)建虛擬機遇到一條new指令時,首先檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執(zhí)行相應的類加載過程。在

對象的創(chuàng)建

虛擬機遇到一條new指令時,首先檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執(zhí)行相應的類加載過程。


在類加載檢查通過后,虛擬機將為新生對象分配內存。對象所需內存的大小在類加載完成后便可完全確定,為對象分配空間的任務等同于把一塊確定大小的內存從Java堆中劃分出來。假設Java堆中內存是絕對規(guī)整的,所有用過的內存都放在一邊,空閑的內存放在另一邊,中間放著一個指針作為分界點的指示器,那所分配內存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump the Pointer)。如果Java堆中的內存并不是規(guī)整的,已使用的內存和空閑的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄,這種分配方式稱為“空閑列表”(Free List)。選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。因此,在使用Serial、ParNew等帶Compact過程的收集器時,系統(tǒng)采用的分配算法是指針碰撞,而使用CMS這種基于Mark-Sweep算法的收集器時,通常采用空閑列表。


除如何劃分可用空間之外,還有另外一個需要考慮的問題是對象創(chuàng)建在虛擬機中是非常頻繁的行為,即使是僅僅修改一個指針所指向的位置,在并發(fā)情況下也并不是線程安全的,可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的情況。解決這個問題有兩種方案,一種是對分配內存空間的動作進行同步處理——實際上虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性;另一種是把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)。哪個線程要分配內存,就在哪個線程的TLAB上分配,只有TLAB用完并分配新的TLAB時,才需要同步鎖定。虛擬機是否使用TLAB,可以通過-XX:+/-UseTLAB參數來設定。


內存分配完成后,虛擬機需要將分配到的內存空間都初始化為零值(不包括對象頭),如果使用TLAB,這一工作過程也可以提前至TLAB分配時進行。這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。


接下來,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭(Object Header)之中。根據虛擬機當前的運行狀態(tài)的不同,如是否啟用偏向鎖等,對象頭會有不同的設置方式。


在上面工作都完成之后,從虛擬機的視角來看,一個新的對象已經產生了,但從Java程序的視角來看,對象創(chuàng)建才剛剛開始——<init>方法還沒有執(zhí)行,所有的字段都還為零。所以,一般來說(由字節(jié)碼中是否跟隨invokespecial指令所決定),執(zhí)行new指令之后會接著執(zhí)行<init>方法,把對象按照程序員的意愿進行初始化,這樣一個真正可用的對象才算完全產生出來。


對象的內存布局 在HotSpot虛擬機中,對象在內存中存儲的布局可以分為3塊區(qū)域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
HotSpot虛擬機的對象頭包括兩部分信息,第一部分用于存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機(未開啟壓縮指針)中分別為32bit和64bit,官方稱它為“Mark Word”。對象需要存儲的運行時數據很多,其實已經超出了32位、64位Bitmap結構所能記錄的限度,但是對象頭信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存儲盡量多的信息,它會根據對象的狀態(tài)復用自己的存儲空間。


對象頭的另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。并不是所有的虛擬機實現都必須在對象數據上保留類型指針,換句話說,查找對象的元數據信息并不一定要經過對象本身,另外,如果對象是一個Java數組,那在對象頭中還必須有一塊用于記錄數組長度的數據,因為虛擬機可以通過普通Java對象的元數據信息確定Java對象的大小,但是從數組的元數據中卻無法確定數組的大小。

實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。這部分的存儲順序會受到虛擬機分配策略參數(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響。HotSpot虛擬機默認的分配策略為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),從分配策略中可以看出,相同寬度的字段總是被分配到一起。在滿足這個前提條件的情況下,在父類中定義的變量會出現在子類之前。如果CompactFields參數值為true(默認為true),那么子類之中較窄的變量也可能會插入到父類變量的空隙之中。

第三部分對齊填充并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用。由于HotSpot VM的自動內存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數倍,換句話說,就是對象的大小必須是8字節(jié)的整數倍。而對象頭部分正好是8字節(jié)的倍數(1倍或者2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。

對象的訪問定位 建立對象是為了使用對象,我們的Java程序需要通過棧上的reference數據來操作堆上的具體對象。由于reference類型在Java虛擬機規(guī)范中只規(guī)定了一個指向對象的引用,并沒有定義這個引用應該通過何種方式去定位、訪問堆中的對象的具體位置,所以對象訪問方式也是取決于虛擬機實現而定的。目前主流的訪問方式有使用句柄和直接指針兩種。
?如果使用句柄訪問的話,那么Java堆中將會劃分出一塊內存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息,


如果使用直接指針訪問,那么Java堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息,而reference中存儲的直接就是對象地址,


使用句柄來訪問 好處 是 reference中存儲的是穩(wěn)定的句柄地址,在對象被移動(垃圾收集時)時只會改變句柄中的實例數據指針,而reference本身不需要修改。

使用直接指針訪問 好處速度更快,它節(jié)省了一次指針定位的時間開銷。

OutOfMemoryError異常 在Java虛擬機規(guī)范的描述中,除了程序計數器外,虛擬機內存的其他幾個運行時區(qū)域都有發(fā)生OutOfMemoryError(OOM)異常的可能。

Java堆溢出 Java堆用于存儲對象實例,只要不斷地創(chuàng)建對象,并且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那么在對象數量到達最大堆的容量限制后就會產生內存溢出異常。

如下代碼限制Java堆的大小為20MB,不可擴展(將堆的最小值-Xms參數與最大值-Xmx參數設置為一樣即可避免堆自動擴展),通過參數-XX:+HeapDumpOnOutOfMemoryError可以讓虛擬機在出現內存溢出異常時Dump出當前的內存堆轉儲快照以便事后進行分析。
VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError?

public?class?HeapOOM{
static?class?OOMObject{?}
public?static?void?main(String[]args){
?List<OOMObject>list=new?ArrayList<OOMObject>();
?while(true){
list.add(new?OOMObject());
}
}
}


運行結果:

java.lang.OutOfMemoryError:
Java?heap?space?Dumping?heap?to?java_pid3404.hprof……
?Heap?dump?file?created[22045981?bytes?in?0.663?secs]


Java堆內存的OOM異常是實際應用中常見的內存溢出異常情況。 當出現Java堆內存溢出時,異常堆棧信息“java.lang.OutOfMemoryError”會跟著進一步提示“Java heap space”。

要解決這個區(qū)域的異常,一般的手段是先通過內存映像分析工具 對Dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是否是必要的,也就是要先分清楚到底是出現了內存泄漏(Memory Leak)還是內存溢出(Memory Overflow)

要解決這個區(qū)域的異常,一般的手段是先通過內存映像分析工具(如Eclipse Memory Analyzer)對Dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是否是必要的,也就是要先分清楚到底是出現了內存泄漏(Memory Leak)還是內存溢出(Memory Overflow)

由于在HotSpot虛擬機中并不區(qū)分虛擬機棧和本地方法棧,因此,對于HotSpot來說,雖然-Xoss參數(設置本地方法棧大?。┐嬖?,但實際上是無效的,棧容量只由-Xss參數設定。關于虛擬機棧和本地方法棧,在Java虛擬機規(guī)范中描述了兩種異常:
如果線程請求的棧深度大于虛擬機所允許的最大深度,將拋出StackOverflowError異常。 如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。

這里把異常分成兩種情況,實際存在著一些互相重疊的地方:當??臻g無法繼續(xù)分配時,到底是內存太小,還是已使用的??臻g太大,其本質上只是對同一件事情的兩種描述而已。

定義了大量的本地變量,增大此方法幀中本地變量表的長度。結果:拋出StackOverflowError異常時輸出的堆棧深度相應縮小。
使用-Xss參數減少棧內存容量,VM Args:-Xss128k

public?class?JavaVMStackSOF{?
private?int?stackLength=1;?
public?void?stackLeak(){?
stackLength++;?
stackLeak();
?}?
public?static?void?main(String[]args)throws?Throwable{?JavaVMStackSOF?oom=new?JavaVMStackSOF();
try{?oom.stackLeak();?
}catch(Throwable?e){?
System.out.println("stack?length:"+oom.stackLength);
?throw?e;?}?}


運行結果:

stack?length:2402
Exception?in?thread"main"java.lang.StackOverflowError
at?org.fenixsoft.oom.VMStackSOF.leak(VMStackSOF.java:20)
at?org.fenixsoft.oom.VMStackSOF.leak(VMStackSOF.java:21)
at?org.fenixsoft.oom.VMStackSOF.leak(VMStackSOF.java:21)
……后續(xù)異常堆棧信息省略


實驗結果表明:在單個線程下,無論是由于棧幀太大還是虛擬機棧容量太小,當內存無法分配的時候,虛擬機拋出的都是StackOverflowError異常。
如果測試時不限于單線程,通過不斷地建立線程的方式倒是可以產生內存溢出異常。但是這樣產生的內存溢出異常與??臻g是否足夠大并不存在任何聯系,或者準確地說,在這種情況下,為每個線程的棧分配的內存越大,反而越容易產生內存溢出異常。
操作系統(tǒng)分配給每個進程的內存是有限制的,譬如32位的Windows限制為2GB。虛擬機提供了參數來控制Java堆和方法區(qū)的這兩部分內存的最大值。剩余的內存為2GB(操作系統(tǒng)限制)減去Xmx(最大堆容量),再減去MaxPermSize(最大方法區(qū)容量),程序計數器消耗內存很小,可以忽略掉。如果虛擬機進程本身耗費的內存不計算在內,剩下的內存就由虛擬機棧和本地方法?!肮戏帧绷?。每個線程分配到的棧容量越大,可以建立的線程數量自然就越少,建立線程時就越容易把剩下的內存耗盡。

這一點讀者需要在開發(fā)多線程的應用時特別注意,出現StackOverflowError異常時有錯誤堆??梢蚤喿x,相對來說,比較容易找到問題的所在。而且,如果使用虛擬機默認參數,棧深度在大多數情況下(因為每個方法壓入棧的幀大小并不是一樣的,所以只能說在大多數情況下)達到1000~2000完全沒有問題,對于正常的方法調用(包括遞歸),這個深度應該完全夠用了。但是,如果是建立過多線程導致的內存溢出,在不能減少線程數或者更換64位虛擬機的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程。如果沒有這方面的處理經驗,這種通過“減少內存”的手段來解決內存溢出的方式會比較難以想到。

VM Args:-Xss2M

public?class?JavaVMStackOOM{?
private?void?dontStop(){?
while(true){?}
?}?
public?void?stackLeakByThread(){?
while(true){?
Thread?thread=new?Thread(new?Runnable(){
?@Override
?public?void?run(){
?dontStop();
?}?});
?thread.start();?}?}
?public?static?void?main(String[]args)throws?Throwable{?
JavaVMStackOOM?oom=new?JavaVMStackOOM();
?oom.stackLeakByThread();
?}?}


運行結果:

Exception?in?thread"main"java.lang.OutOfMemoryError:unable?to?create?new?native?thread


方法區(qū)和運行時常量池溢出 String.intern()是一個Native方法,它的作用是:如果字符串常量池中已經包含一個等于此String對象的字符串,則返回代表池中這個字符串的String對象;否則,將此String對象包含的字符串添加到常量池中,并且返回此String對象的引用。 在JDK 1.6及之前的版本中,由于常量池分配在永久代內,我們可以通過-XX:PermSize和-XX:MaxPermSize限制方法區(qū)大小,從而間接限制其中常量池的容量。
VM Args:-XX:PermSize=10M-XX:MaxPermSize=10M

public?class?RuntimeConstantPoolOOM{?
public?static?void?main(String[]args){
?//使用List保持著常量池引用,避免Full?GC回收常量池行為
?List<String>list=new?ArrayList<String>();?
//10MB的PermSize在integer范圍內足夠產生OOM了
?int?i=0;?while(true){?
list.add(String.valueOf(i++).intern());?}?}
?}


運行結果:

Exception?in?thread"main"java.lang.OutOfMemoryError:
PermGen?space?at?java.lang.String.intern(Native?Method)
at?org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)


從運行結果中可以看到,運行時常量池溢出,在OutOfMemoryError后面跟隨的提示信息是“PermGen space”,說明運行時常量池屬于方法區(qū)(HotSpot虛擬機中的永久代)的一部分。

而使用JDK 1.7運行這段程序就不會得到相同的結果,while循環(huán)將一直進行下去。關于這個字符串常量池的實現問題,還可以引申出一個更有意思的影響。

public?class?RuntimeConstantPoolOOM{
?public?static?void?main(String[]args){
?String?str1=new?StringBuilder("計算機").append("軟件").toString();?
System.out.println(str1.intern()==str1);
?String?str2=new?StringBuilder("ja").append("va").toString();
?System.out.println(str2.intern()==str2);
?}?}?}

這段代碼在JDK 1.6中運行,會得到兩個false,而在JDK 1.7中運行,會得到一個true和一個false。產生差異的原因是:在JDK 1.6中,intern()方法會把首次遇到的字符串實例復制到永久代中,返回的也是永久代中這個字符串實例的引用,而由StringBuilder創(chuàng)建的字符串實例在Java堆上,所以必然不是同一個引用,將返回false。而JDK 1.7(以及部分其他虛擬機,例如JRockit)的intern()實現不會再復制實例,只是在常量池中記錄首次出現的實例引用,因此intern()返回的引用和由StringBuilder創(chuàng)建的那個字符串實例是同一個。對str2比較返回false是因為“java”這個字符串在執(zhí)行StringBuilder.toString()之前已經出現過,字符串常量池中已經有它的引用了,不符合“首次出現”的原則,而“計算機軟件”這個字符串則是首次出現的,因此返回true。
方法區(qū)用于存放Class的相關信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。對于這些區(qū)域的測試,基本的思路是運行時產生大量的類去填滿方法區(qū),直到溢出。雖然直接使用Java SE API也可以動態(tài)產生類(如反射時的GeneratedConstructorAccessor和動態(tài)代理等)。借助CGLib[1]直接操作字節(jié)碼運行時生成了大量的動態(tài)類。
這樣的應用經常會出現在實際應用中:當前的很多主流框架,如Spring、Hibernate,在對類進行增強時,都會使用到CGLib這類字節(jié)碼技術,增強的類越多,就需要越大的方法區(qū)來保證動態(tài)生成的Class可以加載入內存。另外,JVM上的動態(tài)語言(例如Groovy等)通常都會持續(xù)創(chuàng)建類來實現語言的動態(tài)性,隨著這類語言的流行,也越來越容易遇到如下的溢出場景。
VM Args:-XX:PermSize=10M-XX:MaxPermSize=10M

public?class?JavaMethodAreaOOM{?
public?static?void?main(String[]args){
?while(true){?
Enhancer?enhancer=new?Enhancer();
?enhancer.setSuperclass(OOMObject.class);
?enhancer.setUseCache(false);?
enhancer.setCallback(new?MethodInterceptor(){?
public?Object?intercept(Object?obj,Method?method,Object[]args,MethodProxy?proxy)throws?Throwable{
?return?proxy.invokeSuper(obj,args);
?}?});
?enhancer.create();?}?}
?static?class?OOMObject{?}?}


運行結果:

Caused?by:java.lang.OutOfMemoryError:
PermGen?space?at?java.lang.ClassLoader.defineClass1(Native?Method)
?at?java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
at?java.lang.ClassLoader.defineClass(ClassLoader.java:616)?……8?more


方法區(qū)溢出也是一種常見的內存溢出異常,一個類要被垃圾收集器回收掉,判定條件是比較苛刻的。在經常動態(tài)生成大量Class的應用中,需要特別注意類的回收狀況。這類場景除了上面提到的程序使用了CGLib字節(jié)碼增強和動態(tài)語言之外,常見的還有:大量JSP或動態(tài)產生JSP文件的應用(JSP第一次運行時需要編譯為Java類)、基于OSGi的應用(即使是同一個類文件,被不同的加載器加載也會視為不同的類)等。

本機直接內存溢出 DirectMemory容量可通過-XX:MaxDirectMemorySize指定,如果不指定,則默認與Java堆最大值(-Xmx指定)一樣,下例越過了DirectByteBuffer類,直接通過反射獲取Unsafe實例進行內存分配(Unsafe類的getUnsafe()方法限制了只有引導類加載器才會返回實例,也就是設計者希望只有rt.jar中的類才能使用Unsafe的功能)。因為,雖然使用DirectByteBuffer分配內存也會拋出內存溢出異常,但它拋出異常時并沒有真正向操作系統(tǒng)申請分配內存,而是通過計算得知內存無法分配,于是手動拋出異常,真正申請分配內存的方法是unsafe.allocateMemory()。

*VM Args:-Xmx20M-XX:MaxDirectMemorySize=10M

public?class?DirectMemoryOOM{
?private?static?final?int_1MB=1024*1024;?
public?static?void?main(String[]args)throws?Exception{?
Field?unsafeField=Unsafe.class.getDeclaredFields()[0];
?unsafeField.setAccessible(true);
?Unsafe?unsafe=(Unsafe)unsafeField.get(null);?
while(true){?
unsafe.allocateMemory(_1MB);
?}?}?}


Exception?in?thread"main"java.lang.OutOfMemoryError
at?sun.misc.Unsafe.allocateMemory(Native?Method)
at?org.fenixsoft.oom.DMOOM.main(DMOOM.java:20)


由DirectMemory導致的內存溢出,一個明顯的特征是在Heap Dump文件中不會看見明顯的異常,如果發(fā)現OOM之后Dump文件很小,而程序中又直接或間接使用了NIO,那就可以考慮檢查一下是不是這方面的原因。

本站聲明: 本文章由作者或相關機構授權發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內容真實性等。需要轉載請聯系該專欄作者,如若文章內容侵犯您的權益,請及時聯系本站刪除。
換一批
延伸閱讀

LED驅動電源的輸入包括高壓工頻交流(即市電)、低壓直流、高壓直流、低壓高頻交流(如電子變壓器的輸出)等。

關鍵字: 驅動電源

在工業(yè)自動化蓬勃發(fā)展的當下,工業(yè)電機作為核心動力設備,其驅動電源的性能直接關系到整個系統(tǒng)的穩(wěn)定性和可靠性。其中,反電動勢抑制與過流保護是驅動電源設計中至關重要的兩個環(huán)節(jié),集成化方案的設計成為提升電機驅動性能的關鍵。

關鍵字: 工業(yè)電機 驅動電源

LED 驅動電源作為 LED 照明系統(tǒng)的 “心臟”,其穩(wěn)定性直接決定了整個照明設備的使用壽命。然而,在實際應用中,LED 驅動電源易損壞的問題卻十分常見,不僅增加了維護成本,還影響了用戶體驗。要解決這一問題,需從設計、生...

關鍵字: 驅動電源 照明系統(tǒng) 散熱

根據LED驅動電源的公式,電感內電流波動大小和電感值成反比,輸出紋波和輸出電容值成反比。所以加大電感值和輸出電容值可以減小紋波。

關鍵字: LED 設計 驅動電源

電動汽車(EV)作為新能源汽車的重要代表,正逐漸成為全球汽車產業(yè)的重要發(fā)展方向。電動汽車的核心技術之一是電機驅動控制系統(tǒng),而絕緣柵雙極型晶體管(IGBT)作為電機驅動系統(tǒng)中的關鍵元件,其性能直接影響到電動汽車的動力性能和...

關鍵字: 電動汽車 新能源 驅動電源

在現代城市建設中,街道及停車場照明作為基礎設施的重要組成部分,其質量和效率直接關系到城市的公共安全、居民生活質量和能源利用效率。隨著科技的進步,高亮度白光發(fā)光二極管(LED)因其獨特的優(yōu)勢逐漸取代傳統(tǒng)光源,成為大功率區(qū)域...

關鍵字: 發(fā)光二極管 驅動電源 LED

LED通用照明設計工程師會遇到許多挑戰(zhàn),如功率密度、功率因數校正(PFC)、空間受限和可靠性等。

關鍵字: LED 驅動電源 功率因數校正

在LED照明技術日益普及的今天,LED驅動電源的電磁干擾(EMI)問題成為了一個不可忽視的挑戰(zhàn)。電磁干擾不僅會影響LED燈具的正常工作,還可能對周圍電子設備造成不利影響,甚至引發(fā)系統(tǒng)故障。因此,采取有效的硬件措施來解決L...

關鍵字: LED照明技術 電磁干擾 驅動電源

開關電源具有效率高的特性,而且開關電源的變壓器體積比串聯穩(wěn)壓型電源的要小得多,電源電路比較整潔,整機重量也有所下降,所以,現在的LED驅動電源

關鍵字: LED 驅動電源 開關電源

LED驅動電源是把電源供應轉換為特定的電壓電流以驅動LED發(fā)光的電壓轉換器,通常情況下:LED驅動電源的輸入包括高壓工頻交流(即市電)、低壓直流、高壓直流、低壓高頻交流(如電子變壓器的輸出)等。

關鍵字: LED 隧道燈 驅動電源
關閉