JVM字节码(四)

之前分析的MyTest1程序是比较简单的程序,接来下我们将再用一个程序来巩固一下对JVM字节码的理解:

package com.leolin.jvm.bytecode;

public class MyTest2 {
    String str = "Welcome";
    private int x = 5;
    public static Integer in = 10;
    private Object obj = new Object();

    public MyTest2() {
    }

    public MyTest2(int a) {
    }


    public static void main(String[] args) {
        MyTest2 myTest2 = new MyTest2();
        myTest2.setX(8);
        in = 20;
    }

    private synchronized void setX(int x) {
        this.x = x;
    }

    private void test(String str) {
        synchronized (obj) {
            System.out.println("str:" + str);
        }
    }

    static {
        System.out.println("hello");
    }
}

  

上面的代码如果从Java语言层面来看,是比较简单的,但是经过编译之后,对应的字节码文件,就不一定了。这里我们编译后再用javap -verbose -p来看看反编译之后的方法,这里之所比相比以前加上一个-p,是因为如果单单使用-verbose不会打印我们另外两个私有方法。反编译结果如下:

D:\F\java_space\jvm-lecture\target\classes>javap -verbose -p com.leolin.jvm.bytecode.MyTest2
Classfile /D:/F/java_space/jvm-lecture/target/classes/com/leolin/jvm/bytecode/MyTest2.class
  Last modified 2020-5-14; size 1578 bytes
  MD5 checksum e247d6b8aea41e469b1ebcd282f8eabb
  Compiled from "MyTest2.java"
public class com.leolin.jvm.bytecode.MyTest2
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#53         // java/lang/Object."<init>":()V
   #2 = String             #54            // Welcome
   #3 = Fieldref           #7.#55         // com/leolin/jvm/bytecode/MyTest2.str:Ljava/lang/String;
   #4 = Fieldref           #7.#56         // com/leolin/jvm/bytecode/MyTest2.x:I
   #5 = Class              #57            // java/lang/Object
   #6 = Fieldref           #7.#58         // com/leolin/jvm/bytecode/MyTest2.obj:Ljava/lang/Object;
   #7 = Class              #59            // com/leolin/jvm/bytecode/MyTest2
   #8 = Methodref          #7.#53         // com/leolin/jvm/bytecode/MyTest2."<init>":()V
   #9 = Methodref          #7.#60         // com/leolin/jvm/bytecode/MyTest2.setX:(I)V
  #10 = Methodref          #61.#62        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  #11 = Fieldref           #7.#63         // com/leolin/jvm/bytecode/MyTest2.in:Ljava/lang/Integer;
  #12 = Fieldref           #64.#65        // java/lang/System.out:Ljava/io/PrintStream;
  #13 = Class              #66            // java/lang/StringBuilder
  #14 = Methodref          #13.#53        // java/lang/StringBuilder."<init>":()V
  #15 = String             #67            // str:
  #16 = Methodref          #13.#68        // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #17 = Methodref          #13.#69        // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #18 = Methodref          #70.#71        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #19 = String             #72            // hello
  #20 = Utf8               str
  #21 = Utf8               Ljava/lang/String;
  #22 = Utf8               x
  #23 = Utf8               I
  #24 = Utf8               in
  #25 = Utf8               Ljava/lang/Integer;
  #26 = Utf8               obj
  #27 = Utf8               Ljava/lang/Object;
  #28 = Utf8               <init>
  #29 = Utf8               ()V
  #30 = Utf8               Code
  #31 = Utf8               LineNumberTable
  #32 = Utf8               LocalVariableTable
  #33 = Utf8               this
  #34 = Utf8               Lcom/leolin/jvm/bytecode/MyTest2;
  #35 = Utf8               (I)V
  #36 = Utf8               a
  #37 = Utf8               main
  #38 = Utf8               ([Ljava/lang/String;)V
  #39 = Utf8               args
  #40 = Utf8               [Ljava/lang/String;
  #41 = Utf8               myTest2
  #42 = Utf8               setX
  #43 = Utf8               test
  #44 = Utf8               (Ljava/lang/String;)V
  #45 = Utf8               StackMapTable
  #46 = Class              #59            // com/leolin/jvm/bytecode/MyTest2
  #47 = Class              #73            // java/lang/String
  #48 = Class              #57            // java/lang/Object
  #49 = Class              #74            // java/lang/Throwable
  #50 = Utf8               <clinit>
  #51 = Utf8               SourceFile
  #52 = Utf8               MyTest2.java
  #53 = NameAndType        #28:#29        // "<init>":()V
  #54 = Utf8               Welcome
  #55 = NameAndType        #20:#21        // str:Ljava/lang/String;
  #56 = NameAndType        #22:#23        // x:I
  #57 = Utf8               java/lang/Object
  #58 = NameAndType        #26:#27        // obj:Ljava/lang/Object;
  #59 = Utf8               com/leolin/jvm/bytecode/MyTest2
  #60 = NameAndType        #42:#35        // setX:(I)V
  #61 = Class              #75            // java/lang/Integer
  #62 = NameAndType        #76:#77        // valueOf:(I)Ljava/lang/Integer;
  #63 = NameAndType        #24:#25        // in:Ljava/lang/Integer;
  #64 = Class              #78            // java/lang/System
  #65 = NameAndType        #79:#80        // out:Ljava/io/PrintStream;
  #66 = Utf8               java/lang/StringBuilder
  #67 = Utf8               str:
  #68 = NameAndType        #81:#82        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #69 = NameAndType        #83:#84        // toString:()Ljava/lang/String;
  #70 = Class              #85            // java/io/PrintStream
  #71 = NameAndType        #86:#44        // println:(Ljava/lang/String;)V
  #72 = Utf8               hello
  #73 = Utf8               java/lang/String
  #74 = Utf8               java/lang/Throwable
  #75 = Utf8               java/lang/Integer
  #76 = Utf8               valueOf
  #77 = Utf8               (I)Ljava/lang/Integer;
  #78 = Utf8               java/lang/System
  #79 = Utf8               out
  #80 = Utf8               Ljava/io/PrintStream;
  #81 = Utf8               append
  #82 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #83 = Utf8               toString
  #84 = Utf8               ()Ljava/lang/String;
  #85 = Utf8               java/io/PrintStream
  #86 = Utf8               println
{
  java.lang.String str;
    descriptor: Ljava/lang/String;
    flags:

  private int x;
    descriptor: I
    flags: ACC_PRIVATE

  public static java.lang.Integer in;
    descriptor: Ljava/lang/Integer;
    flags: ACC_PUBLIC, ACC_STATIC

  private java.lang.Object obj;
    descriptor: Ljava/lang/Object;
    flags: ACC_PRIVATE

  public com.leolin.jvm.bytecode.MyTest2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String Welcome
         7: putfield      #3                  // Field str:Ljava/lang/String;
        10: aload_0
        11: iconst_5
        12: putfield      #4                  // Field x:I
        15: aload_0
        16: new           #5                  // class java/lang/Object
        19: dup
        20: invokespecial #1                  // Method java/lang/Object."<init>":()V
        23: putfield      #6                  // Field obj:Ljava/lang/Object;
        26: return
      LineNumberTable:
        line 9: 0
        line 4: 4
        line 5: 10
        line 7: 15
        line 10: 26
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      27     0  this   Lcom/leolin/jvm/bytecode/MyTest2;

  public com.leolin.jvm.bytecode.MyTest2(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=2
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String Welcome
         7: putfield      #3                  // Field str:Ljava/lang/String;
        10: aload_0
        11: iconst_5
        12: putfield      #4                  // Field x:I
        15: aload_0
        16: new           #5                  // class java/lang/Object
        19: dup
        20: invokespecial #1                  // Method java/lang/Object."<init>":()V
        23: putfield      #6                  // Field obj:Ljava/lang/Object;
        26: return
      LineNumberTable:
        line 12: 0
        line 4: 4
        line 5: 10
        line 7: 15
        line 13: 26
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      27     0  this   Lcom/leolin/jvm/bytecode/MyTest2;
            0      27     1     a   I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #7                  // class com/leolin/jvm/bytecode/MyTest2
         3: dup
         4: invokespecial #8                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: bipush        8
        11: invokespecial #9                  // Method setX:(I)V
        14: bipush        20
        16: invokestatic  #10                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        19: putstatic     #11                 // Field in:Ljava/lang/Integer;
        22: return
      LineNumberTable:
        line 17: 0
        line 18: 8
        line 19: 14
        line 20: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  args   [Ljava/lang/String;
            8      15     1 myTest2   Lcom/leolin/jvm/bytecode/MyTest2;

  private synchronized void setX(int);
    descriptor: (I)V
    flags: ACC_PRIVATE, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #4                  // Field x:I
         5: return
      LineNumberTable:
        line 23: 0
        line 24: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/leolin/jvm/bytecode/MyTest2;
            0       6     1     x   I

  private void test(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PRIVATE
    Code:
      stack=3, locals=4, args_size=2
         0: aload_0
         1: getfield      #6                  // Field obj:Ljava/lang/Object;
         4: dup
         5: astore_2
         6: monitorenter
         7: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        10: new           #13                 // class java/lang/StringBuilder
        13: dup
        14: invokespecial #14                 // Method java/lang/StringBuilder."<init>":()V
        17: ldc           #15                 // String str:
        19: invokevirtual #16                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: aload_1
        23: invokevirtual #16                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        26: invokevirtual #17                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        29: invokevirtual #18                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        32: aload_2
        33: monitorexit
        34: goto          42
        37: astore_3
        38: aload_2
        39: monitorexit
        40: aload_3
        41: athrow
        42: return
      Exception table:
         from    to  target type
             7    34    37   any
            37    40    37   any
      LineNumberTable:
        line 27: 0
        line 28: 7
        line 29: 32
        line 30: 42
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      43     0  this   Lcom/leolin/jvm/bytecode/MyTest2;
            0      43     1   str   Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 37
          locals = [ class com/leolin/jvm/bytecode/MyTest2, class java/lang/String, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: bipush        10
         2: invokestatic  #10                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         5: putstatic     #11                 // Field in:Ljava/lang/Integer;
         8: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        11: ldc           #19                 // String hello
        13: invokevirtual #18                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        16: return
      LineNumberTable:
        line 6: 0
        line 33: 8
        line 34: 16
}
SourceFile: "MyTest2.java"

  

首先,我们来分析两个构造方法:

  public com.leolin.jvm.bytecode.MyTest2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String Welcome
         7: putfield      #3                  // Field str:Ljava/lang/String;
        10: aload_0
        11: iconst_5
        12: putfield      #4                  // Field x:I
        15: aload_0
        16: new           #5                  // class java/lang/Object
        19: dup
        20: invokespecial #1                  // Method java/lang/Object."<init>":()V
        23: putfield      #6                  // Field obj:Ljava/lang/Object;
        26: return
      LineNumberTable:
        line 9: 0
        line 4: 4
        line 5: 10
        line 7: 15
        line 10: 26
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      27     0  this   Lcom/leolin/jvm/bytecode/MyTest2;

  public com.leolin.jvm.bytecode.MyTest2(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=2
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String Welcome
         7: putfield      #3                  // Field str:Ljava/lang/String;
        10: aload_0
        11: iconst_5
        12: putfield      #4                  // Field x:I
        15: aload_0
        16: new           #5                  // class java/lang/Object
        19: dup
        20: invokespecial #1                  // Method java/lang/Object."<init>":()V
        23: putfield      #6                  // Field obj:Ljava/lang/Object;
        26: return
      LineNumberTable:
        line 12: 0
        line 4: 4
        line 5: 10
        line 7: 15
        line 13: 26
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      27     0  this   Lcom/leolin/jvm/bytecode/MyTest2;
            0      27     1     a   I

  

第一个是无参构造方法,但很奇怪的是,第一个构造方法的参数数量args_size为1。这是因为Java在调用实例方法时,每个方法的第一个参数都是接收一个this,而Java会隐式的帮我们把当前对象传入实例方法。比如下面的A类:

class A {
    public void test() {
    }
}

  

当我们调用一个A的实例方法test时,会转换成这样的形式:A.test(this),同理构造方法。这样我们也就能够理解,为什么MyTest2第二个构造方法的的参数数量args_size为2。第一个是Java帮我们隐式传入的this,第二个是我们显式声明的int类型变量a。我们知道,如果我们在类里为成员变量赋值,这些赋值的动作其实是在构造方法里完成。这里我们可以看到,两个构造方法中对应的字节码指令其实是一样的,也就是说,给成员变量赋值的字节码指令,会在每一个构造方法中冗余一份。我们来看下构造方法中的字节码指令:

 0: aload_0
 1: invokespecial #1                  // Method java/lang/Object."<init>":()V
 4: aload_0
 5: ldc           #2                  // String Welcome
 7: putfield      #3                  // Field str:Ljava/lang/String;
10: aload_0
11: iconst_5
12: putfield      #4                  // Field x:I
15: aload_0
16: new           #5                  // class java/lang/Object
19: dup
20: invokespecial #1                  // Method java/lang/Object."<init>":()V
23: putfield      #6                  // Field obj:Ljava/lang/Object;
26: return

  

在构造方法中,先执行aload_0指令加载this的引用到栈顶,再执行invokespecial指令调用this的父类Object的构造方法,完成父类的初始化。执行完偏移0和1的指令之后,this引用会从栈中弹出,所以在此执行aload_0加载this到栈顶,然后执行ldc指令,加载常量池的元素第二个元素:Welcome到栈顶。putfield指令接收一个参数,即要在对象中设置值的字段,然后从栈中弹出两个元素,即是this引用和Welcome,完成成员变量str的赋值,这是偏移4到7所做的事。接着在偏移10到12完成字段x的赋值。在偏移15到23之间,依旧是加载this到栈顶,用new指令创建一个Object的对象然后推至栈顶,接着调用dup指令,根据栈顶的Object对象引用再次创建一份引用推至栈顶,至此栈顶有连续两个同一Object对象的引用,执行invokespecial指令,会调用Object对象的构造方法,然后弹出Object对象的引用,此时栈顶还有一个Object对象和this对象的引用,执行putfield指令,将栈中的两个引用弹出,把Object对象的引用设置进this对象的obj字段中。最后,执行偏移26的指令return,构造方法结束。

接着,我们分析main方法:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #7                  // class com/leolin/jvm/bytecode/MyTest2
         3: dup
         4: invokespecial #8                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: bipush        8
        11: invokespecial #9                  // Method setX:(I)V
        14: bipush        20
        16: invokestatic  #10                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        19: putstatic     #11                 // Field in:Ljava/lang/Integer;
        22: return
      LineNumberTable:
        line 17: 0
        line 18: 8
        line 19: 14
        line 20: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  args   [Ljava/lang/String;
            8      15     1 myTest2   Lcom/leolin/jvm/bytecode/MyTest2;

  

因为main方法是静态方法,静态方法就不像之前的实例方法,会隐式传入一个this,所以main方法设定传入一个数组参数,args_size即为1。首先,偏移0~7完成将创建的MyTest2实例压入栈顶,复制一份实例的引用压入栈顶,然后弹出引用执行构造方法,之后执行astore_1弹出MyTest2实例,将其保存在局部变量表索引为1的位置,,至此,操作数栈中不存在任何元素。

然后在偏移8到11之间,先执行aload_1,将局部变量表索引为1的引用加载到栈顶,这里是之前创建的MyTest2实例,再执行bipush,将一个字节长度的数字推送到栈顶,这里是8。之后调用invokespecial,从栈中弹出两个元素,将8作为参数传入MyTest2实例的方法setX(int x)。

最后在偏移14到22之间,执行bipush,将20推送至栈顶,然后调用invokestatic指令,执行Integer类中的静态方法valueOf(String s),将原先栈顶的元素20弹出传入该方法中,得到一个Integer对象后压入栈顶。执行putstatic,从栈顶弹出Integer对象,这只为类中静态变量in的值。

接下来,我们来看我们定义的方法setX(int x)和void text(),这两个都是同步方法:

  ……
  private synchronized void setX(int);
    descriptor: (I)V
    flags: ACC_PRIVATE, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #4                  // Field x:I
         5: return
  ……
  
  private void test(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PRIVATE
    Code:
      stack=3, locals=4, args_size=2
         0: aload_0
         1: getfield      #6                  // Field obj:Ljava/lang/Object;
         4: dup
         5: astore_2
         6: monitorenter
         7: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        10: new           #13                 // class java/lang/StringBuilder
        13: dup
        14: invokespecial #14                 // Method java/lang/StringBuilder."<init>":()V
        17: ldc           #15                 // String str:
        19: invokevirtual #16                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: aload_1
        23: invokevirtual #16                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        26: invokevirtual #17                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        29: invokevirtual #18                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        32: aload_2
        33: monitorexit
        34: goto          42
        37: astore_3
        38: aload_2
        39: monitorexit
        40: aload_3
        41: athrow
        42: return
  ……

  

首先是setX(int x),我们在方法的声明处加上synchronized,因此方法的访问标志就有ACC_SYNCHRONIZED。当有多个线程调用setX(int x)方法时,该方法是通过对MyTest2的class对象加锁和释放锁来实现的同步。

而在test()方法中,我们是通过synchronized块对实例内的对象obj上锁来实现同步的,这里有两个我们之前没见过的命令monitorenter和monitorexit。每个对象都会关联一个监视器,监视器有个条目计数,执行monitorenter指令时进入对象的监视器,如果监视器的条目计数为0,则设置为1,当前线程为监视器的所有者。线程还可以进入其他的synchronized块,如果synchronized块上锁的对象所关联的监视器归当前线程所拥有,会对监视器的条目计数加1。其他线程如果想拥有监视器的所有权,必须等到监视器条目为0的时候,才可以获取监视器的所有权。

那我们很容易想到monitorexit的作用,当线程离开一个synchronized块时,synchronized块所关联的监视器的条目计数就会减1。当监视器的条目计数减为0时,代表监视器此时无无主的状态,其他因为监视器而陷入阻塞的线程就能获得该监视器的所有权。我们注意到,上面的monitorenter指令只有一条,而monitorexit却不止一条。这是因为synchronized块的入口只有一个,出口却可以有好几个,如果代码没有异常的话,synchronized块应该是从上执行到下,先执行monitorenter对监视器的条目计数加1,最后执行monitorexit对监视器条目减1,这是最理想的状态。但是我们的程序是有可能抛出异常的,当异常抛出时,我们也要对监视器的条目计数减1,这就要求Java要能观察出程序中所有有可能抛出异常的地方,当异常发生要对监视器的条目计数减1,否则其他需要监视器的线程将永远陷入阻塞的状态。

最后是我们的静态代码块,在一个类中,会在一个名为clinit的静态方法中去执行所有静态变量的赋值、以及静态代码块的执行。根据之前类加载机制,我们知道类的加载和链接,只是完成静态变量内存的分配,以及赋予静态变量默认值,比如int的默认值为0,对象的默认值为null。但是在初始化的时候,才会将程序员赋予变量的值,赋予静态变量,即在clinit中。

  ……
  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: bipush        10
         2: invokestatic  #10                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         5: putstatic     #11                 // Field in:Ljava/lang/Integer;
         8: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        11: ldc           #19                 // String hello
        13: invokevirtual #18                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        16: return
      LineNumberTable:
        line 6: 0
        line 33: 8
        line 34: 16
  ……

  

在MyTest2这个类中,我们声明了一个Integer类型的静态变量in,而且还有一个静态代码块,静态代码块中我们打印了一个hello。可以看到,上面偏移0到5和偏移8到13分别对应静态变量in赋值为10和获取IO流打印hello。至此,MyTest2的方法分析完毕。

posted @ 2020-05-15 21:39  北洛  阅读(145)  评论(0编辑  收藏  举报