part 2
這個comment希望能分析一下GenCollectedHeap::do_collection這個函數(shù)的具體執(zhí)行流程泌枪,根據(jù)函數(shù)名字可以猜測該函數(shù)實現(xiàn)的功能就是做垃圾回收近哟,下面是它的方法聲明(聲明和定義是有區(qū)別的啼肩,聲明僅僅是告訴別人有這樣一個函數(shù),而定義則是說這個函數(shù)具體實現(xiàn)了什么功能):
// Helper function for two callbacks below.
// Considers collection of the first max_level+1 generations.
void do_collection(bool full,
bool clear_all_soft_refs,
size_t size,
bool is_tlab,
GenerationType max_generation);
參數(shù)full代表是否是FullGC,clear_all_soft_refs參數(shù)表示是否要回收sort reference,size參數(shù)需要多說明一點,在一些情況下涎跨,GC發(fā)生是因為發(fā)送了"Allocate Fail",這個size就代表了申請分配的內(nèi)存大新浴;is_tlab表示是否使用 TLAB(線程分配Buffer碘赖,可以避免多線程在堆上并發(fā)申請內(nèi)存)驾荣,max_generation參數(shù)表示最大的回收代,只有兩種類型普泡,YoungGen或者OldGen播掷;下面來仔細(xì)分析一下這個函數(shù)。
- (1)撼班、首先是做一些基本的校驗歧匈,比如是否在safe_point,是否是GC線程訪問該函數(shù)砰嘁,以及是否已經(jīng)有其他的線程觸發(fā)了GC件炉,這些條件都需要滿足才能執(zhí)行接下來的代碼。
- (2)矮湘、接下來需要做一些GC策略的生成斟冕,主要是判斷是否回收soft reference對象,是否收集Young或者Old區(qū)域等缅阳。complete表示是否收集整個堆磕蛇,old_collects_young表示是否在收集老年代的同時收集新生代,也就是是否有必要收集新生代十办,JVM參數(shù)ScavengeBeforeFullGC控制是否在FullGC前做一次YoungGC秀撇,如果設(shè)置了該參數(shù),那在收集old區(qū)的時候就沒有必要再回收young區(qū)了向族;do_young_collection表示是否需要對young區(qū)域進(jìn)行垃圾收集呵燕,判斷標(biāo)準(zhǔn)就是young區(qū)域確實需要回收了,也就是進(jìn)行YoungGC件相,
- (3)虏等、現(xiàn)在弄唧,知道該回收哪些區(qū)域了,那么接下來就去回收需要回收的區(qū)域霍衫,如果do_young_collection是true的候引,那么就執(zhí)行YoungGC,collect_generation函數(shù)是具體的執(zhí)行某個區(qū)域垃圾回收的入口敦跌,待會再來分析這個函數(shù)的具體流程澄干;接著也判讀oldGen是否需要回收,如果需要的話也進(jìn)行回收柠傍。
- (4)麸俘、垃圾收集完成之后,需要計算各個分代的大小因為GC之后堆可能會擴(kuò)展惧笛,所以需要重新計算一下各個分代的大小从媚,重新計算大小通過調(diào)用函數(shù)compute_new_size實現(xiàn),該函數(shù)需要調(diào)整各個分代的各種指針患整,使得堆擴(kuò)展后各個分代依然可以正常工作拜效。
下面,來分析上面幾個步驟中出現(xiàn)的一些關(guān)鍵函數(shù)各谚,首先是should_collect函數(shù)紧憾,該函數(shù)用于判斷某一個分代是否需要做垃圾回收下面來看看該方法的細(xì)節(jié)
// Returns "true" iff collect() should subsequently be called on this
// this generation. See comment below.
// This is a generic implementation which can be overridden.
//
// Note: in the current (1.4) implementation, when genCollectedHeap's
// incremental_collection_will_fail flag is set, all allocations are
// slow path (the only fast-path place to allocate is DefNew, which
// will be full if the flag is set).
// Thus, older generations which collect younger generations should
// test this flag and collect if it is set.
virtual bool should_collect(bool full,
size_t word_size,
bool is_tlab) {
return (full || should_allocate(word_size, is_tlab));
}
如果是FullGC,那么無論哪個分代都應(yīng)該被回收昌渤,如果不是FullGC赴穗,那么就使用should_allocate函數(shù)繼續(xù)判斷是否需要在該分代進(jìn)行收集,比如對于DefNew(Serial GC下新生代)分代來說膀息,其具體實現(xiàn)就如下:
// Allocation support
virtual bool should_allocate(size_t word_size, bool is_tlab) {
assert(UseTLAB || !is_tlab, "Should not allocate tlab");
size_t overflow_limit = (size_t)1 << (BitsPerSize_t - LogHeapWordSize);
const bool non_zero = word_size > 0;
const bool overflows = word_size >= overflow_limit;
const bool check_too_big = _pretenure_size_threshold_words > 0;
const bool not_too_big = word_size < _pretenure_size_threshold_words;
const bool size_ok = is_tlab || !check_too_big || not_too_big;
bool result = !overflows &&
non_zero &&
size_ok;
return result;
}
接著一個重要的函數(shù)就是collect_generation般眉,這個函數(shù)將回收給定的分代中的垃圾,主要看下面的這段代碼片段:
// Do collection work
{
// Note on ref discovery: For what appear to be historical reasons,
// GCH enables and disabled (by enqueing) refs discovery.
// In the future this should be moved into the generation's
// collect method so that ref discovery and enqueueing concerns
// are local to a generation. The collect method could return
// an appropriate indication in the case that notification on
// the ref lock was needed. This will make the treatment of
// weak refs more uniform (and indeed remove such concerns
// from GCH). XXX
HandleMark hm; // Discard invalid handles created during gc
save_marks(); // save marks for all gens
// We want to discover references, but not process them yet.
// This mode is disabled in process_discovered_references if the
// generation does some collection work, or in
// enqueue_discovered_references if the generation returns
// without doing any work.
ReferenceProcessor* rp = gen->ref_processor();
// If the discovery of ("weak") refs in this generation is
// atomic wrt other collectors in this configuration, we
// are guaranteed to have empty discovered ref lists.
if (rp->discovery_is_atomic()) {
rp->enable_discovery();
rp->setup_policy(clear_soft_refs);
} else {
// collect() below will enable discovery as appropriate
}
gen->collect(full, clear_soft_refs, size, is_tlab);
if (!rp->enqueuing_is_done()) {
rp->enqueue_discovered_references();
} else {
rp->set_enqueuing_is_done(false);
}
rp->verify_no_references_recorded();
}
接著看gen->collect函數(shù)調(diào)用潜支,這里面就是做具體的垃圾收集工作煤篙,比如下面分析在DefNew分代中的gen->collect實現(xiàn)。
- (1)毁腿、DefNew是Serial GC下的新生代辑奈,首先它要判斷是否有必要讓老年代來做這次GC,使用collection_attempt_is_safe函數(shù)來做這個判斷已烤,也就是判斷出收集該區(qū)域是否是安全的鸠窗,所謂安全的,就是DefNew分代收集了之后胯究,old 區(qū)域是否可以完整的將這次Minor GC之后晉升的對象安置起來稍计,如果不能的話,那DefNew就舉得自己做GC是不安全的裕循,應(yīng)該讓老年代來做GC臣嚣,這也是最合適的選擇净刮,老年代會做一次規(guī)模宏大的GC,并且做一些內(nèi)存規(guī)整的工作硅则,避免新生代中晉升上來的大對象無法找到連續(xù)的空間放置淹父,當(dāng)然,老年代GC實現(xiàn)上幾乎都包含"整理"的階段怎虫,這也是為什么老年代發(fā)生GC耗時是新生代GC的10倍的原因之一暑认,新生代使用copying算法,是一種非炒笊螅快速的收集算法蘸际,當(dāng)然也得益于新生代中的對象壽命都比較短,不像老年代中的對象壽命較長徒扶,當(dāng)然粮彤,這也是分代的意義所在;
[圖片上傳失敗...(image-f91c09-1627873692281)]
collection_attempt_is_safe函數(shù)的實現(xiàn)如下:
bool DefNewGeneration::collection_attempt_is_safe() {
if (!to()->is_empty()) {
log_trace(gc)(":: to is not empty ::");
return false;
}
if (_old_gen == NULL) {
GenCollectedHeap* gch = GenCollectedHeap::heap();
_old_gen = gch->old_gen();
}
return _old_gen->promotion_attempt_is_safe(used());
}
正常來說姜骡,用區(qū)域內(nèi)兩個survivor中有一個區(qū)域總是空閑的导坟,但是在某些情況下也會發(fā)生意外,使得兩個survivor都不為空溶浴,這種情況是有可能發(fā)生的乍迄,首先DefNew在進(jìn)行YoungGC之后管引,會將Eden + From中存活的對象拷貝到To中去士败,并且將一些符合晉升要求的對象拷貝到old區(qū)域中去,然后調(diào)換兩個survivor的角色褥伴,所以按理來說其中某個survivor區(qū)域總是空的谅将,但是這是在YoungGC順利完成的情況,在發(fā)生"promotion failed"的時候就不會去清理From和To重慢,這一點在后續(xù)會再次說明饥臂;但是肯定的是,如果To區(qū)域不為空似踱,那么就說明前一次YoungGC并不是很順利隅熙,此時DefNew就舉得沒必要再冒險去做一次可能沒啥用處的Minor GC,因為有可能Minor GC之后需要出發(fā)一次Full GC來解決某些難題核芽,所以DefNew基于自己的歷史GC告訴Old去做一些較為徹底的GC工作時必要的囚戚;如果沒有發(fā)生"promotion fail"這種不愉快的事情,那么接下來就讓old區(qū)自己判斷是否允許本次Minor GC的發(fā)生轧简,也就是_old_gen->promotion_attempt_is_safe的調(diào)用驰坊,下面來看看該函數(shù)的具體實現(xiàn);
bool TenuredGeneration::promotion_attempt_is_safe(size_t max_promotion_in_bytes) const {
size_t available = max_contiguous_available();
size_t av_promo = (size_t)gc_stats()->avg_promoted()->padded_average();
bool res = (available >= av_promo) || (available >= max_promotion_in_bytes);
log_trace(gc)("Tenured: promo attempt is%s safe: available(" SIZE_FORMAT ") %s av_promo(" SIZE_FORMAT "), max_promo(" SIZE_FORMAT ")",
res? "":" not", available, res? ">=":"<", av_promo, max_promotion_in_bytes);
return res;
}
老年代也會看歷史數(shù)據(jù)哮独,如果發(fā)現(xiàn)老年代的最大連續(xù)空間大小大于新生代歷史晉升的平均大小或者新生代中存活的對象拳芙,那么老年代就認(rèn)為本次Minor GC是安全的察藐,沒必要做一次Full GC;當(dāng)然這是有一些冒險的成分的舟扎,如果某一次minorGC發(fā)生之后符合晉升條件的對象大小遠(yuǎn)遠(yuǎn)大小評價晉升大小分飞,而且這個時候老年代連續(xù)空間小于這些符合晉升的對象大小的時候,悲劇就發(fā)生了浆竭,也就是上面說到的"promotion fail"浸须,這個時候就要做一次Full GC。
- (2)邦泄、接著關(guān)鍵的一個步驟就是進(jìn)行對象存活判斷删窒,并且將存活的對象轉(zhuǎn)移到正確的位置,比如To區(qū)域或者old區(qū)域顺囊;
FastEvacuateFollowersClosure是一個遞歸的過程肌索,Closure后綴代表 它是一個回調(diào)操作,所謂遞歸特碳,就是在判斷對象存活并且copying的工作是遞歸進(jìn)行的诚亚,首先找到root objects,然后根據(jù)root objects去標(biāo)記存活的對象午乓,并且將它們轉(zhuǎn)移到合適的區(qū)域中去站宗;gch->young_process_roots做的工作就是將root objects轉(zhuǎn)移到其他空間去的函數(shù):
void GenCollectedHeap::young_process_roots(StrongRootsScope* scope,
OopsInGenClosure* root_closure,
OopsInGenClosure* old_gen_closure,
CLDClosure* cld_closure) {
MarkingCodeBlobClosure mark_code_closure(root_closure, CodeBlobToOopClosure::FixRelocations);
process_roots(scope, SO_ScavengeCodeCache, root_closure, root_closure,
cld_closure, cld_closure, &mark_code_closure);
process_string_table_roots(scope, root_closure);
if (!_process_strong_tasks->is_task_claimed(GCH_PS_younger_gens)) {
root_closure->reset_generation();
}
// When collection is parallel, all threads get to cooperate to do
// old generation scanning.
old_gen_closure->set_generation(_old_gen);
rem_set()->younger_refs_iterate(_old_gen, old_gen_closure, scope->n_threads());
old_gen_closure->reset_generation();
_process_strong_tasks->all_tasks_completed(scope->n_threads());
}
這里面關(guān)鍵的函數(shù)是process_roots,該函數(shù)會對設(shè)置的各種Closure進(jìn)行回調(diào)益愈,比如FastScanClosure梢灭,具體的回調(diào)工作將在Closure的do_oop_work進(jìn)行:
// NOTE! Any changes made here should also be made
// in ScanClosure::do_oop_work()
template <class T> inline void FastScanClosure::do_oop_work(T* p) {
T heap_oop = oopDesc::load_heap_oop(p);
// Should we copy the obj?
if (!oopDesc::is_null(heap_oop)) {
oop obj = oopDesc::decode_heap_oop_not_null(heap_oop);
if ((HeapWord*)obj < _boundary) {
assert(!_g->to()->is_in_reserved(obj), "Scanning field twice?");
oop new_obj = obj->is_forwarded() ? obj->forwardee()
: _g->copy_to_survivor_space(obj);
oopDesc::encode_store_heap_oop_not_null(p, new_obj);
if (is_scanning_a_klass()) {
do_klass_barrier();
} else if (_gc_barrier) {
// Now call parent closure
do_barrier(p);
}
}
}
}
如果對象已經(jīng)被復(fù)制過了,那么就不用再復(fù)制一次了蒸其,否則調(diào)用copy_to_survivor_space將該對象復(fù)制到to區(qū)域中去敏释,下面是copy_to_survivor_space函數(shù)的具體實現(xiàn):
oop DefNewGeneration::copy_to_survivor_space(oop old) {
assert(is_in_reserved(old) && !old->is_forwarded(),
"shouldn't be scavenging this oop");
size_t s = old->size();
oop obj = NULL;
// Try allocating obj in to-space (unless too old)
if (old->age() < tenuring_threshold()) {
obj = (oop) to()->allocate_aligned(s);
}
// Otherwise try allocating obj tenured
if (obj == NULL) {
obj = _old_gen->promote(old, s);
if (obj == NULL) {
handle_promotion_failure(old);
return old;
}
} else {
// Prefetch beyond obj
const intx interval = PrefetchCopyIntervalInBytes;
Prefetch::write(obj, interval);
// Copy obj
Copy::aligned_disjoint_words((HeapWord*)old, (HeapWord*)obj, s);
// Increment age if obj still in new generation
obj->incr_age();
age_table()->add(obj, s);
}
// Done, insert forward pointer to obj in this header
old->forward_to(obj);
return obj;
}
這個函數(shù)的流程大概是這樣的:首先判斷對象是否達(dá)到了晉升到老年代的年齡閾值,如果到了摸袁,那么就要將對象拷貝到老年代中去钥顽,否則就要將對象拷貝到to區(qū)域中去,這里面也包括一個細(xì)節(jié)靠汁,如果對象沒有達(dá)到晉升老年代的年齡閾值蜂大,但是無法拷貝到To區(qū)域中去,那么也試圖將對象晉升到老年代蝶怔,也就是將對象提前晉升奶浦,晉升是有風(fēng)險的,可能晉升失敗添谊,那么就要通過調(diào)用handle_promotion_failure來處理晉升失敗的情況财喳,如果對象成功拷貝到了To區(qū)域中來,那么就要將對象的年齡更新一下,最后耳高,需要需要標(biāo)記對象已經(jīng)被轉(zhuǎn)移扎瓶,如果可能,那么就把老的對象清空吧泌枪;下面來先來看看promote函數(shù)概荷,該函數(shù)用于將對象晉升到老年代:
// Ignores "ref" and calls allocate().
oop Generation::promote(oop obj, size_t obj_size) {
assert(obj_size == (size_t)obj->size(), "bad obj_size passed in");
#ifndef PRODUCT
if (GenCollectedHeap::heap()->promotion_should_fail()) {
return NULL;
}
#endif // #ifndef PRODUCT
HeapWord* result = allocate(obj_size, false);
if (result != NULL) {
Copy::aligned_disjoint_words((HeapWord*)obj, result, obj_size);
return oop(result);
} else {
GenCollectedHeap* gch = GenCollectedHeap::heap();
return gch->handle_failed_promotion(this, obj, obj_size);
}
}
這個函數(shù)較為簡單,首先通過allocate函數(shù)試圖在老年代申請一塊可以容納對象的內(nèi)存碌燕,如果成功了误证,那么就將對象復(fù)制到里面去,否則通過handle_failed_promotion函數(shù)來處理晉升失敗的情況修壕,晉升失敗的前提下愈捅,handle_failed_promotion在handle_promotion_failure前執(zhí)行,看起來都是處理晉升失敗的情況慈鸠,下面先看看handle_failed_promotion:
oop GenCollectedHeap::handle_failed_promotion(Generation* old_gen,
oop obj,
size_t obj_size) {
guarantee(old_gen == _old_gen, "We only get here with an old generation");
assert(obj_size == (size_t)obj->size(), "bad obj_size passed in");
HeapWord* result = NULL;
result = old_gen->expand_and_allocate(obj_size, false);
if (result != NULL) {
Copy::aligned_disjoint_words((HeapWord*)obj, result, obj_size);
}
return oop(result);
}
可以看到蓝谨,oldGen將試圖去擴(kuò)展自己的堆空間來讓更多的新生代對象可以成功晉升,但是很多情況下青团,堆空間被設(shè)置為不可擴(kuò)展譬巫,這種情況下這個方法也就做了無用功,接著會調(diào)用handle_promotion_failure督笆,調(diào)用handle_promotion_failure代表老年代也就明確告訴新生代無法將本次晉升的這個對象放置到老年代芦昔,來看看handle_promotion_failure會有什么對策:
void DefNewGeneration::handle_promotion_failure(oop old) {
log_debug(gc, promotion)("Promotion failure size = %d) ", old->size());
_promotion_failed = true;
_promotion_failed_info.register_copy_failure(old->size());
_preserved_marks_set.get()->push_if_necessary(old, old->mark());
// forward to self
old->forward_to(old);
_promo_failure_scan_stack.push(old);
if (!_promo_failure_drain_in_progress) {
// prevent recursion in copy_to_survivor_space()
_promo_failure_drain_in_progress = true;
drain_promo_failure_scan_stack();
_promo_failure_drain_in_progress = false;
}
}
看起來DefNew還是比較樂觀的,既然老年代容納不了你娃肿,那么這個晉升的對象就還呆在新生代吧咕缎,說不定下次老年代發(fā)生GC就可以成功把它拷貝過去呢。這個時候_promotion_failed也被標(biāo)記物為了true咸作,這個標(biāo)記之后會有用锨阿,發(fā)生"promotion fail"之后From區(qū)域可能存在一些對象沒有成功晉升到老年代宵睦,但是又不是垃圾记罚,這個時候From和To區(qū)域都不為空了,這是個難題壳嚎。
接著桐智,是時候執(zhí)行遞歸標(biāo)記&復(fù)制的過程了,也就是evacuate_followers.do_void()烟馅,這個過程是非常復(fù)雜的说庭,下面來稍微看看這個函數(shù):
void DefNewGeneration::FastEvacuateFollowersClosure::do_void() {
do {
_gch->oop_since_save_marks_iterate(GenCollectedHeap::YoungGen, _scan_cur_or_nonheap, _scan_older);
} while (!_gch->no_allocs_since_save_marks());
guarantee(_young_gen->promo_failure_scan_is_complete(), "Failed to finish scan");
}
不斷使用oop_since_save_marks_iterate來做遞歸遍歷的工作,結(jié)束條件是通過no_allocs_since_save_marks來決定的郑趁,下面是no_allocs_since_save_marks函數(shù)的具體實現(xiàn):
bool GenCollectedHeap::no_allocs_since_save_marks() {
return _young_gen->no_allocs_since_save_marks() &&
_old_gen->no_allocs_since_save_marks();
}
看名字應(yīng)該是說沒有分配發(fā)生了刊驴,比如看看DefNew的no_allocs_since_save_marks函數(shù)實現(xiàn):
bool DefNewGeneration::no_allocs_since_save_marks() {
assert(eden()->saved_mark_at_top(), "Violated spec - alloc in eden");
assert(from()->saved_mark_at_top(), "Violated spec - alloc in from");
return to()->saved_mark_at_top();
}
top()指向To區(qū)域空閑空間的起點,上面已經(jīng)說過的一個過程是將root objects先標(biāo)記并且拷貝到To區(qū)域或者老年代,這個時候To區(qū)域內(nèi)已經(jīng)存在的對象是存活的捆憎,需要遞歸遍歷這些對象引用的對象舅柜,然后也進(jìn)行拷貝工作,saved_mark_at_top就是判斷是否還在有對象唄拷貝到To區(qū)域中來躲惰,如果還有對象拷貝進(jìn)來致份,那么就說明GC還沒有完成,繼續(xù)循環(huán)執(zhí)行oop_since_save_marks_iterate础拨,否則就可以停止了氮块;下面來看看oop_since_save_marks_iterate函數(shù)的實現(xiàn):
#define ContigSpace_OOP_SINCE_SAVE_MARKS_DEFN(OopClosureType, nv_suffix) \
\
void ContiguousSpace:: \
oop_since_save_marks_iterate##nv_suffix(OopClosureType* blk) { \
HeapWord* t; \
HeapWord* p = saved_mark_word(); \
assert(p != NULL, "expected saved mark"); \
\
const intx interval = PrefetchScanIntervalInBytes; \
do { \
t = top(); \
while (p < t) { \
Prefetch::write(p, interval); \
debug_only(HeapWord* prev = p); \
oop m = oop(p); \
p += m->oop_iterate_size(blk); \
} \
} while (t < top()); \
\
set_saved_mark_word(p); \
}
ALL_SINCE_SAVE_MARKS_CLOSURES(ContigSpace_OOP_SINCE_SAVE_MARKS_DEFN)
在深入下去的部分就比較復(fù)雜了,不再做分析诡宗,但是需要注意的一點是滔蝉,DefNew在將存活對象復(fù)制到To區(qū)域的時候,Eden + From區(qū)域的對象是否存活不僅僅會看是否被To區(qū)域的對象引用塔沃,還會看老年代是否存在跨代引用新生代的對象的情況锰提,這種情況也需要將存活的對象轉(zhuǎn)到To或者老年代。
- (3)芳悲、接下來需要對GC過程中發(fā)現(xiàn)的引用進(jìn)行一些處理立肘,比如是否回收soft reference,以及堆weak reference的回收等工作名扛;
- (4)谅年、到此GC工作大概已經(jīng)完成了,接下來需要做一些收尾工作肮韧,如果發(fā)現(xiàn)在Minor GC的過程中發(fā)生了"promotion fail",那么就要做特殊的處理融蹂,younger_refs_iterate會將那些晉升失敗的對象恢復(fù)回來,否則下一次發(fā)生Minor GC的時候會誤以為這些對象已經(jīng)被復(fù)制過了弄企,但是他們確實沒有被轉(zhuǎn)移成功超燃,這樣的話,這些對象可能一直留在新生代拘领,無論經(jīng)歷多少次GC都無法發(fā)生轉(zhuǎn)移意乓;
[圖片上傳失敗...(image-b054f0-1627873692281)]
無論如何,新生代發(fā)生了GC约素,經(jīng)過這次GC届良,需要轉(zhuǎn)換From和To兩個survivor的角色,swap_spaces函數(shù)實現(xiàn)了這個功能:
void DefNewGeneration::swap_spaces() {
ContiguousSpace* s = from();
_from_space = to();
_to_space = s;
eden()->set_next_compaction_space(from());
// The to-space is normally empty before a compaction so need
// not be considered. The exception is during promotion
// failure handling when to-space can contain live objects.
from()->set_next_compaction_space(NULL);
if (UsePerfData) {
CSpaceCounters* c = _from_counters;
_from_counters = _to_counters;
_to_counters = c;
}
}
這個函數(shù)較為簡單圣猎,只是swap了一下From和To士葫;再說一句,如果沒有發(fā)生"Promotion Fail"送悔,那么在Minor GC之后慢显,需要將From和Eden清空爪模,因為沒有發(fā)生晉升失敗事件,就說明所以在新生代(Eden + From)存活的對象都安全的轉(zhuǎn)移到了To或者老年代荚藻,所以可以清空呻右,但是發(fā)生晉升失敗意味著有部分存活的對象依然還留在原地等待,所以不能clear掉鞋喇。