转载------如何dump出一个Java进程里的类对应的Class文件

原文出处:http://rednaxelafx.iteye.com/blog/727938/


如何dump出一个Java进程里的类对应的Class文件?

大家可能对JVM很好奇,想看看运行中某时刻上JVM里各种内部数据结构的状态。可能有人想看堆上所有对象都有哪些,分别位于哪个分代之类;可能有人想看当前所有线程的stack trace;可能有人想看一个方法是否被JIT编译过,被编译后的native代码是怎样的。对Sun HotSpot JVM而言,这些需求都有现成的API可以满足——通过Serviceability Agent(下面简称SA)。大家熟悉的jstack、jmap等工具在使用-F参数启动时其实就是通过SA来实现功能的。

这里介绍的是按需把class给dump出来的方法。
为什么我们要dump运行中的JVM里的class呢?直接从classpath上把Class文件找到不就好了么?这样的话只要用ClassLoader.getResourceAsStream(name)就能行了。例如说要找foo.bar.Baz的Class文件,类似这样就行:

Java代码  收藏代码
  1. ClassLoader loader = Thread.currentThread().getContextClassLoader();  
  2. InputStream in = loader.getResourceAsStream("foo/bar/Baz.class");  
  3. // 从in把内容拿出来,然后随便怎么处理  

用Groovy的交互式解释器shell来演示一下:
Groovysh代码  收藏代码
  1. D:\>\sdk\groovy-1.7.2\bin\groovysh  
  2. Groovy Shell (1.7.2, JVM: 1.6.0_20)  
  3. Type 'help' or '\h' for help.  
  4. -----------------------------------------------------------------------------  
  5. groovy:000> loader = Thread.currentThread().contextClassLoader  
  6. ===> org.codehaus.groovy.tools.RootLoader@61de33  
  7. groovy:000> stream = loader.getResourceAsStream('java/util/ArrayList.class')  
  8. ===> sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@5dfaf1  
  9. groovy:000> file = new File('ArrayList.class')  
  10. ===> ArrayList.class  
  11. groovy:000> file.createNewFile()  
  12. ===> true  
  13. groovy:000> file << stream  
  14. ===> ArrayList.class  
  15. groovy:000> quit  

这样就在当前目录建了个ArrayList.class文件,把java.util.ArrayList对应的Class文件拿到手了。

问题是,上述方式其实只是借助ClassLoader把它在classpath上能找到的Class文件复制了一份而已。如果我们想dump的类在加载时被修改过(例如说某些AOP的实现会这么做),或者在运行过程中被修改过(通过HotSwap),或者干脆就是运行时才创建出来的,那就没有现成的Class文件了。

需要注意,java.lang.Class<T>这个类虽然实现了java.io.Serializable接口,但直接将一个Class对象序列化是得不到对应的Class文件的。参考src/share/classes/java/lang/Class.java里的注释:
Java代码  收藏代码
  1. package java.lang;  
  2.   
  3. import java.io.ObjectStreamField;  
  4. // ...  
  5.   
  6. public final  
  7.     class Class<T> implements java.io.Serializable,   
  8.                   java.lang.reflect.GenericDeclaration,   
  9.                   java.lang.reflect.Type,  
  10.                               java.lang.reflect.AnnotatedElement {  
  11.     /** 
  12.      * Class Class is special cased within the Serialization Stream Protocol.  
  13.      * 
  14.      * A Class instance is written initially into an ObjectOutputStream in the  
  15.      * following format: 
  16.      * <pre> 
  17.      *      <code>TC_CLASS</code> ClassDescriptor 
  18.      *      A ClassDescriptor is a special cased serialization of  
  19.      *      a <code>java.io.ObjectStreamClass</code> instance.  
  20.      * </pre> 
  21.      * A new handle is generated for the initial time the class descriptor 
  22.      * is written into the stream. Future references to the class descriptor 
  23.      * are written as references to the initial class descriptor instance. 
  24.      * 
  25.      * @see java.io.ObjectStreamClass 
  26.      */  
  27.     private static final ObjectStreamField[] serialPersistentFields =   
  28.         new ObjectStreamField[0];  
  29.       
  30.     // ...  
  31. }  


=================================================================

HotSpot有一套私有API提供了对JVM内部数据结构的审视功能,称为Serviceability Agent。它是一套Java API,虽然HotSpot是用C++写的,但SA提供了HotSpot中重要数据结构的Java镜像类,所以可以直接写Java代码来查看一个跑在HotSpot上的Java进程的内部状态。它也提供了一些封装好的工具,可以直接在命令行上跑,包括下面提到的ClassDump工具。
SA的一个重要特征是它是“进程外审视工具”。也就是说,SA并不运行在要审视的目标进程中,而是运行在一个独立的Java进程中,通过操作系统上提供的调试API来连接到目标进程上。这样,SA的运行不会受到目标进程状态的影响,因而可以用于审视一个已经挂起的Java进程,或者是core dump文件。当然,这也就意味这一个SA进程不能用于审视自己。
一个被调试器连接上的进程会被暂停下来。所以在SA连接到目标进程时,目标进程也是一直处于暂停状态的,直到SA解除连接。如果需要在线上使用SA的话需要小心,不要通过SA做过于耗时的分析,宁可先把数据都抓出来,把SA的连接解除掉之后再离线分析。目前的使用经验是,连接上一个小Java进程的话很快就好了,但连接上一个“大”的Java进程(堆比较大、加载的类比较多)可能会在连接阶段卡住好几分钟,线上需要慎用。

目前(JDK6)在Windows上SA没有随HotSpot一起发布,所以无法在Windows上使用;在Linux、Solaris、Mac上使用都没问题。从JDK7 build 64开始Windows版JDK也带上SA,如果有兴趣尝鲜JDK7的话可以试试(http://dlc.sun.com.edgesuite.net/jdk7/binaries/index.html),当前版本是build 103;正式的JDK7今年10月份应该有指望吧。
在Windows版JDK里带上SA的相关bug是:
Bug 6743339: Enable building sa-jdi.jar and sawindbg.dll on Windows with hotspot build
Bug 6755621: Include SA binaries into Windows JDK

前面废话了那么多,接下来回到正题,介绍一下ClassDump工具。
SA自带了一个能把当前在HotSpot中加载了的类dump成Class文件的工具,称为ClassDump。它的全限定类名是sun.jvm.hotspot.tools.jcore.ClassDump,有main()方法,可以直接从命令行执行;接收一个命令行参数,是目标Java进程的进程ID,可以通过JDK自带的jps工具查找Java进程的ID。要执行该工具需要确保SA的JAR包在classpath上,位于$JAVA_HOME/lib/sa-jdi.jar。
默认条件下执行ClassDump会把当前加载的所有Java类都dump到当前目录下,如果有全限定名相同但内容不同的类同时存在于一个Java进程中,那么dump的时候会有覆盖现象,实际dump出来的是同名的类的最后一个(根据ClassDump工具的遍历顺序)。
如果需要指定被dump的类的范围,可以自己写一个过滤器,在启动ClassDump工具时指定-Dsun.jvm.hotspot.tools.jcore.filter=filterClassName,具体方法见下面例子;如果需要指定dump出来的Class文件的存放路径,可以用-Dsun.jvm.hotspot.tools.jcore.outputDir=path来指定,path替换为实际路径。

以下演示在Linux上进行。大家或许已经知道,Sun JDK对反射调用方法有一些特别的优化,会在运行时生成专门的“调用类”来提高反射调用的性能。这次演示就来看看生成的类是什么样子的。

首先编写一个自定义的过滤器。只要实现sun.jvm.hotspot.tools.jcore.ClassFilter接口即可。
Java代码  收藏代码
  1. import sun.jvm.hotspot.tools.jcore.ClassFilter;  
  2. import sun.jvm.hotspot.oops.InstanceKlass;  
  3.   
  4. public class MyFilter implements ClassFilter {  
  5.     @Override  
  6.     public boolean canInclude(InstanceKlass kls) {  
  7.         String klassName = kls.getName().asString();  
  8.         return klassName.startsWith("sun/reflect/GeneratedMethodAccessor");  
  9.     }  
  10. }  

InstanceKlass对应于HotSpot中表示Java类的内部对象。Sun JDK为反射调用生成的类的名字形如sun/reflect/GeneratedMethodAccessorN,其中N是一个整数;所以只要看看类名是否以"sun/reflect/GeneratedMethodAccessor"开头就能找出来了。留意到这里包名的分隔符是“/”而不是“.”,这是Java类在JVM中的“内部名称”形式,参考Java虚拟机规范第二版4.2小节

接下来写一个会引发JDK生成反射调用类的演示程序:
Java代码  收藏代码
  1. import java.lang.reflect.Method;  
  2.   
  3. public class Demo {  
  4.     public static void main(String[] args) throws Exception {  
  5.         Method p = System.out.getClass().getMethod("println", String.class);  
  6.         for (int i = 0; i < 16; i++) {  
  7.             p.invoke(System.out, "demo");  
  8.         }  
  9.         System.in.read(); // block the program  
  10.     }  
  11. }  


让Demo跑起来,然后先不要让它结束。通过jps工具看看它的进程ID是多少:
Command prompt代码  收藏代码
  1. [sajia@sajia class_dump]$ jps  
  2. 20542 Demo  
  3. 20554 Jps  


接下来执行ClassDump,指定上面自定义的过滤器(过滤器的类要在classpath上,本例中它在./bin):
Command prompt代码  收藏代码
  1. [sajia@sajia class_dump]$ java -classpath ".:./bin:$JAVA_HOME/lib/sa-jdi.jar" -Dsun.jvm.hotspot.tools.jcore.filter=MyFilter sun.jvm.hotspot.tools.jcore.ClassDump 20542  

执行结束后,可以看到dump出了一个Class文件,在./sun/reflect/GeneratedMethodAccessor1.class;.是默认的输出目录,后面的目录结构对应包名。

用javap看看这个Class文件有啥内容:
Javap代码  收藏代码
  1. [sajia@sajia class_dump]$ javap -verbose sun.reflect.GeneratedMethodAccessor1  
  2. public class sun.reflect.GeneratedMethodAccessor1 extends sun.reflect.MethodAccessorImpl  
  3.   minor version: 0  
  4.   major version: 46  
  5.   Constant pool:  
  6. const #1 = Asciz        sun/reflect/GeneratedMethodAccessor1;  
  7. const #2 = class        #1;     //  sun/reflect/GeneratedMethodAccessor1  
  8. const #3 = Asciz        sun/reflect/MethodAccessorImpl;  
  9. const #4 = class        #3;     //  sun/reflect/MethodAccessorImpl  
  10. const #5 = Asciz        java/io/PrintStream;  
  11. const #6 = class        #5;     //  java/io/PrintStream  
  12. const #7 = Asciz        println;  
  13. const #8 = Asciz        (Ljava/lang/String;)V;  
  14. const #9 = NameAndType  #7:#8;//  println:(Ljava/lang/String;)V  
  15. const #10 = Method      #6.#9;  //  java/io/PrintStream.println:(Ljava/lang/String;)V  
  16. const #11 = Asciz       invoke;  
  17. const #12 = Asciz       (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;;  
  18. const #13 = Asciz       java/lang/String;  
  19. const #14 = class       #13;    //  java/lang/String  
  20. const #15 = Asciz       java/lang/Throwable;  
  21. const #16 = class       #15;    //  java/lang/Throwable  
  22. const #17 = Asciz       java/lang/ClassCastException;  
  23. const #18 = class       #17;    //  java/lang/ClassCastException  
  24. const #19 = Asciz       java/lang/NullPointerException;  
  25. const #20 = class       #19;    //  java/lang/NullPointerException  
  26. const #21 = Asciz       java/lang/IllegalArgumentException;  
  27. const #22 = class       #21;    //  java/lang/IllegalArgumentException  
  28. const #23 = Asciz       java/lang/reflect/InvocationTargetException;  
  29. const #24 = class       #23;    //  java/lang/reflect/InvocationTargetException  
  30. const #25 = Asciz       <init>;  
  31. const #26 = Asciz       ()V;  
  32. const #27 = NameAndType #25:#26;//  "<init>":()V  
  33. const #28 = Method      #20.#27;        //  java/lang/NullPointerException."<init>":()V  
  34. const #29 = Method      #22.#27;        //  java/lang/IllegalArgumentException."<init>":()V  
  35. const #30 = Asciz       (Ljava/lang/String;)V;  
  36. const #31 = NameAndType #25:#30;//  "<init>":(Ljava/lang/String;)V  
  37. const #32 = Method      #22.#31;        //  java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V  
  38. const #33 = Asciz       (Ljava/lang/Throwable;)V;  
  39. const #34 = NameAndType #25:#33;//  "<init>":(Ljava/lang/Throwable;)V  
  40. const #35 = Method      #24.#34;        //  java/lang/reflect/InvocationTargetException."<init>":(Ljava/lang/Throwable;)V  
  41. const #36 = Method      #4.#27; //  sun/reflect/MethodAccessorImpl."<init>":()V  
  42. const #37 = Asciz       java/lang/Object;  
  43. const #38 = class       #37;    //  java/lang/Object  
  44. const #39 = Asciz       toString;  
  45. const #40 = Asciz       ()Ljava/lang/String;;  
  46. const #41 = NameAndType #39:#40;//  toString:()Ljava/lang/String;  
  47. const #42 = Method      #38.#41;        //  java/lang/Object.toString:()Ljava/lang/String;  
  48. const #43 = Asciz       Code;  
  49. const #44 = Asciz       Exceptions;  
  50. const #45 = Asciz       java/lang/Boolean;  
  51. const #46 = class       #45;    //  java/lang/Boolean  
  52. const #47 = Asciz       (Z)V;  
  53. const #48 = NameAndType #25:#47;//  "<init>":(Z)V  
  54. const #49 = Method      #46.#48;        //  java/lang/Boolean."<init>":(Z)V  
  55. const #50 = Asciz       booleanValue;  
  56. const #51 = Asciz       ()Z;  
  57. const #52 = NameAndType #50:#51;//  booleanValue:()Z  
  58. const #53 = Method      #46.#52;        //  java/lang/Boolean.booleanValue:()Z  
  59. const #54 = Asciz       java/lang/Byte;  
  60. const #55 = class       #54;    //  java/lang/Byte  
  61. const #56 = Asciz       (B)V;  
  62. const #57 = NameAndType #25:#56;//  "<init>":(B)V  
  63. const #58 = Method      #55.#57;        //  java/lang/Byte."<init>":(B)V  
  64. const #59 = Asciz       byteValue;  
  65. const #60 = Asciz       ()B;  
  66. const #61 = NameAndType #59:#60;//  byteValue:()B  
  67. const #62 = Method      #55.#61;        //  java/lang/Byte.byteValue:()B  
  68. const #63 = Asciz       java/lang/Character;  
  69. const #64 = class       #63;    //  java/lang/Character  
  70. const #65 = Asciz       (C)V;  
  71. const #66 = NameAndType #25:#65;//  "<init>":(C)V  
  72. const #67 = Method      #64.#66;        //  java/lang/Character."<init>":(C)V  
  73. const #68 = Asciz       charValue;  
  74. const #69 = Asciz       ()C;  
  75. const #70 = NameAndType #68:#69;//  charValue:()C  
  76. const #71 = Method      #64.#70;        //  java/lang/Character.charValue:()C  
  77. const #72 = Asciz       java/lang/Double;  
  78. const #73 = class       #72;    //  java/lang/Double  
  79. const #74 = Asciz       (D)V;  
  80. const #75 = NameAndType #25:#74;//  "<init>":(D)V  
  81. const #76 = Method      #73.#75;        //  java/lang/Double."<init>":(D)V  
  82. const #77 = Asciz       doubleValue;  
  83. const #78 = Asciz       ()D;  
  84. const #79 = NameAndType #77:#78;//  doubleValue:()D  
  85. const #80 = Method      #73.#79;        //  java/lang/Double.doubleValue:()D  
  86. const #81 = Asciz       java/lang/Float;  
  87. const #82 = class       #81;    //  java/lang/Float  
  88. const #83 = Asciz       (F)V;  
  89. const #84 = NameAndType #25:#83;//  "<init>":(F)V  
  90. const #85 = Method      #82.#84;        //  java/lang/Float."<init>":(F)V  
  91. const #86 = Asciz       floatValue;  
  92. const #87 = Asciz       ()F;  
  93. const #88 = NameAndType #86:#87;//  floatValue:()F  
  94. const #89 = Method      #82.#88;        //  java/lang/Float.floatValue:()F  
  95. const #90 = Asciz       java/lang/Integer;  
  96. const #91 = class       #90;    //  java/lang/Integer  
  97. const #92 = Asciz       (I)V;  
  98. const #93 = NameAndType #25:#92;//  "<init>":(I)V  
  99. const #94 = Method      #91.#93;        //  java/lang/Integer."<init>":(I)V  
  100. const #95 = Asciz       intValue;  
  101. const #96 = Asciz       ()I;  
  102. const #97 = NameAndType #95:#96;//  intValue:()I  
  103. const #98 = Method      #91.#97;        //  java/lang/Integer.intValue:()I  
  104. const #99 = Asciz       java/lang/Long;  
  105. const #100 = class      #99;    //  java/lang/Long  
  106. const #101 = Asciz      (J)V;  
  107. const #102 = NameAndType        #25:#101;//  "<init>":(J)V  
  108. const #103 = Method     #100.#102;      //  java/lang/Long."<init>":(J)V  
  109. const #104 = Asciz      longValue;  
  110. const #105 = Asciz      ()J;  
  111. const #106 = NameAndType        #104:#105;//  longValue:()J  
  112. const #107 = Method     #100.#106;      //  java/lang/Long.longValue:()J  
  113. const #108 = Asciz      java/lang/Short;  
  114. const #109 = class      #108;   //  java/lang/Short  
  115. const #110 = Asciz      (S)V;  
  116. const #111 = NameAndType        #25:#110;//  "<init>":(S)V  
  117. const #112 = Method     #109.#111;      //  java/lang/Short."<init>":(S)V  
  118. const #113 = Asciz      shortValue;  
  119. const #114 = Asciz      ()S;  
  120. const #115 = NameAndType        #113:#114;//  shortValue:()S  
  121. const #116 = Method     #109.#115;      //  java/lang/Short.shortValue:()S  
  122.   
  123. {  
  124. public sun.reflect.GeneratedMethodAccessor1();  
  125.   Code:  
  126.    Stack=1, Locals=1, Args_size=1  
  127.    0:   aload_0  
  128.    1:   invokespecial   #36; //Method sun/reflect/MethodAccessorImpl."<init>":()V  
  129.    4:   return  
  130.   
  131. public java.lang.Object invoke(java.lang.Object, java.lang.Object[])   throws java.lang.reflect.InvocationTargetException;  
  132.   Exceptions:   
  133.    throws java.lang.reflect.InvocationTargetException  Code:  
  134.    Stack=5, Locals=3, Args_size=3  
  135.    0:   aload_1  
  136.    1:   ifnonnull       12  
  137.    4:   new     #20; //class java/lang/NullPointerException  
  138.    7:   dup  
  139.    8:   invokespecial   #28; //Method java/lang/NullPointerException."<init>":()V  
  140.    11:  athrow  
  141.    12:  aload_1  
  142.    13:  checkcast       #6; //class java/io/PrintStream  
  143.    16:  aload_2  
  144.    17:  arraylength  
  145.    18:  sipush  1  
  146.    21:  if_icmpeq       32  
  147.    24:  new     #22; //class java/lang/IllegalArgumentException  
  148.    27:  dup  
  149.    28:  invokespecial   #29; //Method java/lang/IllegalArgumentException."<init>":()V  
  150.    31:  athrow  
  151.    32:  aload_2  
  152.    33:  sipush  0  
  153.    36:  aaload  
  154.    37:  checkcast       #14; //class java/lang/String  
  155.    40:  invokevirtual   #10; //Method java/io/PrintStream.println:(Ljava/lang/String;)V  
  156.    43:  aconst_null  
  157.    44:  areturn  
  158.    45:  invokespecial   #42; //Method java/lang/Object.toString:()Ljava/lang/String;  
  159.    48:  new     #22; //class java/lang/IllegalArgumentException  
  160.    51:  dup_x1  
  161.    52:  swap  
  162.    53:  invokespecial   #32; //Method java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V  
  163.    56:  athrow  
  164.    57:  new     #24; //class java/lang/reflect/InvocationTargetException  
  165.    60:  dup_x1  
  166.    61:  swap  
  167.    62:  invokespecial   #35; //Method java/lang/reflect/InvocationTargetException."<init>":(Ljava/lang/Throwable;)V  
  168.    65:  athrow  
  169.   Exception table:  
  170.    from   to  target type  
  171.     12    40    45   Class java/lang/ClassCastException  
  172.   
  173.     12    40    45   Class java/lang/NullPointerException  
  174.   
  175.     40    43    57   Class java/lang/Throwable  
  176.   
  177.   
  178. }  


用Java来表现这个类的话,就是:
Java代码  收藏代码
  1. package sun.reflect;  
  2.   
  3. public class GeneratedMethodAccessor1 extends MethodAccessorImpl {  
  4.     public GeneratedMethodAccessor1() {  
  5.         super();  
  6.     }  
  7.       
  8.     public Object invoke(Object obj, Object[] args)  
  9.             throws IllegalArgumentException, InvocationTargetException {  
  10.         // prepare the target and parameters  
  11.         if (obj == nullthrow new NullPointerException();  
  12.         try {  
  13.             PrintStream target = (PrintStream) obj;  
  14.             if (args.length != 1throw new IllegalArgumentException();  
  15.             String arg0 = (String) args[0];  
  16.         } catch (ClassCastException e) {  
  17.             throw new IllegalArgumentException(e.toString());  
  18.         } catch (NullPointerException e) {  
  19.             throw new IllegalArgumentException(e.toString());  
  20.         }  
  21.         // make the invocation  
  22.         try {  
  23.             target.println(arg0);  
  24.             return null;  
  25.         } catch (Throwable t) {  
  26.             throw new InvocationTargetException(t);  
  27.         }  
  28.     }  
  29. }  

这段Java代码跟实际的Class文件最主要的不同的地方在于实际的Class文件是用同一个异常处理器来处理ClassCastException与NullPointerException的。如果用Java 7的多重catch语法来写的话就是:
Java代码  收藏代码
  1. package sun.reflect;  
  2.   
  3. public class GeneratedMethodAccessor1 extends MethodAccessorImpl {  
  4.     public GeneratedMethodAccessor1() {  
  5.         super();  
  6.     }  
  7.       
  8.     public Object invoke(Object obj, Object[] args)  
  9.             throws IllegalArgumentException, InvocationTargetException {  
  10.         // prepare the target and parameters  
  11.         if (obj == nullthrow new NullPointerException();  
  12.         try {  
  13.             PrintStream target = (PrintStream) obj;  
  14.             if (args.length != 1throw new IllegalArgumentException();  
  15.             String arg0 = (String) args[0];  
  16.         } catch (final ClassCastException | NullPointerException e) {  
  17.             throw new IllegalArgumentException(e.toString());  
  18.         }  
  19.         // make the invocation  
  20.         try {  
  21.             target.println(arg0);  
  22.             return null;  
  23.         } catch (Throwable t) {  
  24.             throw new InvocationTargetException(t);  
  25.         }  
  26.     }  
  27. }  


本来想顺带演示一下用Java反编译器把例子里的Class文件反编译为Java源码的,但用了JDJad都无法正确识别这里比较特别的Exceptions属性表,只好人肉反编译写出来……识别不出来也正常,毕竟Java 7之前在Java源码这层是没办法对同一个异常处理器处理指定多个异常类型。

要深究的话,上面人肉反编译的Java文件跟实际Class文件还有些细节差异。
例如说JDK在生成Class文件时为了方便所以把一大堆“很可能会用到”的常量都写到常量池里了,但在代码里可能并没有用到常量池里的所有项;如果用javac编译Java源码就不会出现这种状况。
又例如生成的Class文件里一个局部变量也没用,locals=3之中三个都是参数:第一个是this,第二个是obj,第三个是args。求值的中间结果全部都直接在操作数栈上用掉了。而在Java源码里无法写出这样的代码,像是说try块不能从一个表达式的中间开始之类的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值