版權(quán)說(shuō)明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡(jiǎn)介
Dalvik虛擬機(jī)Java堆創(chuàng)建過(guò)程分析
使用C/C++開(kāi)發(fā)應(yīng)用程序最令頭痛的問(wèn)題就是內(nèi)存管理,慎不留神,要么內(nèi)存泄漏,要么內(nèi)
存破壞。虛擬機(jī)要解決的問(wèn)題之一就是幫助應(yīng)用程序自動(dòng)分配和釋放內(nèi)存。為了達(dá)到這個(gè)目
的,虛擬機(jī)在啟動(dòng)的時(shí)候向操作系統(tǒng)申請(qǐng)?大塊內(nèi)存當(dāng)作對(duì)象堆。之后當(dāng)應(yīng)用程序創(chuàng)建對(duì)象
時(shí),虛擬機(jī)就會(huì)在堆上分配合適的內(nèi)存塊。而當(dāng)對(duì)象不再使用時(shí),虛擬機(jī)就會(huì)將它占用的內(nèi)
存塊歸還給堆。Dalvik虛擬機(jī)也不例外,本文就分析它的Java堆創(chuàng)建過(guò)程。
從前面一文可以知道,在Dalvik虛擬機(jī)中,Java堆實(shí)際上是由一個(gè)Active堆和一個(gè)Zygote
堆組成的,如圖1所示:
HeapSource
Actr>?HeapZygoteHeap
UrdTabte
jveHeapBitmap
MarkHeapBftm^p
圖1Dalvi噓擬機(jī)的Java堆
其中,Zygote堆用來(lái)管理Zygote進(jìn)程在啟動(dòng)過(guò)程中預(yù)加我和創(chuàng)建的各種對(duì)象,而Active堆
是在Zygote進(jìn)程fork第一個(gè)子進(jìn)程之前創(chuàng)建的。之后無(wú)論是Zygoie進(jìn)程還是其子進(jìn)程,都
在Active堆上進(jìn)行對(duì)象分配和釋放。這樣做的H的是使得Zygote進(jìn)程和其子進(jìn)程最大限度
地共享Zygote堆所占用的內(nèi)存。
為了管理Java堆,Dalvik虛擬機(jī)需要一些輔助數(shù)據(jù)結(jié)構(gòu),包括一個(gè)CardTable、兩
個(gè)HeapBitm叩和一個(gè)MarkSlack。CardTable是為了記錄在垃圾收集過(guò)程中對(duì)象的引用情
況的,以便可以實(shí)現(xiàn)ConcurrentGo圖1的兩個(gè)HeapBitmap,一個(gè)稱為L(zhǎng)iveHeapBitmap,
用來(lái)記錄上次GC之后,還存活的對(duì)象,另一個(gè)稱為MarkHeapBitm叩,用來(lái)記錄當(dāng)前GC
中還存活的對(duì)象。這樣,上次GC后存活的但是當(dāng)前GC不存活的對(duì)象,就是需要釋放的對(duì)
象。Davlk虛擬機(jī)使用標(biāo)記-清除(Mark-Sweep)算法進(jìn)行GC。在標(biāo)記階段,通過(guò)一個(gè)Mark
Stack來(lái)實(shí)現(xiàn)遞歸檢查被引用的對(duì)象,即在當(dāng)前GC中存活的對(duì)象。有了這個(gè)MarkStack,
就可以通過(guò)循環(huán)來(lái)模擬函數(shù)遞歸調(diào)用。
Dalvik虛擬機(jī)Java堆的創(chuàng)建過(guò)程實(shí)際上就是上面分析的各種數(shù)據(jù)結(jié)構(gòu)的創(chuàng)建過(guò)程,
它們是在Dalvik虛擬機(jī)啟動(dòng)的過(guò)程中創(chuàng)建的。接下來(lái),我們就詳細(xì)分析這個(gè)過(guò)程。
從前面一文可以知道,Dalvik虛擬機(jī)在啟動(dòng)的過(guò)程中,會(huì)通過(guò)調(diào)用函數(shù)dvmGcSiarlup
來(lái)創(chuàng)建Java堆,它的實(shí)現(xiàn)如下所示:
[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片
booldvmGcStartupO
(
dvmInitMutex(&gDvni.gcHeapLock);
pthread_cond_init(&gDvm.gcHeapCond,NULL);
returndvmHeapStartupO;
)
這個(gè)函數(shù)定義在文件dalvik/vm/alloc/Alloc.cpp中。
函數(shù)dvmGcStartup首先是分別初始化一個(gè)鎖和一個(gè)條件變量,它們都是用來(lái)保護(hù)堆
的并行訪問(wèn)的,接著再調(diào)用另夕I、一個(gè)函數(shù)dvmHeapStartup來(lái)創(chuàng)建Java堆。
函數(shù)dvmHeapStartup的實(shí)現(xiàn)如下所示:
[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片
booldvmHeapStartupO
(
GcIIeap*gclleap;
if(gDvm.heapGrowthLimil==0){
gDvm.heapGrowthLimit=gDvm.heapMaximumSize;
)
gcHcap=dvniHcapSourccStariup(gDvm.hcapStartingSize,
gDvm.heapMaximumSize,
gDvm.heapGrowthLimit);
gDvm.gcHeap=gcHcap;
if(!dvmCardTableStanup(gDvm.heapMaximumSize,gDvm.heapGrowthLimit)){
LOGE_HEAP("cardtablestartupfailed.");
returnfalse;
}
returntrue;
I
這個(gè)函數(shù)定義在文件dalvik/vm/alloc/Heap.cpp中。
gDvm是一個(gè)類型為DvmGlobals的全局變量,它通過(guò)各個(gè)成員變量記錄了Dalvik
虛擬機(jī)的各種信息。這里涉及到三個(gè)重要與Java堆相關(guān)的信息,分別是Java堆的起始大小
(StartingSize)^最大值(MaximumSize)和增長(zhǎng)上限值(GrowthLimit)?在啟動(dòng)Dalvik
虛擬機(jī)的時(shí)候,我們可以分別通過(guò)-Xms、-Xmx和-XX:HeapGrowthLimil三個(gè)選項(xiàng)來(lái)指定上
述三個(gè)值。
Java堆的起始大小(StartingSize)指定了Davlik虛擬機(jī)在啟動(dòng)的時(shí)候向系統(tǒng)申請(qǐng)的
物理內(nèi)存的大小。后面再根據(jù)需要逐漸向系統(tǒng)申請(qǐng)更多的物理內(nèi)存,直到達(dá)到最大值
(MaximumSize)為止。這是一種按需要分配策略,可以避免內(nèi)存浪費(fèi)。在默認(rèn)情況下,Java
堆的起始大小(StartingSize)和最大值(MaximumSize)等于4M和16M。但是廠商會(huì)通
過(guò)dalvik.vm.heapstartsize和dalvik.vm.heapsize這兩個(gè)屬性將它們?cè)O(shè)置為合適設(shè)備的值的。
注意,雖然Java堆使用的物理內(nèi)存是按需要分配的,但是它使用的虛擬內(nèi)存的總大
小卻是需要在Dalvik啟動(dòng)的時(shí)候就確定的。這個(gè)虛擬內(nèi)存的大小就等于Java堆的最大值
(MaximumSize)o想象一下,如果不這樣做的話,會(huì)出現(xiàn)什么情況。假設(shè)開(kāi)始時(shí)創(chuàng)建的虛
擬內(nèi)存小于Java堆的最大值(MaximumSize),由于實(shí)際情況是允許虛擬內(nèi)存的大小是達(dá)到
Java堆的最大值(MaximumSize)的,因此,當(dāng)開(kāi)始時(shí)創(chuàng)建的虛擬內(nèi)存無(wú)法滿足需求時(shí),
那么就需要重新創(chuàng)建另外一塊更大的虛擬內(nèi)存。這樣就需要將之前的虛擬內(nèi)存的內(nèi)容挑貝到
新創(chuàng)建的更大的虛擬內(nèi)存去,并且還要相應(yīng)地修改各種輔助數(shù)據(jù)結(jié)構(gòu)。這樣太麻煩了,而且
效率也太低了。因此就在一開(kāi)始的時(shí)候,就創(chuàng)建一塊與Java堆的最大值(MaximumSize)
相等的虛擬內(nèi)存。
但是,Dalvik虛擬機(jī)乂希望能夠動(dòng)態(tài)地調(diào)整Java堆的可用最大值,于是就出現(xiàn)了一
個(gè)稱為增長(zhǎng)上限的值(GrowthLimit)o這個(gè)增長(zhǎng)上限值(GrowthLimit),我們可以認(rèn)為它
是Java堆大小的軟限制,而前面所描述的最大值(MaximumSize),是Java堆大小的硬限
制。通過(guò)動(dòng)態(tài)地調(diào)整增長(zhǎng)上限值(GrowthLimit),就可以實(shí)現(xiàn)動(dòng)態(tài)調(diào)整Java堆的可用最大
值,但是這個(gè)增長(zhǎng)上限值必須要小于等于最大值(MaximumSize)o從函數(shù)dvmHeapSiartup
的實(shí)現(xiàn)可以知道,如果沒(méi)有指定Java堆的增長(zhǎng)上限的值(GrowthLimit),那么它的值就等
于Java堆的最大值(MaximumSize)。
事實(shí)上,在全局變量gDvm中,除了上面提到的三個(gè)信息之外,還有三種信息是與
Java堆相關(guān)的,它們分別是堆最小空閑值(MinFree)、堆最大空閑值(MaxFree)和堆目
標(biāo)利用率(TargetUtilization)(>這三個(gè)值可以分別通過(guò)Dalvik虛擬機(jī)的啟動(dòng)選項(xiàng)
-XX:HeapMinFree、-XX:HcapMaxFree和-XX:HeapTargetUtilizalion來(lái)指定。它們用來(lái)確保每
次GC之后,Java堆已經(jīng)使用和空閑的內(nèi)存有一個(gè)合適的比例,這樣可以盡量地減少GC的
次數(shù)。舉個(gè)例子說(shuō),堆的利用率為U,最小空閑值為MinFree字節(jié),最大空閑值為MaxFree
字節(jié)。假設(shè)在某一次GC之后,存活對(duì)象占用內(nèi)存的大小為L(zhǎng)iveSize。那么這時(shí)候堆的理想
大小應(yīng)該為(LivcSize/U)。但是(LivcSize/U)必須大于等于(LivcSize+MinFree)并且小于等
于(LiveSize+MaxFree)?
了解了這些與Java堆大小相關(guān)的信息之后,我們回到函數(shù)dvmGcStartup中,可以
清楚看到,它先是調(diào)用函數(shù)dvniHeapSourceStaitup來(lái)創(chuàng)建一個(gè)Java堆,接著再調(diào)用函數(shù)
dvmCardTableStartup來(lái)為該Java堆創(chuàng)建一個(gè)CardTable。接下來(lái)我們先分析函數(shù)
dvmHeapSourceStarlup的實(shí)現(xiàn),接著再分析函數(shù)dvmCardTableStartup的實(shí)現(xiàn)。
函數(shù)dvmHeapSourceStartup的實(shí)現(xiàn)如下所示:
[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片
GcHeap*dvmHeapSourceStartup(size_tstartSize,size_tmaximumSize,
size_tgrowthLimit)
GcHcap*gcHcap;
HeapSource*hs;
mspacemsp;
size_llength;
void*base;
*Allocateacontiguousregionofvirtualmemorytosubdivided
*amongtheheapsmanagedbythegarbagecollector.
*/
length=ALIGN_UP_TO_PAGE_SIZE(maximumSize);
base=dvmAllocRcgion(lengih,PROT_NONE,gDvm.zygote?"dalvik-zygote"
"dalvik-heap");
/*Createanunlockeddlmallocmspacctouseas
*aheapsource.
*/
msp=createMspace(base,klnitialMorecoreStart,startSize);
gcHeap=(GcHeap*)calloc(l,sizeof(*gcHeap));
hs=(HeapSource*)calloc(l,sizeof(*hs));
hs->targetUtilization=gDvm.heapTargetUtilization*HEAP_UTILIZATION_MAX;
hs->minFree=gDvm.heapMinFree;
hs->maxFrcc=gDvm.hcapMaxFrec;
hs->startSize=startSize;
hs->maximumSize=niaximuniSize;
hs->growthLimit=growthLimit;
hs->numHeaps=0;
hs->hcapBasc=(char*)basc;
hs->heapLength=length;
if(!addInitialHeap(hs,msp,growthLimit)){
if(!dvmHeapBitmapInit(&hs->liveBits,base,length,"dalvik-bitmap-r')){
if(!dvmHeapBi(mapInit(&hs->markBits,base,length,'dalvik-bitmap-2")){
if(!allocMarkStack(&gcHeap->>markContext.stack,hs-^maximumSize)){
gcHeap->markContext.bitmap=&hs->markBits;
gcHeap->heapSource=hs;
gHs=hs;
returngcHeap;
這個(gè)函數(shù)定義在文件dalvik/vm/alloc/HeapSource.cpp中。
函數(shù)dvmHeapSourceStartup的執(zhí)行過(guò)程如下所示:
1.將參數(shù)maximum指定的最大堆大小對(duì)齊到內(nèi)存頁(yè)邊界,得到結(jié)果為length,并
且調(diào)用函數(shù)dvmAllocRegion分配一塊大小等于length的匿名共享內(nèi)存塊,起始地址為base。
這塊匿名共享內(nèi)存即作為Dalvik虛擬機(jī)的Java堆。
2.調(diào)用函數(shù)createMspace將前面得到的匿名共享內(nèi)存塊封裝為一個(gè)mspace,以便
后面可以通過(guò)C庫(kù)得供的mspace_maUoc和rnspace_bulk_free等函數(shù)來(lái)管理Java堆。這個(gè)
mspace的起始大小為Java堆的起始大小,這意味著一開(kāi)始在該mspace上能夠分配的內(nèi)存不
能超過(guò)Java堆的起始大小。不過(guò)后面我們動(dòng)態(tài)地調(diào)整這個(gè)mspace的大小,使得它可以使用
更多的內(nèi)存,但是不能超過(guò)Java堆的最大值。
3,分配一個(gè)GcHeap結(jié)構(gòu)體gcHeap和一個(gè)HeapSource結(jié)構(gòu)體hs,用來(lái)維護(hù)Java
堆的信息,包括Java堆的目標(biāo)利用率、最小空閑值、最大空閑值、起始大小、最大值、增
長(zhǎng)上限值、堆個(gè)數(shù)、起始地址和大小等信信息。
4.調(diào)用函數(shù)addlnitialHeap在前面得到的匿名共享內(nèi)存上創(chuàng)建一個(gè)Active堆。這個(gè)
Active堆的最大值被設(shè)置為Java堆的起始大小。
5.調(diào)用函數(shù)dvmHeapBitmapInit創(chuàng)建和初始化一個(gè)LiveBitmap和一個(gè)Mark
Bitmap,它們?cè)贕C時(shí)會(huì)用得到。
6.調(diào)用函數(shù)allockMarkStack創(chuàng)建和初始化一個(gè)MarkStack,它在GC時(shí)也會(huì)用到。
7.將前面創(chuàng)建和初始化好的MarkBitmap和HeapSource結(jié)構(gòu)體hs保存在前面創(chuàng)建
的GcHcap結(jié)構(gòu)體gcHcap中,并且將該GcHcap結(jié)構(gòu)體gcHcap返回給調(diào)用者。同時(shí),
HeapSource結(jié)構(gòu)體hs也會(huì)保存在全局變量gHs中。
為了更好地對(duì)照?qǐng)D2來(lái)理解函數(shù)dvmHeapSourceSlartup所做的事情,接下來(lái)我們?cè)?/p>
細(xì)分析上述提到的關(guān)鍵函數(shù)dvmAIIocRegion、createMspace、addlnitialHeap%
dvmHeapBitmapInit和allcckMarkStack的實(shí)現(xiàn)。
函數(shù)dvmAIIocRegion的實(shí)現(xiàn)如下所示:
[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片
void*dvmAllocRegion(size_tbyteCounl,iniprot,constchar*name){
void*base;
intfd,ret;
bytcCount=ALIGN_UP_TO_PAGE_SlZE(bytcCount);
fd=ashmem_crcatc_rcgion(namc,bytcCount);
if(fd==-1){
returnNULL;
}
base=rnniap(NULL,byteCount,prot,MAP_PRIVATE,fd,0);
ret=close(fd);
if(base==MAP_FAILED){
returnNULL;
)
if(ret==-1){
munniapCbase,byteCount);
returnNULL;
)
returnbase;
}
這個(gè)函數(shù)定義在文件dalvik/vm/Misc.cpp中。
從這里就可以清楚地看出,函數(shù)dvmAIIocRegion所做的事情就是調(diào)用函數(shù)
ashmem_create_region來(lái)創(chuàng)建一塊匿名共享內(nèi)存。關(guān)于Android系統(tǒng)的匿名共享內(nèi)存,可以
參考前面Android系統(tǒng)匿名共享內(nèi)存Ashmem(AnonymousSharedMemory)簡(jiǎn)要介紹和學(xué)
習(xí)計(jì)劃一文。
函數(shù)createMspace的實(shí)現(xiàn)如下所示:
[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片
staticmspacecreateMspace(void*begin,size_tmorecoreStart,size_tstartingSize)
//Clearerrnotoallowstrerroronerror.
errno=0;
//Allowaccesstoinitaipagesthatwillholdmspace.
mprotcct(bcgin,morccorcStart,PROT_READ|PROT_WRITE);
//Createmspaceusingourbackingstoragestartingatbeginandwithafootprintof
//morecorcStart.Don'tuseaninternaldlmalloclock.WhenmorccoreStartbytesofmemory
are
//exhaustedmorecorewillbecalled.
mspacenisp=create_mspace_vvith_base(begin,morecoreStart,false/*Iocked*/);
if(msp!=NULL){
//Donotallowmorccorcrequeststosucceedbeyondthestartingsizeoftheheap.
mspace_sel_fooiprin(_limit(msp,star(ingSize);
}else{
ALOGE("creale_^ispace_with_basefailed%s",slrerror(ermo));
)
returnmsp;
}
這個(gè)函數(shù)定義在文件dalvik/vm/aHoc/HcapSourcc.cpp中。
參數(shù)begin指向前面創(chuàng)建的一塊法名共享內(nèi)存的起始地址,也就是Java堆的起始地
址,函數(shù)createMspace通過(guò)C庫(kù)提供的函數(shù)crcatc_mspacc_\vith_basc將該塊匿名共享內(nèi)存
封裝成一個(gè)mspace,并且通過(guò)調(diào)用C庫(kù)提供的函數(shù)mspace_sel_foolprint_limit設(shè)置該mspace
的大小為Java堆的起始大小。
函數(shù)addlnitialHeap的實(shí)現(xiàn)如下所示:
[epp]viewplaincopy在CODE上查看代碼片派生到我的代碼片
staticbooladdInitialHcap(HcapSourcc*hs,mspacemsp,sizs_tmaximumsizc)
(
assert(hs!=NULL);
assert(msp!=NULL);
if(hs->numHeaps!=0){
returnfalse;
)
hs->hcaps[0].msp=msp;
hs->heaps[O].maximunSize=maximumSize;
hs->heaps[0].concurrentStartBytes=SIZE_MAX;
hs->heaps[()].base=hs->heapBase;
hs->heaps[0].limit=hs->heapBase+maximumSize;
hs->heaps[()].brk=hs->heapBase+klnitialMorecoreStart;
hs->nuniHeaps=1;
returntrue;
}
這個(gè)函數(shù)定義在文件daivik/vm/alloc/HeapSource.cpp中。
在分析函數(shù)addlnitialHeap的實(shí)現(xiàn)之前,我們先解釋一下兩個(gè)數(shù)據(jù)結(jié)構(gòu)HeapSource
和He叩。
在結(jié)構(gòu)體HeapSource中,有一個(gè)類型為Heap的數(shù)組he叩s,如下所示:
[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片
structHeapSource{
/*Theheaps;heapsl()jisalwaystheactiveheap,
*whichnewobjectsshouldbeallocatedfrom.
*/
Heapheaps[HEAP_SOURCE_MAX_HEAP_COUNT];
/*Thecurrentnumberofheaps.
*/
size_tnuniHcaps;
};
這個(gè)結(jié)構(gòu)體定義在文件dalvik/vm/alloc/HcapSourcc.cpp中。
這個(gè)Heap數(shù)組最多有HEAP_SOURCE_MAX_HEAP_COUNT個(gè)Heap,并且當(dāng)前擁
有的Heap個(gè)數(shù)記錄在nunipHeaps中。
HEAP_SOURCE_MAX_HEAP_COUNT是一個(gè)宏,定義為2,如下所示:
[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片
/*Thelargestnumberofseparateheapswccanhandle.
#defineHEAP_SOURCE_MAX_HEAP_COUNT2
這個(gè)宏定義在文彳匕dalvik/vm/alloc/HeapSource.h中。
這意味著Dalvik虛擬機(jī)的Java堆最多可以劃分為兩個(gè)Heap,就是圖1所示的Active
堆和Zygote堆。
結(jié)構(gòu)Heap的定義如下所示:
[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片
structHeap(
/*Themspacetoallocatefrom.
*/
mspacemsp;
/*Thelargestsizethatthisheapisallowedtogrowto.
*/
size_tmaximumSize;
/*Numberofbytesallocatedfromthismspaceforobjects,
*includinganyoverhead.ThisvalueisNOTexact,and
*shouldonlybeusedasaninputforcertainheuristics.
*/
size_tbytesAllocated;
/*Numberofbytesallocatedfromthismspaceatwhicha
*concurrentgarbagecollectionwillbestarted.
*/
sizc_tconcurrcntStartBytes;
/*Numberofobjectscurrentlyallocatedfromthismspace.
*/
size_tobjectsAllocated;
/*
*Thelowestaddressofthisheap,inclusive.
*/
char*basc;
/*
*Thehighestaddressofthisheap,exclusive.
*/
char
/*
*Iftheheaphasanmspace,thecurrenthighwatermarkin
*allocationsrequestedviadvmHeapSourceMorecore.
*/
char*brk;
);
這個(gè)結(jié)構(gòu)體定義在文件dalvik/vm/alloc/HeapSource.cpp中。
結(jié)構(gòu)體Heap用來(lái)描述一個(gè)堆,它的各個(gè)成員變量的含義如下所示:
msp:描述堆所使用內(nèi)存塊。
maximumSize:描述堆可以使用的最大內(nèi)存值。
bytesAllocated:描述堆已經(jīng)分配的字節(jié)數(shù)。
concurrcntStartBytcs:描述堆已經(jīng)分配的內(nèi)存ii到指定值就要觸發(fā)并行GC。
objectsAllocated:描述已經(jīng)分配的對(duì)象數(shù)。
base:描述堆所使用的內(nèi)存塊的起始地址。
limit:描述堆所使用的內(nèi)存塊的結(jié)束地址。
brk:描述當(dāng)前堆所分配的最大內(nèi)存值。
回到函數(shù)addlnitialHeap中,參數(shù)hs和msp指向的是在函數(shù)dvniHeapSourceSlartup
中創(chuàng)建的HeapSource結(jié)構(gòu)體和mspace內(nèi)存對(duì)象,而參數(shù)maximumSize描述的Java堆的增
長(zhǎng)上限值。
通過(guò)函數(shù)addlnitialHeap的實(shí)現(xiàn)就可以看出,Dalvik虛擬機(jī)在啟動(dòng)的時(shí)候,實(shí)際上
只創(chuàng)建了一個(gè)Heap。這個(gè)Heap就是我們?cè)趫D1中所說(shuō)的Active堆,它開(kāi)始的時(shí)候管理的
是整個(gè)Java堆。但是在圖1中,我們說(shuō)Java堆實(shí)際上還包含有一個(gè)Zygote堆的,那么這個(gè)
Zygote堆是怎么來(lái)的呢?
從前面一文可以知道,Zygote進(jìn)程會(huì)通過(guò)調(diào)用函數(shù)forkAndSpecializeCommcn來(lái)
fork子進(jìn)程,其中與Dalvik虛擬機(jī)Java堆相關(guān)的邏輯如下所示:
[cpplviewplaincopy在CODE上查看代碼片派生到我的代碼片
staticpid_tforkAndSpccial:zcCommon(constu4*args,boolisSystcmScrvcr)
(
pid_tpid;
if(!dvmGcPrcZygotcFork()){
pid=fork();
if(pid==0){
}else{
)
returnpid;
I
這個(gè)函數(shù)定義在文件dalvik/vm/native/dalvik_system_Zygote.cpp中。
從這里就可以看出,Zygote進(jìn)程在fork子進(jìn)程之前,會(huì)調(diào)用函數(shù)
dvmGcPreZygoteFork來(lái)處理一下Dalvik虛擬機(jī)Java堆。接下來(lái)我們就看看函數(shù)
dvmGcPreZygoteFork都做了哪些事情。
函數(shù)dvmGcPreZygoteFork的實(shí)現(xiàn)如下所示:
[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片
booldvniGcPrcZygoteFork()
(
returndvmHcapSourceStartupBcforcFork();
I
這個(gè)函數(shù)定義在文件dalvik/vm/alloc/Alloc.cpp中。
函數(shù)dvmGcPreZygoteFork只是簡(jiǎn)單地封裝了對(duì)另外一個(gè)函數(shù)
dvmHeapSourceStartupBeforeFork的調(diào)用,后者的實(shí)現(xiàn)如下所示:
fcpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片
booldvmHeapSourceStartupBeforeForkO
(
HeapSource*hs=gHs;//usealocaltoavoidtheimplicit"volatile"
HS_BOILERPLATE();
assert(gDvm.zygote);
if(JgDvm.newZygoteHeapAllocated){
/*Ensureheapsaretrimmedtominimizefootprintpre-fork.
?/
trimHeapsO;
/*Createanewheapforpost-forkzygoteallocations.Wconly
*tryonce,evenifitfails.
*/
ALOGV("Splitlingoulnewzygoteheap");
gDvm.newZygoteHeapAllocated=true;
returnaddNewHeap(hs);
)
returntrue;
}
這個(gè)函數(shù)定義在文件daivik/vm/alloc/HcapSource.cpp4'o
前面我們?cè)诜治龊瘮?shù)dvmHeapSourceStartup的實(shí)現(xiàn)時(shí)提到,全局變量gHs指向的
是一個(gè)HeapSource結(jié)構(gòu)體,它描述\Dalvik虛擬機(jī)Java堆的信息。同時(shí),gDvm也是一個(gè)
全局變吊:,它的類型為DvmGlobals。gDvm指向的DvmGlobals結(jié)構(gòu)體的成員變吊:
newZygoteHeapAllocated的值被初始化為false。因此,當(dāng)函數(shù)
dvmHeapSourceStartupBeforeFork第一次被調(diào)用時(shí),它會(huì)先調(diào)用函數(shù)trimHeaps來(lái)將Java堆
中沒(méi)有使用到的內(nèi)存歸還給系統(tǒng),接著再調(diào)用函數(shù)addNewHeap來(lái)創(chuàng)建一個(gè)新的Heap。這
個(gè)新的Heap就是圖1所說(shuō)的Zygote堆了。
由于函數(shù)dvmHeapSourceStartupBeforeFork第一次被調(diào)用之后,gDvm指向的
DvmGlobals結(jié)構(gòu)體的成員變量newZygoteHeapAllocated的值就會(huì)被修改為(rue,因此起到
的效果就是以后Zygote進(jìn)程對(duì)函數(shù)dvmHeapSourceStartupBeforeFork的調(diào)用都是無(wú)用功。這
也意味著Zygote進(jìn)程只會(huì)在fork第一個(gè)子進(jìn)程的時(shí)候,才會(huì)將Java堆劃一分為二來(lái)管理。
接下來(lái)我們就繼續(xù)分析函數(shù)trimHcaps和addNcwHcap的實(shí)現(xiàn),以便更好地理解
Dalvik虛擬機(jī)是如何管理Java堆的。
函數(shù)trimHeaps的實(shí)現(xiàn)如下所示:
[cppjviewplaincopy在CODE上查看代碼片派生到我的代碼片
/*
*Returnunusedmemorytothesystemifpossible.
*/
staticvoidtrimHeaps()
(
HS_BOILERPLATE();
HeapSource*hs=gHs;
sizc_theapBytes=0;
for(sizc_ti=0;i<hs->numlleaps;i++){
Heap*heap=&hs->heaps[i];
/*Returnthewildernesschunktothesystem.*/
inspace_trirn(heap->msp,0);
/*Returnanywholefreepagestothesystem.*/
mspace_inspect_all(heap->msp,releasePagesInRange,&heapBytes);
)
/*Sameforthenativeheap.*/
dlmalloc_trim(0);
size_tnativeBytes=0;
dimalloc_inspect_all(releascPagesInRange,&nativeBytes);
LOGD_HEAP("madviscd%zd(GC)+%zd(native)=%zdtotalbytes",
heapBytes,nativeBytes,heapBytes+nativeBytes);
)
這個(gè)函數(shù)定義在文件dalvik/vm/alloc/HeapSource.cpp中。
函數(shù)trimHeaps對(duì)Dalvik虛擬機(jī)使用的Java堆和默認(rèn)Native堆做了同樣的兩件事
情。
第一件事情是調(diào)用C庫(kù)提供的函數(shù)mspacc_trim/d1malloc_trim來(lái)將沒(méi)有使用到的虛
擬內(nèi)存和物理內(nèi)存歸還給系統(tǒng),這是通過(guò)系統(tǒng)調(diào)用mremap來(lái)實(shí)現(xiàn)的。
第二件事情是調(diào)用C庫(kù)提供的函數(shù)mspace_inspect_all/dlmalloc_inspect_all將不能
使用的內(nèi)存碎片對(duì)應(yīng)的物理內(nèi)存歸還給系統(tǒng),這是通過(guò)系統(tǒng)調(diào)用madvise來(lái)實(shí)現(xiàn)的。注意,
在此種情況下,只能歸還無(wú)用的物理內(nèi)存,而不能歸還無(wú)用的虛擬內(nèi)存。因?yàn)闅w還內(nèi)存碎片
對(duì)應(yīng)的虛擬內(nèi)存會(huì)使得堆為整體虛擬地址不連續(xù)。
函數(shù)addNewHeap的實(shí)現(xiàn)如下所示:
[cpp]viewplaincopy在CODE上杳看代碼片派生到我的代碼片
staticbooladdNewHeap(HeapSource*hs)
(
Heapheap;
asserl(hs!=NULL);
if(hs->numHcaps>=HEAP_SOURCE_MAX_HEAP_COUNT){
returnfalse;
)
mcmsct(&hcap,0,sizcof(hcap));
/*
*Heapstoragecomesfromacommonvirtualmemoryreservation.
*Thenewheapwillstartonthepageaftertheoldheap.
*/
char*base=hs->he叩s[O].brk;
sizc_toverhead=base-hs->hcaps[O].base;
assert(((size_t)hs->heaps[O].base&(SYSTEM_PAGE_SIZE-1))==0);
if(overhead+hs->mirFree>=hs->maximumSize){
returnfalse;
}
size_tmorecoreStart=SYSTEM_PAGE_SIZE;
heap.maximumSize=hs->growthLimit-overhead;
hcap.concurrcntStartBytes=hs->minFrec-CONCURRENT_START;
heap.base=base;
heap.limit=heap,base+heap.maximumSize;
heap.brk=heap.base+morecoreStart;
if(!「emapNewHe叩(hs.&heap)){
returnfalse;
}
hcap.msp=crcatcMspacc(basc,morecoreStart.hs->ininFrcc);
if(heap.msp==NULL){
returnfalse;
/*Don'tletthesoon-tc-be-oldheapgrowanyfurther.
*/
hs->hcaps[O].inaximumSizc=overhead;
hs->heaps[OJ.limit=base;
mspace_set_footprint_liniit(hs->heaps[0].msp,overhead);
/*Putthenewhe叩inthelist,atheaps[0].
*Shiftexistingheapsdown.
*/
memmove(&hs->heaps[1],&hs->heaps[0],hs->numHeaps*sizcof(hs->hcaps[OJ));
hs->heaps[0]=heap;
hs->numHeaps++;
returntrue;
這個(gè)函數(shù)定義在文件dalvik/vm/alloc/HeapSource.cpp中。
函數(shù)addNcwHcap所做的事情實(shí)際上就是將前面創(chuàng)建的Dalvik虛擬機(jī)Java堆一分
為二,得到兩個(gè)Heap。
在劃分之前,HeadSource結(jié)構(gòu)體hs只有一個(gè)Heap,如圖2所示:
overhwd
ta->heap$lo).basehs->heaps(0]brkhs->heap$[0].Wnrt
圖2Dalvi噓擬機(jī)Java堆一分為二之前
接下來(lái)在未使用的Dalvi噓擬機(jī)Java堆中創(chuàng)建另外一個(gè)Heap,如圖3所示:
h$->hejps[O].t>asehs->hups[0].trkhwnMheap.bm?t
圖3在未使用的Dalvi噓擬機(jī)Java地中創(chuàng)建一個(gè)新的Heap
最后調(diào)整HeadSource結(jié)構(gòu)體hs的heaps數(shù)組,即交heaps網(wǎng)和heaps⑴的值,結(jié)果
如圖4所示:
hs->hea3s[l].tanrths->heap5(O].ba$e
overhead
h5->heaps⑴.basehs->he?pS[l].br1chS->heap$[O].brkhs->he?p5|0pimrt
圖4DaM噓擬機(jī)Java堆一分為二之后
其中,heaps[l]就是我們?cè)趫D?中所說(shuō)的Zygote堆,而heaps⑼就是我們?cè)趫D1中所說(shuō)的Active
堆。以后無(wú)論是Zygote進(jìn)程,還是Zygote子進(jìn)程,需要分配對(duì)象時(shí),都在Active堆上進(jìn)行。
這樣就可以使得Zygote城最大限度地在Zygote進(jìn)程及其子進(jìn)程中共享。
這樣我們就分析完了函數(shù)addlnitialHcap及其相關(guān)函數(shù)的實(shí)現(xiàn),接下來(lái)我們繼續(xù)分析
函數(shù)dvmHeapBitmapInil和allocMarkSlack的實(shí)現(xiàn)。
函數(shù)dvniHeapBicmapInit的實(shí)現(xiàn)如下所示:
[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片
booldvmHeapBitmapInil(HeapBilmap*hb,constvoid*base,size」maxSize,
constchar*name)
(
void*bits;
size_tbitsLen;
asscrt(hb!=NULL);
asscrt(name!=NULL);
bitsLcn=HB_OFFSET_TO」NDEX(maxSize)*sizcof(*hb->bits);
bits=dvmAllocRegion(bilsLen,PROT_READ|PROT_WRITE,name);
if(bits==NULL){
ALOGE("Couldnotnunap%zd-byteashmemregion'%s"\bitsLen,name);
returnfalse;
)
hb->bits=(unsignedlong*)bits;
hb->bitsLcn=hb->allocLcn=bitsLcn;
hb->base=(uintptr_t)base;
hb->max=hb->base-1;
returntrue;
)
這個(gè)函數(shù)定義在文件dalvik/vm/alloc/HeapBitmap.cpp中。
參數(shù)hb指向一個(gè)HcapBitmap結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體正是函數(shù)dvmHcapBitmapInit要
進(jìn)行初始化的。參數(shù)base和maxSize描述的是Java堆的起始地址和大小。另外一個(gè)參數(shù)name
描述的是參數(shù)hb指向的HeapBitm叩結(jié)構(gòu)體的名稱。
在分析函數(shù)dvmHeapBitmapInit的實(shí)現(xiàn)之前,我們先來(lái)了解一下結(jié)構(gòu)體HeapBitmap
的定義,如下所示:
[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片
structHeapBmap{
/*Thebitmapdata,whichpointstoanmniap()edareacfzeroed
*anonymousmemory.
*/
unsignedlong*bits;
/*Thesizeoftheusedmemorypointedtobybits,inbytes.This
*valuechangeswhenthebitmapisshrunk.
*/
size_tbitsLen;
/*Therealsizeofthememorypointedtobybits.Thisisthe
*numberofbyteswerequestedfromtheallocatoranddoesnot
*change.
*/
size_tallocLcn;
/*Thebaseaddress,whichcorrespondstothefirstbitin
*thebitmap.
*/
uintptr_tbase;
/*Thehighestpointervalueeverreturnedbyanallocation
*from(hisheap.I.e.,(hehighestaddressthatmaycorrespond
*toasetbit.Iftherearenobitsset,(max<base).
*/
uintptr_tmax;
);
這個(gè)結(jié)構(gòu)體定義在文件dalvik/vm/alloc/HeapBilir.ap.h。
代碼對(duì)HcapBitm叩結(jié)構(gòu)體的各個(gè)成員變量的含義已經(jīng)有很詳細(xì)的注釋,其中最重
要的就是成員變量bits指向的一個(gè)類型為unsignedlong的數(shù)組,這個(gè)數(shù)組的每一個(gè)bit都用
來(lái)標(biāo)記一個(gè)對(duì)象是否存活,
回到函數(shù)dvmHeapBitmapInit中,Java堆的起始地址為base,大小為maxSize,由此
我們就知道,在Java堆上創(chuàng)建的對(duì)象的地址范圍為[base,maxSize)。但是通過(guò)C庫(kù)提供的
mspace_malloc來(lái)在Java堆分配內(nèi)存時(shí),得到的內(nèi)存地虻是以8字節(jié)對(duì)齊的。這意味著我們
只需要(maxSize/8)個(gè)bit來(lái)描述Java堆的對(duì)象。結(jié)構(gòu)體HcapBitmap的成員變量bits是一個(gè)
類型為unsignedlong的數(shù)組,也就是說(shuō),數(shù)組中的每一個(gè)元素都可以描述sizeof(unsignedlong)
個(gè)對(duì)象的存活。在32位設(shè)備上,一個(gè)unsignedlong占用32個(gè)bit,這意味著需要一個(gè)大小
為(maxSize/8/32)的unsignedlong數(shù)組來(lái)描述Java堆對(duì)象的存活。如果換成字節(jié)數(shù)來(lái)描述
的話,就是說(shuō)我們需要一塊大小為(maxSize/8/32)X4的內(nèi)存塊來(lái)描述一個(gè)大小為
maxSize的Java堆對(duì)象。
Dalvik虛擬機(jī)提供了一些宏來(lái)描述對(duì)象地址與HcapBitmap結(jié)構(gòu)體的成員變量bits所
描述的unsignedlong數(shù)組的關(guān)系,如下所示:
[epp]viewplaincopy在CODE上查看代碼片派生到我的代碼片
#defineHB_OBJECT_ALIGNMENT8
#defineHB_BITS_PER_WORD(sizeof(unsignedlong)*CHAR_BIT)
/*<offsct>isthedifferencefrom.basetoapointeraddress.
*<index>istheindexof.bitsthatcontainsthebitrepresenting
*<offset>.
*/
#defineHB_OFFSET_TOJNDEX(offset_)\
((uintptr_t)(offset_)/HB_OBJECT_AL1GNMENT/HB_BITS_PER_WORD)
#defineHB_INDEX_TO_OFFSET(index.)\
((uintptr_t)(index_)*HB_OBJECT_ALIGNMENT*HB_BITS_PER_WORD)
#defineHB_OFFSET_TO_BYTE」NDEX(offset_)\
(HB_OFFSET_TO_INDEX(offset_)*sizeof(*((HeapBitmap*)0)->bils))
這些宏定義在文巳dalvik/vm/alloc/HeapBitmap.h中。
假設(shè)我們知道了一個(gè)對(duì)象的地址為ptr,Java堆的起始地址為base,那么就可以計(jì)算
得到一個(gè)偏移值offset。有了這個(gè)偏移值之后,就可以通過(guò)宏HB_OFFSET_TO」NDEX計(jì)算
得到用來(lái)描述該對(duì)象存活的bit位于HcapBitmap結(jié)構(gòu)體的成員變量bits所描述的unsigned
long數(shù)組的索引index。有了這個(gè)index之后,我們就可以得到一個(gè)unsignedlong值。接著
再通過(guò)對(duì)象地址ptr的第4到第8位表示的數(shù)值為索引,在前面找到的unsignedlong值取出
相應(yīng)的位,就可以得到該對(duì)象是否存活了。
相反,給出一個(gè)HeapBitmap結(jié)構(gòu)體的成員變顯bits所描述的unsignedlong數(shù)組的
索引index,我們可以通過(guò)宏HB」NDEX_TO_OFFSET找到一個(gè)偏移值offset,將這個(gè)偏移
值加上Java堆的起始地址base,就可以得到一個(gè)Java對(duì)象的地址ptr。
第三個(gè)宏HB_OFFSET_TO_BYTEJNDEX借助宏HB_OFFSET_TO_INDEX來(lái)找出
用來(lái)描述對(duì)象存活的bitHeapBitmap結(jié)構(gòu)體的成員變量bits所描述的內(nèi)存塊的字節(jié)索弓I。
有了上述的基礎(chǔ)知識(shí)之后,函數(shù)dvmHcapBitinapInit的實(shí)現(xiàn)就一目了然了。
接下來(lái)我們?cè)賮?lái)看函數(shù)allocMarkStack的實(shí)現(xiàn),如下所示:
[epp]viewplaincopy在CODE上查看代碼片派生到我的代碼片
staticboolallocMarkStack(GcMarkS(ack*s(ack,size_tmaximumSize)
(
constchar*name="dalvik-mark-stack";
void*addr;
assert(stack!=NULL);
stack->lcngth=maximumSize*sizcof(Objcct*)/
(sizeof(Object)+HEAP_SOURCE_CHUNK_OVERHEAD);
addr=dvmAllocRegicn(stack->length,PROT_READ|PROT_WRITE,name);
if(addr==NULL){
returnfalse;
stack->base=(constObject**)addr;
stack->limit=(constObject**)((char*)addr+stack->length);
stack->top=NULL;
niadvise(stack->base,stack->length,MADV_DONTNEED);
returntrue;
)
這個(gè)函數(shù)定義在文件vm/alloc/HeapSource.cpp中。
參數(shù)stack指向的是一個(gè)GcMarkStack結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體正是函數(shù)allocMarkStack
要進(jìn)行初始化的。參數(shù)maximumSize描述的是Java堆的大小。
同樣是在分析函數(shù)allocMarkStack的實(shí)現(xiàn)之前,我們先來(lái)了解一下結(jié)構(gòu)體
GcMarkStack的定義,如下所示:
[epp]viewplaincopy在CODE上查看代碼片派生到我的代碼片
structGcMarkStack{
/*Highestaddress(exclusive)
*/
constObject
/*Currenttopofthestack(exclusive)
*/
constObject**top;
/*Lowestaddress(inclusive)
*/
constObject**base;
/*Maximumstacksize,inbytes.
*/
size_tlength;
};
這個(gè)結(jié)構(gòu)體定義在文件dalvik/vm/alloc/MarkSwecp.h<>
代碼對(duì)HeapBitmap結(jié)構(gòu)體的各個(gè)成員變量的含義己經(jīng)有很詳細(xì)的注釋??偨Y(jié)來(lái)說(shuō),
GcMarkStack通過(guò)一個(gè)Object*數(shù)組來(lái)描述一個(gè)棧。這人Object*數(shù)組的大小通過(guò)成員變量
length來(lái)描述。成員變量base和limit分別描述棧的最低地址和最高地址,另外一個(gè)成員變
量top指向棧頂。
回到函數(shù)allocMarkStack中,我們分析一下需要一個(gè)多大的棧來(lái)描述Java堆的所有
對(duì)象。首先,每一個(gè)Java對(duì)象都是必須要從Object結(jié)構(gòu)體繼承卜來(lái)的,這意味著每一個(gè)Java
對(duì)象占用的內(nèi)存都至少為sizeof(Object)。其次,通過(guò)C庫(kù)提供的接口mspace_mallocJava
堆上為對(duì)象分配內(nèi)存時(shí),C庫(kù)自己需要?些額外的內(nèi)存來(lái)管理該塊內(nèi)存,例如用額外的4個(gè)
字節(jié)來(lái)記錄分配出去的內(nèi)存塊的大小。額外需要的內(nèi)存大小通過(guò)宏
HEAP_SOURCE_CHUNK_OVERHEAD來(lái)描述。最后,我們就可以知道,一個(gè)大小為
maximumSize的Java堆,在最壞情況下,存在(maximumSize/(sizeof(Object)+
HEAP_SOURCE_CHUNK_OVERHEAD))個(gè)對(duì)象。也就是說(shuō),GcMarkStack通過(guò)一個(gè)大小為
(maximumSize/(sizcof(Objcct)+HEAP_SOURCE_CHUNK_OVERHEAD))WObject*數(shù)組來(lái)
描述一個(gè)棧。如果換成字節(jié)數(shù)來(lái)描述的話,就是說(shuō)我們需要一塊大小為(maximumSize*
sizeof(Object*)/(sizeof(Object)+HEAP_SOURCE_CHLNK_OVERHEAD))的內(nèi)存塊來(lái)描述
一個(gè)GcMarkStack棧。
有了上述的基礎(chǔ)知識(shí)之后,函數(shù)allocMarkStack的實(shí)現(xiàn)同樣也一目了然了。
這樣,函數(shù)dvmHcapSourccStartup及其相關(guān)的函數(shù)dvmAilocRcgion>crcatcMspace>
addInitialHeap>dvmHeapBiimapInil和allockMarkSlack的實(shí)現(xiàn)我們就分析完了,回到前面的
函數(shù)dvmHeapStailup中,它調(diào)用函數(shù)dvmHeapSourceStartup創(chuàng)建完成Java堆及其相關(guān)的
HeapBitmap和MarkSlack之后,還需要繼續(xù)調(diào)用函數(shù)dvmCardTableStartup來(lái)創(chuàng)建
溫馨提示
- 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒(méi)有圖紙預(yù)覽就沒(méi)有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 轉(zhuǎn)正輔警考試試題及答案
- 在線考試系統(tǒng)的應(yīng)用與推廣
- 知識(shí)付費(fèi)產(chǎn)品經(jīng)理面試題及答案
- 老化測(cè)試工程師崗位老化測(cè)試風(fēng)險(xiǎn)評(píng)估含答案
- 航天科技工程師崗位面試題庫(kù)含答案
- 廣州港辦公室主任管理能力考試題含答案
- 2025年區(qū)塊鏈技術(shù)助力供應(yīng)鏈透明化項(xiàng)目可行性研究報(bào)告
- 2025年AR技術(shù)在博物館應(yīng)用項(xiàng)目可行性研究報(bào)告
- 2025年銀行金融科技應(yīng)用項(xiàng)目可行性研究報(bào)告
- 2025年智能農(nóng)業(yè)管理軟件開(kāi)發(fā)項(xiàng)目可行性研究報(bào)告
- 電商售后客服主管述職報(bào)告
- 2025昆明市呈貢區(qū)城市投資集團(tuán)有限公司及下屬子公司第一批招聘(12人)筆試考試參考試題及答案解析
- 受控文件管理流程
- GB/T 30341-2025機(jī)動(dòng)車駕駛員培訓(xùn)教練場(chǎng)技術(shù)要求
- 2025年黑龍江省哈爾濱市中考數(shù)學(xué)真題含解析
- 2026年湖南現(xiàn)代物流職業(yè)技術(shù)學(xué)院?jiǎn)握新殬I(yè)技能考試題庫(kù)附答案
- 河北省2025年職業(yè)院校嵌入式系統(tǒng)應(yīng)用開(kāi)發(fā)賽項(xiàng)(高職組)技能大賽參考試題庫(kù)(含答案)
- 2025譯林版新教材初中英語(yǔ)八年級(jí)上冊(cè)單詞表(復(fù)習(xí)必背)
- 企業(yè)微信基礎(chǔ)知識(shí)培訓(xùn)
- 《房間空氣調(diào)節(jié)器室內(nèi)熱舒適性評(píng)價(jià)方法》
- 2025秋期版國(guó)開(kāi)電大本科《管理英語(yǔ)3》一平臺(tái)綜合測(cè)試形考任務(wù)在線形考試題及答案
評(píng)論
0/150
提交評(píng)論