垃圾收集器与内存分配策略

程序计数器、虚拟机栈、本地方法栈三个区域随着线程的创建而创建、执行完成销毁,栈中的栈帧随着放大的进入和退出执行入栈与出栈,每个栈帧分配多少内存基本上是在类结构确定下来时已知,因此这几个区域的内存分配与回收都具备确定性。
Java堆中存放的所有对象的实例,只有在程序运行期间我们才会知道会创建哪些对象,这部分内存分配与回收都是动态的,垃圾收集器重点关注的就是这部分。

引入计数算数
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的的对象就是不可能再被使用的。
缺点:它很难解决对象之间的相互循环引用的问题。

VM ages:-XX:+PrintGCDetails 打印GC详细信息:

package memory;  public class ReferenceCountingGC {      public Object instance = null;      private static  final  int _1MB = 1024 *1024;      private byte[] bigSize = new byte[2 *_1MB];      public static  void  main(String[] arg){         ReferenceCountingGC objA = new ReferenceCountingGC();         ReferenceCountingGC objB = new ReferenceCountingGC();         objA.instance = objB;         objB.instance = objA;          System.gc();     } }

日志:

[15.841s][info   ][gc,start    ] GC(0) Pause Full (System.gc()) [15.842s][info   ][gc,task     ] GC(0) Using 3 workers of 8 for full compaction [15.843s][info   ][gc,phases,start] GC(0) Phase 1: Mark live objects [15.847s][info   ][gc,phases      ] GC(0) Phase 1: Mark live objects 4.412ms [15.848s][info   ][gc,phases,start] GC(0) Phase 2: Prepare for compaction [15.849s][info   ][gc,phases      ] GC(0) Phase 2: Prepare for compaction 1.244ms [15.849s][info   ][gc,phases,start] GC(0) Phase 3: Adjust pointers [15.851s][info   ][gc,phases      ] GC(0) Phase 3: Adjust pointers 1.951ms [15.851s][info   ][gc,phases,start] GC(0) Phase 4: Compact heap [15.853s][info   ][gc,phases      ] GC(0) Phase 4: Compact heap 1.987ms [15.857s][info   ][gc,heap        ] GC(0) Eden regions: 4->0(9) [15.857s][info   ][gc,heap        ] GC(0) Survivor regions: 0->0(0) [15.857s][info   ][gc,heap        ] GC(0) Old regions: 0->3 [15.857s][info   ][gc,heap        ] GC(0) Archive regions: 0->0 [15.857s][info   ][gc,heap        ] GC(0) Humongous regions: 6->6 [15.857s][info   ][gc,metaspace   ] GC(0) Metaspace: 424K(640K)->424K(640K) NonClass: 399K(512K)->399K(512K) Class: 24K(128K)->24K(128K) [15.857s][info   ][gc             ] GC(0) Pause Full (System.gc()) 9M->6M(30M) 15.980ms [15.857s][info   ][gc,cpu         ] GC(0) User=0.02s Sys=0.02s Real=0.02s [17.684s][info   ][gc,heap,exit   ] Heap [17.684s][info   ][gc,heap,exit   ]  garbage-first heap   total 30720K, used 7088K [0x0000000081800000, 0x0000000100000000) [17.684s][info   ][gc,heap,exit   ]   region size 1024K, 1 young (1024K), 0 survivors (0K) [17.684s][info   ][gc,heap,exit   ]  Metaspace       used 425K, committed 640K, reserved 1114112K [17.684s][info   ][gc,heap,exit   ]   class space    used 24K, committed 128K, reserved 1048576K

从日志看出,内存进行了回收,说明JVM 的GC使用的不是引用计数算法。

根搜索算法
通过一系列的名为 “GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象的GC Roots 没有任何引用链相连时,则证明此对象是不可用的。

引用
引用分为:

  • 强引用(Strong Reference) :只要强引用还在,垃圾收集器永远不会回收掉引用的对象。
  • 软引用(Soft Reference):在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用(Weak Reference):被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用(Phantom Reference)(幽灵引用、幻影引用):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。

在跟搜索算法中不可达的对象,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与GC Roots相连的引用链,那它将会被第一次标记并进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况视为“没有必要执行”。
如果对象有必要执行finalize()方法,那么这个对象将被放置在一个F-Queue的队列之中由Finalizer线程(虚拟机建立并出发)执行。finalizer用于告诉垃圾回收器下一步应该执行的操作。然后,GC将对F-Queue中的对象进行二次小规模的标记。

package memory;  public class FinalizeEscapeGC {      public static  FinalizeEscapeGC SAVE_HOOK = null;      public  void  isAlive(){         System.out.println("yes, i am still alive;");     }     @Override     protected void finalize() throws Throwable{         super.finalize();         System.out.println("fialize mehtod executed!");         //重新引用         FinalizeEscapeGC.SAVE_HOOK = this;     }      public static void main(String[] args) throws Throwable{         SAVE_HOOK = new FinalizeEscapeGC();         //对象第一次成功拯救自己         SAVE_HOOK = null;         System.gc();         //因为Finalizer方法优先级很低,暂停500毫秒,等它执行         Thread.sleep(500);          if(SAVE_HOOK != null){             SAVE_HOOK.isAlive();         } else {             System.out.println("no ,i am dead");         }         //下面这段代码与上面的完全相同,但是这次自救失败~         SAVE_HOOK = null;          System.gc();          Thread.sleep(500);          if(SAVE_HOOK != null){             SAVE_HOOK.isAlive();         } else {             System.out.println("no ,i am dead !!!");         }     } }

运行结果:

fialize mehtod executed! yes, i am still alive; no ,i am dead !!!

任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,他的finalize()方法不会被再次执行。
finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及时。

回收方法区

  • 回收的主要内容为:废弃常量和无用的类
  • 废弃常量:没有任何对象引用常量池中的常量,也没有其他地方引用这个字面量。
  • 无用类判定条件:
  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集算法

标记-清除算法
首先标记出所有需要回收的对象,在标记完成后同意回收掉所有被标记的对象。
缺点:

  • 一个是效率问题,标记和清除过程的效率都不高;
  • 另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致程序在需要分配较大对象时无法找到连续的内存,而不得不提前触发另一次垃圾收集动作。

复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完后,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
缺点:将内存缩小为原来的一半。
现在商业虚拟机都采用这种收机算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
这里有个问题,无法保证回收后存活对象一块Survivor空间够用,所以这里需要依赖其他内存(老年代)进行分配担保。

标记-整理算法
根据老年代的特点,对存活对象进行标记,让所有存活对象向一端移动,然后直接清理掉端边以外的内存。

分代收机算法
根据对象吨货周期的不同将内存划分为几块,JAVA一般将堆分为新生代和老生代,这样就可以根据各个年代的特点采用最适当的收集算法。

垃圾收集器
Serial收集器
是最基本、历史最悠久的收集器,是一个单线程收集器,在进行垃圾收集的时候,会暂停掉其他的所有工作线程。(简单而高效)

ParNew收集器
Serial收集器的多线程版,除了使用多线程进行垃圾收集外,其余的与Serial收集器一致。关注点是尽可能缩短用户线程停顿时间。

Parallel Scavenge 收集器
新生代收集器,他也是使用复制算法的收集器,也是并行的多线程收集器。目标是达到一个可控制的吞吐量。自适应调节策略。
吞吐量 = 运行用户代码的时间/(运行用户代码时间+垃圾收集时间)
停顿时间越短就越需要与用户交互的程序,良好的响应速度能提升用户体验;高吞吐量则可以更高效率地利用CPU时间,尽快完成程序的运算任务,主要适用于后台运算而不需要太多交互的任务。

Serial Old收集器
Serial Old是Serial收集器的老年代版本,使用“标记-整理”算法。

Parallel Old收集器
是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

CMS收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。使用“标记-清除”算法实现。
步骤:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除
  • 其中初始标记、重新标记两个步骤需要用户工作线程暂停,初始标记只标记GC Roots能直接关联到的对象,并发标记阶段进行 GC Root Tracing 过程,重新标记阶段则是为修正并发标记期间,用户程序继续运行而导致标记变化产生变动的那部分对象的标记记录。
  • 缺点:
  • CMS收集器对CPU资源非常敏感。CMS默认用的回收线程数(CPU数量+3)/4。为解决该问题,虚拟机提供了一种i-CMS(增量式并发收集器)的CMS收集器变种,工作方式就是在并发标记和并发清理的时候让GC线程与用户线程交替运行,尽量减少GC线程独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得少一些。
  • CMS收集器无法处理浮动垃圾。如果CMS运行期预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时候虚拟机将启动后备元:临时启动Serial Old 收集器重新进行老年代的垃圾收集。
  • CMS是一款基于“标记-清除”算法实现的收集器,收集结束时会产生大量的空间碎片。

G1 收集器

  • G1收集器是基于“标记-整理”算法实现的收集器,也就是说它不会产生空间碎片,这对于长时间运行的应用系统来说非常重要。
  • 它可以非常精准地控制停顿,既能让使用者明确指定在一个长度为M毫秒的时间片段,消耗在垃圾收集上的时间不得超过N毫秒。

G1将正好Java堆(包括新生代、老年代)划分为多个大小固定的独立区域,并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,有限回抽垃圾最多的区域。

内存分配和回收策略

对象优先在Eden分配

多数情况下,对象在新生代Eden区中扽配,当Eden区域没有足够的空间分配时,虚拟机将发起一次Minor GC。

package memory;  public class JavaEdenTest {      private static  final  int _1MB = 1024 *1024;      public  static  void  testAllocation(){         byte[] allocation1,allocation2,allocation3,allocation4;         allocation1 = new byte[2 *_1MB];         allocation2 = new byte[2 * _1MB];         allocation3 = new  byte[2 *_1MB];         allocation4 = new  byte[4 *_1MB];     }      /**      * -verbose:gc      * -Xms20M -Xmx20M -Xmn10M   显示JAVA堆的大小20M且不可扩展      * 其中10M分配给新生代,剩余10M分配给老年代      * -XX:SurvivorRatio=8 确定新生代中的Eden区与一个Survivor区域的空间比例为8:1      * -XX:+PrintGCDetails 打印详细的收集器日志参数      *      * @param args      */     public  static  void  main(String[] args){         testAllocation();     } }

运行日志:

[16.630s][info   ][gc,start    ] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) [16.631s][info   ][gc,task     ] GC(0) Using 2 workers of 8 for evacuation [16.639s][info   ][gc,phases   ] GC(0)   Pre Evacuate Collection Set: 0.3ms [16.639s][info   ][gc,phases   ] GC(0)   Merge Heap Roots: 0.1ms [16.639s][info   ][gc,phases   ] GC(0)   Evacuate Collection Set: 4.8ms [16.639s][info   ][gc,phases   ] GC(0)   Post Evacuate Collection Set: 2.1ms [16.639s][info   ][gc,phases   ] GC(0)   Other: 0.8ms [16.639s][info   ][gc,heap     ] GC(0) Eden regions: 3->0(9) [16.639s][info   ][gc,heap     ] GC(0) Survivor regions: 0->1(2) [16.639s][info   ][gc,heap     ] GC(0) Old regions: 0->0 [16.639s][info   ][gc,heap     ] GC(0) Archive regions: 0->0 [16.639s][info   ][gc,heap     ] GC(0) Humongous regions: 9->9 [16.639s][info   ][gc,metaspace] GC(0) Metaspace: 419K(576K)->419K(576K) NonClass: 394K(448K)->394K(448K) Class: 24K(128K)->24K(128K) [16.639s][info   ][gc          ] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 11M->10M(20M) 9.150ms [16.639s][info   ][gc,cpu      ] GC(0) User=0.00s Sys=0.00s Real=0.01s [16.640s][info   ][gc          ] GC(1) Concurrent Undo Cycle [16.640s][info   ][gc,marking  ] GC(1) Concurrent Cleanup for Next Mark [16.640s][info   ][gc,marking  ] GC(1) Concurrent Cleanup for Next Mark 0.402ms [16.640s][info   ][gc          ] GC(1) Concurrent Undo Cycle 0.770ms

allocation4在实例化时,出发了一次Minor GC。

大对象直接进入老年代
VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:PretenureSizeThreshold=3145728
-XX:PretenureSizeThreshold的作用是令大于这个设置值的对象直接进入老年代中分配。
避免在Eden区及两个Survivor区之间发生大量的内存拷贝。

package memory;  public class JavaTestBigObjectGC {      private static  final int _1MB = 1024 * 1024;      public  static  void  main(String[] args){         byte[] allocation;         allocation = new byte[4 * _1MB];     } }

运行日志:

[5.942s][info   ][gc,heap,exit] Heap [5.942s][info   ][gc,heap,exit]  garbage-first heap   total 20480K, used 7586K [0x00000000fec00000, 0x0000000100000000) [5.942s][info   ][gc,heap,exit]   region size 1024K, 3 young (3072K), 0 survivors (0K) [5.942s][info   ][gc,heap,exit]  Metaspace       used 419K, committed 576K, reserved 1114112K [5.942s][info   ][gc,heap,exit]   class space    used 24K, committed 128K, reserved 1048576K

长期存活对象进入老年代
虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden中生成,没经过一次Minor GC后仍然存活,并且能被Survivor容纳的话,将会被移动到Survivor空间中,并将对象年龄设置为1,当年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。
对象寄生老年的年龄阈值,可以使用-XX:MaxTenuringThreshold来设置。

package memory;  public class JavaTestTenuringThreshold {      private static  final int _1MB = 1024 *1024;      /**      * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8      * -XX:MaxTenuringThreshold=1      */     public  static  void  testTenuringThreshold(){         byte[] allocation1,allocation2,allocation3;         allocation1 = new byte[4 * _1MB];         allocation2 = new byte[4 * _1MB];         allocation3 = new byte[4* _1MB];         allocation3 = null;         allocation3 = new byte[4*_1MB];      }      public  static  void  main(String[] args){         testTenuringThreshold();     } }

[6.680s][info   ][gc,start    ] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) [6.680s][info   ][gc,task     ] GC(0) Using 2 workers of 8 for evacuation [6.688s][info   ][gc,phases   ] GC(0)   Pre Evacuate Collection Set: 0.4ms [6.688s][info   ][gc,phases   ] GC(0)   Merge Heap Roots: 0.2ms [6.688s][info   ][gc,phases   ] GC(0)   Evacuate Collection Set: 4.8ms [6.688s][info   ][gc,phases   ] GC(0)   Post Evacuate Collection Set: 1.8ms [6.688s][info   ][gc,phases   ] GC(0)   Other: 0.7ms [6.688s][info   ][gc,heap     ] GC(0) Eden regions: 3->0(9) [6.688s][info   ][gc,heap     ] GC(0) Survivor regions: 0->1(2) [6.688s][info   ][gc,heap     ] GC(0) Old regions: 0->0 [6.688s][info   ][gc,heap     ] GC(0) Archive regions: 0->0 [6.688s][info   ][gc,heap     ] GC(0) Humongous regions: 5->5 [6.688s][info   ][gc,metaspace] GC(0) Metaspace: 423K(640K)->423K(640K) NonClass: 399K(512K)->399K(512K) Class: 24K(128K)->24K(128K) [6.688s][info   ][gc          ] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation) 7M->6M(20M) 8.803ms [6.688s][info   ][gc,cpu      ] GC(0) User=0.02s Sys=0.02s Real=0.01s [6.689s][info   ][gc          ] GC(1) Concurrent Undo Cycle [6.689s][info   ][gc,marking  ] GC(1) Concurrent Cleanup for Next Mark [6.689s][info   ][gc,marking  ] GC(1) Concurrent Cleanup for Next Mark 0.187ms [6.689s][info   ][gc          ] GC(1) Concurrent Undo Cycle 0.494ms [6.696s][info   ][gc,start    ] GC(2) Pause Young (Concurrent Start) (G1 Humongous Allocation) [6.696s][info   ][gc,task     ] GC(2) Using 2 workers of 8 for evacuation [6.699s][info   ][gc,phases   ] GC(2)   Pre Evacuate Collection Set: 0.2ms [6.699s][info   ][gc,phases   ] GC(2)   Merge Heap Roots: 0.1ms [6.699s][info   ][gc,phases   ] GC(2)   Evacuate Collection Set: 1.9ms [6.699s][info   ][gc,phases   ] GC(2)   Post Evacuate Collection Set: 0.5ms [6.699s][info   ][gc,phases   ] GC(2)   Other: 0.1ms [6.699s][info   ][gc,heap     ] GC(2) Eden regions: 0->0(10) [6.699s][info   ][gc,heap     ] GC(2) Survivor regions: 1->0(2) [6.699s][info   ][gc,heap     ] GC(2) Old regions: 0->1 [6.699s][info   ][gc,heap     ] GC(2) Archive regions: 0->0 [6.699s][info   ][gc,heap     ] GC(2) Humongous regions: 10->10 [6.699s][info   ][gc,metaspace] GC(2) Metaspace: 424K(640K)->424K(640K) NonClass: 399K(512K)->399K(512K) Class: 24K(128K)->24K(128K) [6.699s][info   ][gc          ] GC(2) Pause Young (Concurrent Start) (G1 Humongous Allocation) 11M->10M(20M) 3.165ms [6.699s][info   ][gc,cpu      ] GC(2) User=0.02s Sys=0.02s Real=0.00s [6.699s][info   ][gc          ] GC(3) Concurrent Mark Cycle [6.699s][info   ][gc,marking  ] GC(3) Concurrent Clear Claimed Marks [6.699s][info   ][gc,marking  ] GC(3) Concurrent Clear Claimed Marks 0.011ms [6.699s][info   ][gc,marking  ] GC(3) Concurrent Scan Root Regions [6.700s][info   ][gc,marking  ] GC(3) Concurrent Scan Root Regions 0.900ms [6.700s][info   ][gc,marking  ] GC(3) Concurrent Mark [6.700s][info   ][gc,marking  ] GC(3) Concurrent Mark From Roots [6.700s][info   ][gc,task     ] GC(3) Using 2 workers of 2 for marking [6.701s][info   ][gc,marking  ] GC(3) Concurrent Mark From Roots 0.666ms [6.701s][info   ][gc,marking  ] GC(3) Concurrent Preclean [6.701s][info   ][gc,marking  ] GC(3) Concurrent Preclean 0.013ms [6.715s][info   ][gc,start    ] GC(3) Pause Remark [6.716s][info   ][gc          ] GC(3) Pause Remark 15M->15M(20M) 0.810ms [6.716s][info   ][gc,cpu      ] GC(3) User=0.00s Sys=0.00s Real=0.00s [6.716s][info   ][gc,marking  ] GC(3) Concurrent Mark 16.267ms [6.716s][info   ][gc,marking  ] GC(3) Concurrent Rebuild Remembered Sets [6.716s][info   ][gc,marking  ] GC(3) Concurrent Rebuild Remembered Sets 0.007ms [6.717s][info   ][gc,start    ] GC(3) Pause Cleanup [6.717s][info   ][gc          ] GC(3) Pause Cleanup 15M->15M(20M) 0.019ms [6.717s][info   ][gc,cpu      ] GC(3) User=0.00s Sys=0.00s Real=0.00s [6.717s][info   ][gc,marking  ] GC(3) Concurrent Cleanup for Next Mark [6.717s][info   ][gc,start    ] GC(4) Pause Young (Normal) (G1 Humongous Allocation) [6.717s][info   ][gc,task     ] GC(4) Using 2 workers of 8 for evacuation [6.718s][info   ][gc,phases   ] GC(4)   Pre Evacuate Collection Set: 0.1ms [6.718s][info   ][gc,phases   ] GC(4)   Merge Heap Roots: 0.1ms [6.718s][info   ][gc,phases   ] GC(4)   Evacuate Collection Set: 0.1ms [6.718s][info   ][gc,phases   ] GC(4)   Post Evacuate Collection Set: 0.4ms [6.718s][info   ][gc,phases   ] GC(4)   Other: 0.0ms [6.718s][info   ][gc,heap     ] GC(4) Eden regions: 0->0(10) [6.718s][info   ][gc,heap     ] GC(4) Survivor regions: 0->0(2) [6.718s][info   ][gc,heap     ] GC(4) Old regions: 1->1 [6.718s][info   ][gc,heap     ] GC(4) Archive regions: 0->0 [6.718s][info   ][gc,heap     ] GC(4) Humongous regions: 15->10 [6.718s][info   ][gc,metaspace] GC(4) Metaspace: 424K(640K)->424K(640K) NonClass: 399K(512K)->399K(512K) Class: 24K(128K)->24K(128K) [6.718s][info   ][gc          ] GC(4) Pause Young (Normal) (G1 Humongous Allocation) 15M->10M(20M) 0.861ms [6.718s][info   ][gc,cpu      ] GC(4) User=0.00s Sys=0.00s Real=0.00s [6.718s][info   ][gc,marking  ] GC(3) Concurrent Cleanup for Next Mark 1.440ms [6.718s][info   ][gc          ] GC(3) Concurrent Mark Cycle 19.012ms

动态对象年龄判定
为了能更好地适应不同程序的内存情况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

空间担保
在发生Minor GC时,虚拟机会在检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次 Full GC。


垃圾收集器与内存分配策略

 

发表评论

相关文章