曹工雜談:Java 類加載還會死鎖?這是什麼狀況?

1、前言

今天事不是不少,正好在Java交流羣裏,看到一個比較有意思的問題,因而花了點時間研究了一下,這裏作個簡單的分享。html

先貼一份測試代碼,你們能夠先猜想一下,執行結果會是怎樣的:java

 2 
 3 import java.util.concurrent.TimeUnit;  4 
 5 
 6 public class TestClassLoading {  7     public static class A{  8         static {  9             System.out.println("class A init"); 10             try { 11                 TimeUnit.SECONDS.sleep(1); 12             } catch (InterruptedException e) { 13  e.printStackTrace(); 14  } 15             new B(); 16  } 17 
18         public static void test() { 19             System.out.println("aaa"); 20  } 21  } 22 
23     public static class B{ 24         static { 25             System.out.println("class B init"); 26             new A(); 27  } 28 
29 
30         public static void test() { 31             System.out.println("bbb"); 32  } 33  } 34     public static void main(String[] args) { 35         new Thread(() -> A.test()).start(); 36         new Thread(() -> B.test()).start(); 37  } 38 }

 

不知道,你猜對了沒有呢,實際的執行結果會是下面這樣的:c++

 

2、緣由分析

這裏,一開始你們分析的是,和new有關係;但下面的代碼和上面的結果徹底一致,基本能夠排除 new 的嫌疑:bootstrap

 1 public class TestClassLoadingNew {  2     public static class A{  3         static {  4             System.out.println("class A init");  5             try {  6                 TimeUnit.SECONDS.sleep(1);  7             } catch (InterruptedException e) {  8  e.printStackTrace();  9  } 10 B.test(); 11  } 12 
13         public static void test() { 14             System.out.println("aaa"); 15  } 16  } 17 
18     public static class B{ 19         static { 20             System.out.println("class B init"); 21 A.test(); 22  } 23 
24 
25         public static void test() { 26             System.out.println("bbb"); 27  } 28  } 29     public static void main(String[] args) { 30         new Thread(() -> A.test()).start(); 31         new Thread(() -> B.test()).start(); 32  } 33 }

 

這裏,問題的根本緣由,實際上是:windows

classloader在初始化一個類的時候,會對當前類加鎖,加鎖後,再執行類的靜態初始化塊。app

因此,上面會發生:less

一、線程1:類A對class A加鎖,加鎖後,執行類的靜態初始化塊(在堆棧裏體現爲<clinit>函數),發現用到了class B,因而去加載B;dom

二、線程2:類B對class B加鎖,加鎖後,執行類的靜態初始化塊(在堆棧裏體現爲<clinit>函數),發現用到了class A,因而去加載A;jvm

三、死鎖發生。async

 

有經驗的同窗,對於死鎖是毫無畏懼的,由於咱們有神器,jstack。 jstack 加上 -l 參數,便可打印出各個線程持有的鎖的信息。(windows上直接jconsole就行,還能死鎖檢測):

 

"Thread-1" #15 prio=5 os_prio=0 tid=0x000000002178a000 nid=0x2df8 in Object.wait() [0x0000000021f4e000] java.lang.Thread.State: RUNNABLE at com.dmtest.netty_learn.TestClassLoading$B.<clinit>(TestClassLoading.java:32) at com.dmtest.netty_learn.TestClassLoading.lambda$main$1(TestClassLoading.java:42) at com.dmtest.netty_learn.TestClassLoading$$Lambda$2/736709391.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - None "Thread-0" #14 prio=5 os_prio=0 tid=0x0000000021787800 nid=0x2618 in Object.wait() [0x00000000213be000] java.lang.Thread.State: RUNNABLE at com.dmtest.netty_learn.TestClassLoading$A.<clinit>(TestClassLoading.java:21) at com.dmtest.netty_learn.TestClassLoading.lambda$main$0(TestClassLoading.java:41) at com.dmtest.netty_learn.TestClassLoading$$Lambda$1/611437735.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - None

 

這裏,很奇怪的一個緣由是,明明這兩個線程發生了死鎖,爲何沒有顯示呢?

由於,這是 jvm 內部加了鎖,因此,jconsole、jstack都失效了。

 

3、一塊兒深刻JVM,探個究竟

一、單步跟蹤

class 的加載都是由 classloader 來完成的,並且部分工做是在 jvm 層面完成,咱們能夠看到,在 java.lang.ClassLoader#defineClass1 的定義中:

 

以上幾個方法都是本地方法。

其實際的實如今:/home/ckl/openjdk-jdk8u/jdk/src/share/native/java/lang/ClassLoader.c,

 

 1 JNIEXPORT jclass JNICALL  2 Java_java_lang_ClassLoader_defineClass1(JNIEnv *env,  3  jobject loader,  4  jstring name,  5  jbyteArray data,  6  jint offset,  7  jint length,  8  jobject pd,  9  jstring source) 10 { 11     jbyte *body; 12     char *utfName; 13     jclass result = 0; 14     char buf[128]; 15     char* utfSource; 16     char sourceBuf[1024]; 17 
18     if (data == NULL) { 19         JNU_ThrowNullPointerException(env, 0); 20         return 0; 21  } 22 
23     /* Work around 4153825. malloc crashes on Solaris when passed a 24  * negative size. 25      */
26     if (length < 0) { 27         JNU_ThrowArrayIndexOutOfBoundsException(env, 0); 28         return 0; 29  } 30 
31     body = (jbyte *)malloc(length); 32 
33     if (body == 0) { 34         JNU_ThrowOutOfMemoryError(env, 0); 35         return 0; 36  } 37 
38     (*env)->GetByteArrayRegion(env, data, offset, length, body); 39 
40     if ((*env)->ExceptionOccurred(env)) 41         goto free_body; 42 
43     if (name != NULL) { 44         utfName = getUTF(env, name, buf, sizeof(buf)); 45         if (utfName == NULL) { 46             goto free_body; 47  } 48  VerifyFixClassname(utfName); 49     } else { 50         utfName = NULL; 51  } 52 
53     if (source != NULL) { 54         utfSource = getUTF(env, source, sourceBuf, sizeof(sourceBuf)); 55         if (utfSource == NULL) { 56             goto free_utfName; 57  } 58     } else { 59         utfSource = NULL; 60  } 61 result = JVM_DefineClassWithSource(env, utfName, loader, body, length, pd, utfSource); 62 
63     if (utfSource && utfSource != sourceBuf) 64         free(utfSource); 65 
66  free_utfName: 67     if (utfName && utfName != buf) 68         free(utfName); 69 
70  free_body: 71     free(body); 72     return result; 73 }

 

你們能夠跟着標紅的代碼,咱們一塊兒大概看一下,這個方法的實如今/home/ckl/openjdk-jdk8u/hotspot/src/share/vm/prims/jvm.cpp 中,

1 JVM_ENTRY(jclass, JVM_DefineClassWithSource(JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize len, jobject pd, const char *source)) 2   JVMWrapper2("JVM_DefineClassWithSource %s", name); 3 
4 return jvm_define_class_common(env, name, loader, buf, len, pd, source, true, THREAD); 5 JVM_END

 

jvm_define_class_common 的實現,仍是在  jvm.cpp 中,

 1 // common code for JVM_DefineClass() and JVM_DefineClassWithSource()  2 // and JVM_DefineClassWithSourceCond()
 3 static jclass jvm_define_class_common(JNIEnv *env, const char *name,  4                                       jobject loader, const jbyte *buf,  5                                       jsize len, jobject pd, const char *source,  6  jboolean verify, TRAPS) {  7   if (source == NULL)  source = "__JVM_DefineClass__";  8 
 9   assert(THREAD->is_Java_thread(), "must be a JavaThread"); 10   JavaThread* jt = (JavaThread*) THREAD; 11 
12  PerfClassTraceTime vmtimer(ClassLoader::perf_define_appclass_time(), 13  ClassLoader::perf_define_appclass_selftime(), 14  ClassLoader::perf_define_appclasses(), 15                              jt->get_thread_stat()->perf_recursion_counts_addr(), 16                              jt->get_thread_stat()->perf_timers_addr(), 17  PerfClassTraceTime::DEFINE_CLASS); 18 
19   if (UsePerfData) { 20     ClassLoader::perf_app_classfile_bytes_read()->inc(len); 21  } 22 
23   // Since exceptions can be thrown, class initialization can take place 24   // if name is NULL no check for class name in .class stream has to be made.
25   TempNewSymbol class_name = NULL; 26   if (name != NULL) { 27     const int str_len = (int)strlen(name); 28     if (str_len > Symbol::max_length()) { 29       // It's impossible to create this class; the name cannot fit 30       // into the constant pool.
31  THROW_MSG_0(vmSymbols::java_lang_NoClassDefFoundError(), name); 32  } 33     class_name = SymbolTable::new_symbol(name, str_len, CHECK_NULL); 34  } 35 
36  ResourceMark rm(THREAD); 37   ClassFileStream st((u1*) buf, len, (char *)source); 38  Handle class_loader (THREAD, JNIHandles::resolve(loader)); 39   if (UsePerfData) { 40  is_lock_held_by_thread(class_loader, 41  ClassLoader::sync_JVMDefineClassLockFreeCounter(), 42  THREAD); 43  } 44  Handle protection_domain (THREAD, JNIHandles::resolve(pd)); 45 Klass* k = SystemDictionary::resolve_from_stream(class_name, class_loader, 46                                                      protection_domain, &st, 47                                                      verify != 0, 48  CHECK_NULL); 49 
50   if (TraceClassResolution && k != NULL) { 51  trace_class_resolution(k); 52  } 53 
54   return (jclass) JNIHandles::make_local(env, k->java_mirror()); 55 }

 

resolve_from_stream 的實如今 SystemDictionary 類中,下面咱們看下:

 1 Klass* SystemDictionary::resolve_from_stream(Symbol* class_name,  2  Handle class_loader,  3  Handle protection_domain,  4                                              ClassFileStream* st,  5                                              bool verify,  6  TRAPS) {  7 
 8   // Classloaders that support parallelism, e.g. bootstrap classloader,  9   // or all classloaders with UnsyncloadClass do not acquire lock here
10   bool DoObjectLock = true; 11   if (is_parallelCapable(class_loader)) { 12     DoObjectLock = false; 13  } 14 
15   ClassLoaderData* loader_data = register_loader(class_loader, CHECK_NULL); 16 
17   // Make sure we are synchronized on the class loader before we proceed
18 Handle lockObject = compute_loader_lock_object(class_loader, THREAD); 19 check_loader_lock_contention(lockObject, THREAD); 20 ObjectLocker ol(lockObject, THREAD, DoObjectLock); 21 22 TempNewSymbol parsed_name = NULL; 23 24 // Parse the stream. Note that we do this even though this klass might 25 // already be present in the SystemDictionary, otherwise we would not 26 // throw potential ClassFormatErrors. 27 // 28 // Note: "name" is updated. 29 30 instanceKlassHandle k = ClassFileParser(st).parseClassFile(class_name, 31 loader_data, 32 protection_domain, 33 parsed_name, 34 verify, 35 THREAD); 36 37 const char* pkg = "java/"; 38 size_t pkglen = strlen(pkg); 39 if (!HAS_PENDING_EXCEPTION && 40 !class_loader.is_null() && 41 parsed_name != NULL && 42 parsed_name->utf8_length() >= (int)pkglen && 43 !strncmp((const char*)parsed_name->bytes(), pkg, pkglen)) { 44 // It is illegal to define classes in the "java." package from 45 // JVM_DefineClass or jni_DefineClass unless you're the bootclassloader 46 ResourceMark rm(THREAD); 47 char* name = parsed_name->as_C_string(); 48 char* index = strrchr(name, '/'); 49 assert(index != NULL, "must be"); 50 *index = '\0'; // chop to just the package name 51 while ((index = strchr(name, '/')) != NULL) { 52 *index = '.'; // replace '/' with '.' in package name 53 } 54 const char* fmt = "Prohibited package name: %s"; 55 size_t len = strlen(fmt) + strlen(name); 56 char* message = NEW_RESOURCE_ARRAY(char, len); 57 jio_snprintf(message, len, fmt, name); 58 Exceptions::_throw_msg(THREAD_AND_LOCATION, 59 vmSymbols::java_lang_SecurityException(), message); 60 } 61 62 if (!HAS_PENDING_EXCEPTION) { 63 assert(parsed_name != NULL, "Sanity"); 64 assert(class_name == NULL || class_name == parsed_name, "name mismatch"); 65 // Verification prevents us from creating names with dots in them, this 66 // asserts that that's the case. 67 assert(is_internal_format(parsed_name), 68 "external class name format used internally"); 69 70 // Add class just loaded 71 // If a class loader supports parallel classloading handle parallel define requests 72 // find_or_define_instance_class may return a different InstanceKlass 73 if (is_parallelCapable(class_loader)) { 74 k = find_or_define_instance_class(class_name, class_loader, k, THREAD); 75 } else { 76 define_instance_class(k, THREAD); 77 } 78 } 79 96 97 return k(); 98 }

 

上面的方法裏,有幾處值得注意的:

1:18-20行,進行了加鎖,18行獲取鎖對象,這裏是當前類加載器(從註釋能夠看出),20行就是加鎖的語法

2:37-60行,這裏是判斷要加載的類的包名是否以 java 開頭,以 java 開頭的類是非法的,不能加載

3:第76行, define_instance_class(k, THREAD); 進行後續操做

 

接下來,咱們看看 define_instance_class 的實現:

 1 void SystemDictionary::define_instance_class(instanceKlassHandle k, TRAPS) {  2 
 3   ClassLoaderData* loader_data = k->class_loader_data();  4   Handle class_loader_h(THREAD, loader_data->class_loader());  5 
 6   for (uintx it = 0; it < GCExpandToAllocateDelayMillis; it++){}  7 
 8  // for bootstrap and other parallel classloaders don't acquire lock,  9  // use placeholder token 10  // If a parallelCapable class loader calls define_instance_class instead of 11  // find_or_define_instance_class to get here, we have a timing 12  // hole with systemDictionary updates and check_constraints
13  if (!class_loader_h.is_null() && !is_parallelCapable(class_loader_h)) { 14     assert(ObjectSynchronizer::current_thread_holds_lock((JavaThread*)THREAD, 15  compute_loader_lock_object(class_loader_h, THREAD)), 16          "define called without lock"); 17  } 18 
19   // Check class-loading constraints. Throw exception if violation is detected. 20   // Grabs and releases SystemDictionary_lock 21   // The check_constraints/find_class call and update_dictionary sequence 22   // must be "atomic" for a specific class/classloader pair so we never 23   // define two different instanceKlasses for that class/classloader pair. 24   // Existing classloaders will call define_instance_class with the 25   // classloader lock held 26   // Parallel classloaders will call find_or_define_instance_class 27   // which will require a token to perform the define class
28   Symbol*  name_h = k->name(); 29   unsigned int d_hash = dictionary()->compute_hash(name_h, loader_data); 30   int d_index = dictionary()->hash_to_index(d_hash); 31   check_constraints(d_index, d_hash, k, class_loader_h, true, CHECK); 32 
33   // Register class just loaded with class loader (placed in Vector) 34   // Note we do this before updating the dictionary, as this can 35   // fail with an OutOfMemoryError (if it does, we will *not* put this 36   // class in the dictionary and will not update the class hierarchy). 37   // JVMTI FollowReferences needs to find the classes this way.
38   if (k->class_loader() != NULL) { 39  methodHandle m(THREAD, Universe::loader_addClass_method()); 40  JavaValue result(T_VOID); 41  JavaCallArguments args(class_loader_h); 42     args.push_oop(Handle(THREAD, k->java_mirror())); 43     JavaCalls::call(&result, m, &args, CHECK); 44  } 45 
46   // Add the new class. We need recompile lock during update of CHA.
47  { 48     unsigned int p_hash = placeholders()->compute_hash(name_h, loader_data); 49     int p_index = placeholders()->hash_to_index(p_hash); 50 
51  MutexLocker mu_r(Compile_lock, THREAD); 52 
53     // Add to class hierarchy, initialize vtables, and do possible 54     // deoptimizations.
55     add_to_hierarchy(k, CHECK); // No exception, but can block 56 
57     // Add to systemDictionary - so other classes can see it. 58     // Grabs and releases SystemDictionary_lock
59  update_dictionary(d_index, d_hash, p_index, p_hash, 60  k, class_loader_h, THREAD); 61  } 62 k->eager_initialize(THREAD); 63 
64   // notify jvmti
65   if (JvmtiExport::should_post_class_load()) { 66       assert(THREAD->is_Java_thread(), "thread->is_Java_thread()"); 67       JvmtiExport::post_class_load((JavaThread *) THREAD, k()); 68 
69  } 70 
71 }

 

 這裏,因爲咱們的案例中,是class A 在初始化過程當中出現死鎖,因此咱們關注第62行,eager_initialize:

 

 1 void InstanceKlass::eager_initialize(Thread *thread) {  2   if (!EagerInitialization) return;  3 
 4   if (this->is_not_initialized()) {  5     // abort if the the class has a class initializer
 6     if (this->class_initializer() != NULL) return;  7 
 8     // abort if it is java.lang.Object (initialization is handled in genesis)
 9     Klass* super = this->super(); 10     if (super == NULL) return; 11 
12     // abort if the super class should be initialized
13     if (!InstanceKlass::cast(super)->is_initialized()) return; 14 
15     // call body to expose the this pointer
16     instanceKlassHandle this_oop(thread, this); 17 eager_initialize_impl(this_oop); 18  } 19 }

 

咱們接着進入 eager_initialize_impl,該方法進入到了 InstanceKlass:

 1 void InstanceKlass::eager_initialize_impl(instanceKlassHandle this_oop) {  2  EXCEPTION_MARK;  3 oop init_lock = this_oop->init_lock(); 4 ObjectLocker ol(init_lock, THREAD, init_lock != NULL);  5 
 6   // abort if someone beat us to the initialization
 7   if (!this_oop->is_not_initialized()) return;  // note: not equivalent to is_initialized()
 8 
 9   ClassState old_state = this_oop->init_state(); 10   link_class_impl(this_oop, true, THREAD); 11   if (HAS_PENDING_EXCEPTION) { 12  CLEAR_PENDING_EXCEPTION; 13     // Abort if linking the class throws an exception. 14 
15     // Use a test to avoid redundantly resetting the state if there's 16     // no change. Set_init_state() asserts that state changes make 17     // progress, whereas here we might just be spinning in place.
18     if( old_state != this_oop->_init_state ) 19       this_oop->set_init_state (old_state); 20   } else { 21     // linking successfull, mark class as initialized
22     this_oop->set_init_state (fully_initialized); 23     this_oop->fence_and_clear_init_lock(); 24     // trace
25     if (TraceClassInitialization) { 26  ResourceMark rm(THREAD); 27       tty->print_cr("[Initialized %s without side effects]", this_oop->external_name()); 28  } 29  } 30 }

 

這裏,咱們重點關注第3,4行:

一、第3行,獲取初始化鎖;

二、第4行,加鎖

 

二、獲取初始化鎖並加鎖

這裏,咱們首先獲取鎖的操做,

1 oop InstanceKlass::init_lock() const { 2   // return the init lock from the mirror
3   oop lock = java_lang_Class::init_lock(java_mirror()); 4   // Prevent reordering with any access of initialization state
5  OrderAccess::loadload(); 6   assert((oop)lock != NULL || !is_not_initialized(), // initialized or in_error state
7          "only fully initialized state can have a null lock"); 8   return lock; 9 }

 

其中,java_mirror() 方法就是返回 Klass 類中的如下字段:

1   // java/lang/Class instance mirroring this class
2   oop       _java_mirror;

 

再看 init_lock 方法:

1 oop java_lang_Class::init_lock(oop java_class) { 2   assert(_init_lock_offset != 0, "must be set"); 3   return java_class->obj_field(_init_lock_offset); 4 }

 

這裏呢,應該就是獲取 咱們傳入的 java_class 中的某個字段,該字段就是充當 init_lock。(我的水平有限,還請指正)

 

下面爲加鎖操做的語句:

1   ObjectLocker ol(init_lock, THREAD, init_lock != NULL);

 

 1 / ObjectLocker enforced balanced locking and can never thrown an  2 // IllegalMonitorStateException. However, a pending exception may  3 // have to pass through, and we must also be able to deal with  4 // asynchronous exceptions. The caller is responsible for checking  5 // the threads pending exception if needed.  6 // doLock was added to support classloading with UnsyncloadClass which  7 // requires flag based choice of locking the classloader lock.
 8 class ObjectLocker : public StackObj {  9  private: 10   Thread* _thread; 11  Handle _obj; 12  BasicLock _lock; 13   bool      _dolock;   // default true
14  public: 15 ObjectLocker(Handle obj, Thread* thread, bool doLock = true);

 

 1 // -----------------------------------------------------------------------------  2 // Internal VM locks on java objects  3 // standard constructor, allows locking failures
 4 ObjectLocker::ObjectLocker(Handle obj, Thread* thread, bool doLock) {  5   _dolock = doLock;  6   _thread = thread;  8   _obj = obj;  9 
10 if (_dolock) { 11  TEVENT (ObjectLocker) ; 12 
13     ObjectSynchronizer::fast_enter(_obj, &_lock, false, _thread); 14  } 15 }

 

接下來會進入到 synchronizer.cpp,

 1 // -----------------------------------------------------------------------------  2 // Fast Monitor Enter/Exit  3 // This the fast monitor enter. The interpreter and compiler use  4 // some assembly copies of this code. Make sure update those code  5 // if the following function is changed. The implementation is  6 // extremely sensitive to race condition. Be careful.
 7 
 8 void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {  9  if (UseBiasedLocking) { 10     if (!SafepointSynchronize::is_at_safepoint()) { 11       BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD); 12       if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) { 13         return; 14  } 15     } else { 16       assert(!attempt_rebias, "can not rebias toward VM thread"); 17  BiasedLocking::revoke_at_safepoint(obj); 18  } 19     assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); 20  } 21 
22 slow_enter (obj, lock, THREAD) ; 23 }

 

上面會判斷,是否使用偏向鎖,若是不使用,則走 slow_enter 。

 1 // -----------------------------------------------------------------------------  2 // Interpreter/Compiler Slow Case  3 // This routine is used to handle interpreter/compiler slow case  4 // We don't need to use fast path here, because it must have been  5 // failed in the interpreter/compiler code.
 6 void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {  7   markOop mark = obj->mark();  8   assert(!mark->has_bias_pattern(), "should not see bias pattern here");  9 
10   if (mark->is_neutral()) { 11     // Anticipate successful CAS -- the ST of the displaced mark must 12     // be visible <= the ST performed by the CAS.
13     lock->set_displaced_header(mark); 14     if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { 15  TEVENT (slow_enter: release stacklock) ; 16       return ; 17  } 18     // Fall through to inflate() ...
19   } else
20   if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { 21     assert(lock != mark->locker(), "must not re-lock the same lock"); 22     assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock"); 23     lock->set_displaced_header(NULL); 24     return; 25  } 26 
34 
35   // The object header will never be displaced to this lock, 36   // so it does not matter what the value is, except that it 37   // must be non-zero to avoid looking like a re-entrant lock, 38   // and must not look locked either.
39   lock->set_displaced_header(markOopDesc::unused_mark()); 40   ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD); 41 }

 

這裏的代碼結合註釋,能大概看出來是,前面部分爲輕量級鎖,這裏先不展開了,鎖這塊均可以單獨寫了。有興趣的讀者能夠自行閱讀。

 

 

4、總結

這裏再說下結論吧,類初始化的過程,會對class加鎖,再執行class的初始化,若是這時候發生了循環依賴,就會致使死鎖。

 

若是有讀者對上面的c++代碼感興趣,能夠參考下面的文章,搭建調試環境:

源碼編譯OpenJdk 8,Netbeans調試Java原子類在JVM中的實現(Ubuntu 16.04)

相關文章
相關標籤/搜索