zoukankan      html  css  js  c++  java
  • JVM详解(五)——运行时数据区-方法区

    一、概述

    1、介绍

      《Java虚拟机规范》中明确说明:尽管所有的方法区在逻辑上属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。但对于HotSpot JVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
      所以,方法区看作是一块独立于Java堆的内存空间。官方文档:

      方法区在JVM启动的时候被创建,并且它的实际的物理内存空间和Java堆区一样,可以是不连续的。方法区的大小和堆一样,可选择固定大小可扩展。方法区的大小决定了系统可以保持多少个类,如果系统定义了太多的类,导致方法区溢出,会报OOM。比如:加载大量的第三方jar包,Tomcat部署的工程过多(30~50个),大量动态的生成反射类。关闭JVM,会释放这个区域的内存。

    2、栈、堆、方法区的交互关系

    3、方法区的演进

      把方法区理解成接口,永久代和元空间是这个接口的落地实现。在JDK7及以前,习惯上把方法区称为永久代,JDK8开始,使用元空间取代了永久代。二者最大的区别是:元空间不在虚拟机设置的内存中,而是使用本地内存。
      本质上,方法区和永久代并不等价,仅是对HotSpot而言的。《Java虚拟机规范》对如何实现方法区,不做统一要求,像BEA JRockit,IBM J9中不存在永久代。
      如果方法区无法满足新的内存分配需求时,将报OOM。现在来看,当年使用永久代,不是好的idea,导致Java程序更容易OOM(超过-XX:MaxPermSize上限)。

    4、设置方法区大小与OOM

      方法区的大小不是固定的,可以动态调整。
      JDK7及以前:
      -XX:PerSize来设置永久代初始空间大小。默认值是20.75M
      -XX:MaxPerSize来设置永久代最大空间大小。32位机器默认是64M,64位机器默认是82M。
      当JVM加载的类信息容量超过这个值,会报OOM:PerGen space
      JDK8及以后:
      元空间的大小可以使用-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定。
      Windows下,-XX:MetaspaceSize=约21M,-XX:MaxMetaspaceSize=-1,即没有限制。默认值依赖平台。对于一个64位的服务器端JVM来说,默认-XX:MetaspaceSize=21M。一旦触及这个水位线,Full GC将会触发,并卸载没用的类(即这些类对应的类加载器不再存活)。然后这个水位线将会重置,新的水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
      如果初始的水位线设置过低,上述水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到Full GC多次调用。为了避免频繁FC,建议将初始值设置为一个相对较高的值。
      代码示例:方法区OOM

     1 // jdk6/7中:
     2 // -XX:PermSize=10m -XX:MaxPermSize=10m
     3 
     4 // jdk8中:
     5 // -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
     6 public class OOMTest extends ClassLoader {
     7     public static void main(String[] args) {
     8         int j = 0;
     9         try {
    10             OOMTest test = new OOMTest();
    11             for (int i = 0; i < 10_000; i++) {
    12                 // 创建ClassWriter对象,用于生成类的二进制字节码
    13                 ClassWriter classWriter = new ClassWriter(0);
    14                 // 指明版本号,修饰符,类名,包名,父类,接口
    15                 classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
    16 
    17                 byte[] code = classWriter.toByteArray();
    18 
    19                 // 类的加载
    20                 // Class对象
    21                 test.defineClass("Class" + i, code, 0, code.length);
    22 
    23                 j++;
    24             }
    25         } finally {
    26             System.out.println(j);
    27         }
    28     }
    29 }
    30 
    31 // 不设置参数
    32 // 10000.无报错.使用动态的方法区,元空间.
    33 // 在元空间创建并加载了10000个类.元空间做了一个动态的扩展.
    34 
    35 // JDK8设置参数
    36 // 8466.
    37 // Exception in thread "main" java.lang.OutOfMemoryError: Metaspace

      如何解决这些OOM?
      (1)要解决OOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
      (2)如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots到引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确的定位出泄漏代码的位置。
      (3)如果不存在内存泄漏,内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx,-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

    二、方法区的内部结构

    1、介绍

      《深入理解Java虚拟机》书中对方法区存储内容描述如下:它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编辑器编译后的代码缓存等。

      注意:这里只是一个经典的结构图,不完全正确。因为在jdk7以后,静态变量,运行时常量池里面的字符串常量池有变化,不再放在方法区了,而是放在了堆空间,这个后面会提到(看图)。

    2、类型信息

      对每个加载的类型(类Class、接口Interface、枚举Enum、注解Annotation),JVM必须在方法区中存储以下类型信息:
      (1)这个类型的完整类路径(包名.类名)。
      (2)这个类型直接父类的完整类路径。
      (3)这个类型的修饰符(public,abstract,final的某个子集)。
      (4)这个类型直接接口的一个有序列表。
      类型信息里记录了,这个类是使用哪个类加载器加载进来的。

    3、域(Field)信息

      JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。域的相关信息包括:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient的某个子集)。

    4、方法信息

      JVM必须保存所有方法以下信息,同域信息一样包括声明顺序:
      (1)方法名称。
      (2)方法的返回类型(或void)。
      (3)方法的参数列表。
      (4)方法的修饰符(public、private、protected、static、final、synchronized、native、abstract的某个子集)。
      (5)方法的字节码、操作数栈、局部变量表及大小(abstract和native方法除外)。
      (6)异常表(abstract和native方法除外),每个异常处理的开始位置,结束位置,代码处理在程序计数器中的偏移地址,被捕获的异常类的常量池索引。
      代码示例:方法区的内部构成

     1 public class Main extends Object implements Comparable<String>, Serializable {
     2     //属性
     3     public int num = 10;
     4     private static String str = "测试方法的内部结构";
     5     
     6     public void test1() {
     7         int count = 20;
     8         System.out.println("count = " + count);
     9     }
    10 
    11     public static int test2(int cal) {
    12         int result = 0;
    13         try {
    14             int value = 30;
    15             result = value / cal;
    16         } catch (Exception e) {
    17             e.printStackTrace();
    18         }
    19         return result;
    20     }
    21 
    22     @Override
    23     public int compareTo(String o) {
    24         return 0;
    25     }
    26 }
      1 javap -v -p Main.class > temp.txt
      2 
      3 // 解析后的字节码文件.(删掉了部分无关的信息)
      4 Classfile /D:/workspace/Java/myweb/target/classes/com/lx/myweb/Main.class
      5   Last modified 2020-10-6; size 1670 bytes
      6   MD5 checksum 54aed047bf420df2ddb06256a1ed4a33
      7   Compiled from "Main.java"
      8 
      9 // 类型信息
     10 public class com.lx.myweb.Main extends java.lang.Object implements java.lang.Comparable<java.lang.String>, java.io.Serializable
     11   minor version: 0
     12   major version: 52
     13   flags: ACC_PUBLIC, ACC_SUPER
     14 
     15 // 常量池.#1:符号引用
     16 Constant pool:
     17    #1 = Methodref          #18.#53        // java/lang/Object."<init>":()V
     18 ……
     19   #85 = Utf8               printStackTrace
     20 {
     21     
     22 // 域信息
     23   public int num;
     24     descriptor: I
     25     flags: ACC_PUBLIC
     26 
     27   private static java.lang.String str;
     28     descriptor: Ljava/lang/String;
     29     flags: ACC_PRIVATE, ACC_STATIC
     30 
     31 // 方法信息(构造器)
     32   public com.lx.myweb.Main();
     33     descriptor: ()V
     34     flags: ACC_PUBLIC
     35     Code:
     36       stack=2, locals=1, args_size=1
     37          0: aload_0
     38          1: invokespecial #1                  // Method java/lang/Object."<init>":()V
     39          4: aload_0
     40          5: bipush        10
     41          7: putfield      #2                  // Field num:I
     42         10: return
     43       LineNumberTable:
     44         line 6: 0
     45         line 8: 4
     46       LocalVariableTable:
     47         Start  Length  Slot  Name   Signature
     48             0      11     0  this   Lcom/lx/myweb/Main;
     49 
     50 // 方法信息
     51   public void test1();
     52     descriptor: ()V
     53     flags: ACC_PUBLIC
     54     Code:
     55       stack=3, locals=2, args_size=1
     56          0: bipush        20
     57          2: istore_1
     58          3: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
     59          6: new           #4                  // class java/lang/StringBuilder
     60          9: dup
     61         10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
     62         13: ldc           #6                  // String count =
     63         15: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     64         18: iload_1
     65         19: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
     66         22: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
     67         25: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     68         28: return
     69       LineNumberTable:
     70         line 12: 0
     71         line 13: 3
     72         line 14: 28
     73       LocalVariableTable:
     74         Start  Length  Slot  Name   Signature
     75             0      29     0  this   Lcom/lx/myweb/Main;
     76             3      26     1 count   I
     77 
     78 // 方法信息
     79   public static int test2(int);
     80     descriptor: (I)I
     81     flags: ACC_PUBLIC, ACC_STATIC
     82     Code:
     83       stack=2, locals=3, args_size=1
     84          0: iconst_0
     85          1: istore_1
     86          2: bipush        30
     87          4: istore_2
     88          5: iload_2
     89          6: iload_0
     90          7: idiv
     91          8: istore_1
     92          9: goto          17
     93         12: astore_2
     94         13: aload_2
     95         14: invokevirtual #12                 // Method java/lang/Exception.printStackTrace:()V
     96         17: iload_1
     97         18: ireturn
     98       Exception table:
     99          from    to  target type
    100              2     9    12   Class java/lang/Exception
    101       LineNumberTable:
    102         line 17: 0
    103         line 19: 2
    104         line 20: 5
    105         line 23: 9
    106         line 21: 12
    107         line 22: 13
    108         line 24: 17
    109       LocalVariableTable:
    110         Start  Length  Slot  Name   Signature
    111             5       4     2 value   I
    112            13       4     2     e   Ljava/lang/Exception;
    113             0      19     0   cal   I
    114             2      17     1 result   I
    115       StackMapTable: number_of_entries = 2
    116         frame_type = 255 /* full_frame */
    117           offset_delta = 12
    118           locals = [ int, int ]
    119           stack = [ class java/lang/Exception ]
    120         frame_type = 4 /* same */
    121     MethodParameters:
    122       Name                           Flags
    123       cal
    124 
    125 // 子类方法信息
    126   public int compareTo(java.lang.String);
    127     descriptor: (Ljava/lang/String;)I
    128     flags: ACC_PUBLIC
    129     Code:
    130       stack=1, locals=2, args_size=2
    131          0: iconst_0
    132          1: ireturn
    133       LineNumberTable:
    134         line 29: 0
    135       LocalVariableTable:
    136         Start  Length  Slot  Name   Signature
    137             0       2     0  this   Lcom/lx/myweb/Main;
    138             0       2     1     o   Ljava/lang/String;
    139     MethodParameters:
    140       Name                           Flags
    141       o
    142 
    143 // 父类方法信息
    144   public int compareTo(java.lang.Object);
    145     descriptor: (Ljava/lang/Object;)I
    146     flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    147     Code:
    148       stack=2, locals=2, args_size=2
    149          0: aload_0
    150          1: aload_1
    151          2: checkcast     #13                 // class java/lang/String
    152          5: invokevirtual #14                 // Method compareTo:(Ljava/lang/String;)I
    153          8: ireturn
    154       LineNumberTable:
    155         line 6: 0
    156       LocalVariableTable:
    157         Start  Length  Slot  Name   Signature
    158             0       9     0  this   Lcom/lx/myweb/Main;
    159     MethodParameters:
    160       Name                           Flags
    161       o                              synthetic
    162 
    163 // 静态变量信息
    164   static {};
    165     descriptor: ()V
    166     flags: ACC_STATIC
    167     Code:
    168       stack=1, locals=0, args_size=0
    169          0: ldc           #15  // String 测试方法的内部结构
    170          2: putstatic     #16  // Field str:Ljava/lang/String;
    171          5: return
    172       LineNumberTable:
    173         line 9: 0
    174 }
    175 Signature: #50  // Ljava/lang/Object;Ljava/lang/Comparable<Ljava/lang/String;>;Ljava/io/Serializable;
    176 SourceFile: "Main.java"
    字节码文件

    5、non-final的类变量

      静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。类变量被类的所有实例共享,即使没有类实例,也可访问。
      代码示例:

     1 public class Main {
     2     public static void main(String[] args) {
     3         Order order = null;
     4         order.method();
     5 
     6         System.out.println(order.count);
     7     }
     8 }
     9 
    10 class Order {
    11     public static int count = 1;
    12 
    13     public static void method() {
    14         System.out.println("count = " + count);
    15     }
    16 }
    17 
    18 // IDEA会提示有编译错误.但是直接执行不会报错,不会有空指针.
    19 // count = 1
    20 // 1

    6、final的类变量:全局常量

      被声明为 final static 的变量的处理方法不同,每个全局常量在编译的时候就被分配了。
      代码示例:

    1 public class Main {
    2 
    3     public static int count = 1;
    4     public static final int num = 100;
    5 
    6     public static void method() {
    7         System.out.println("count = " + count);
    8     }
    9 }
     1 javap -v -p Main.class > temp.txt
     2 
     3 // 解析后的字节码文件.(删掉了部分无关的信息)
     4 {
     5   public static int count;
     6     descriptor: I
     7     flags: ACC_PUBLIC, ACC_STATIC
     8 
     9   public static final int num;
    10     descriptor: I
    11     flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    12     ConstantValue: int 100  // 声明为final的静态变量在编译时就有值了
    13 }

    7、常量池

      方法区,内部包含了运行时常量池;字节码文件,内部包含了常量池。要理解方法区,就需要理解清楚字节码文件,因为加载类的信息都在方法区。要理解方法区的运行时常量池,就需要理解字节码文件的常量池。

      一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息,就是常量池(constant pool),包括各种字面量和对类型、域、方法的符号引用。
      (1)为什么需要常量池?
      一个Java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里。换一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池。
      代码示例:

    1 public class Main {
    2     public static void main(String[] args) {
    3         System.out.println("hello world");
    4     }
    5 }

      这里面使用了String、System、PrintStream及Object等结构。这里代码量已经很小了,如果代码多,引用到的结构会更多,就需要常量池了!
      (2)常量池中有什么?
      常量池内存储的数据类型包括:数量值,字符串值,类引用,字段引用,方法引用。

      小结:常量池,可以看做一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

    8、运行时常量池

      运行时常量池是方法区的一部分。
      常量池表(Constant Pool)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
      JVM为每个已加载的类型(类或接口)都维护一个常量池,池中的数据项像数组项一样,是通过索引访问的。
      运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或字段引用,此时不再是常量池中的符号地址了,这里换为真实地址。
      运行时常量池,相对于Class文件常量池的另一重要特征是,具备动态性。
      运行时常量池类似于传统编程语言中的符号表,但是它所包含的数据却比符号表要更加丰富一些。
      字节码文件中的常量池经过类加载器加载放到方法区以后,对应的结构就称为运行时常量池。

    三、方法区的使用举例

      代码示例:方法区使用举例

    1 public class Main {
    2     public static void main(String[] args) {
    3         int x = 500;
    4         int y = 100;
    5         int a = x / y;
    6         int b = 50;
    7         System.out.println(a + b);
    8     }
    9 }

      下面讲解在程序执行的过程中,main方法的指令在一个一个执行的过程当中,程序计数器,虚拟机栈之间的协作关系。由于没有new对象,就不看堆空间的情况了。

    四、方法区的演进细节(重要)

    1、介绍

      首先明确,只有HotSpot才有永久代,BEA JRockit,IBM J9中不存在永久代。方法区如何实现属于虚拟机实现细节,《Java虚拟机规范》并不要求统一。
      JDK6及以前、JDK7、JDK8及以后,方法区的变化:

      StringTable,字符串常量池。
      注意:JDK7,此时的方法区(永久代)还是用的虚拟机的内存,和本地内存还有一个映射的概念。JDK8用的本地内存,不再使用虚拟机的内存。

    2、永久代为什么被元空间替换?

      官网给的理由是因为 JRockit 没有,为了整合JRockit和Hotspot,所以去掉了。这里也很明确的可以看到,字符串常量池静态变量移动到堆中。

      http://openjdk.java.net/jeps/122

      随着Java8 的到来,HotSpot VM中再也见不到永久代了。但这并不意味着类的元数据信息消失了,这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫元空间。
      由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。这项变动是很有必要的,原因:
      (1)为永久代设置空间大小是很难确定的。在某些场景下,如果动态加载类过多,容易产生Perm区的OOM。比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
      (2)对永久代进行调优是很困难的。对永久代的 Full GC 需要去判断这个类或者这个常量,不再被使用了,也挺花时间的。所以调优也会变得很困难。

    3、StringTable(字符串常量池)为什么要调整位置?

      因为永久代的回收效率很低,在Full GC的时候才会触发。而Full GC是老年代的空间不足、永久代不足时才会触发,这就导致StringTable回收效率不高。而开发中会有大量的字符串被创建,回收效率低,将导致永久代内存不足,放到堆里,能及时回收内存。

    4、如何证明静态变量存在哪?

      代码示例:

     1 // jdk7:-Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails
     2 // jdk8:-Xms200m -Xmx200m -XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails
     3 public class Main {
     4     // 100MB
     5     private static byte[] arr = new byte[1024 * 1024 * 100];
     6 
     7     public static void main(String[] args) {
     8         System.out.println(Main.arr);
     9 
    10 //        try {
    11 //            Thread.sleep(1000_000);
    12 //        } catch (InterruptedException e) {
    13 //            e.printStackTrace();
    14 //        }
    15     }
    16 }

      结果:基于jdk8。jdk6,jdk7没有演示。

      结论:jdk6,7,8,静态引用对应的对象实体始终都存在堆空间。
      注意:这里指的new的对象实体,始终都在堆空间中。上面(图示)指的变化,是静态变量 arr 这个变量名。
      那么如何验证呢?
      案例:一个静态属性,一个非静态属性,一个局部变量。这三个变量的对象放在哪? 这三个变量本身放在哪?这是《深入理解Java虚拟机》中的案例,staticObj、instanceObj、localObj存放在哪里?
      代码示例:

     1 // staticObj、instanceObj、localObj存放在哪里?
     2 public class StaticObjTest {
     3 
     4     static class Test {
     5         static ObjectHolder staticObj = new ObjectHolder();
     6         ObjectHolder instanceObj = new ObjectHolder();
     7 
     8         void foo() {
     9             ObjectHolder localObj = new ObjectHolder();
    10             System.out.println("done");
    11         }
    12     }
    13 
    14     private static class ObjectHolder {
    15     }
    16 
    17     public static void main(String[] args) {
    18         Test test = new StaticObjTest.Test();
    19         test.foo();
    20     }
    21 }

      这里需要使用一个工具,JHSDB,这个工具JDK9才有。结论:

      这三个地址都在eden区,也就是在堆中。也就证明了这三个对象实体都在堆中。即:只要是new出来的对象实体都在堆中。

    五、方法区的垃圾回收

    1、介绍

      《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上,也确实有未实现或未能完整实现方法区类型卸载的收集器存在,如JDK 11时期的ZGC收集器就不支持类卸载。
      一般来说,这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前Sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。
      方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。

    2、回收内容

      方法区常量池中主要存放的两大类常量:字面量和符号引用。
      字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。
      符号引用则属于编译原理方面的概念,包括下面三类常量:
      (1)类和接口的全限定名
      (2)字段的名称和描述符
      (3)方法的名称和描述符
      HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。回收废弃常量与回收Java堆中的对象非常类似。

    3、判定需要回收

    4、小结

      方法区要不要垃圾回收?Java虚拟机规范中没有明说,可以回收,也可以不回收。平时说的HotSpot是要的。
      回收的话,主要针对的是什么?不再使用的类型信息,运行时常量池中废弃的常量。

      动态链接,指向了运行时常量池当前方法的引用。

    作者:Craftsman-L

    本博客所有文章仅用于学习、研究和交流目的,版权归作者所有,欢迎非商业性质转载。

    如果本篇博客给您带来帮助,请作者喝杯咖啡吧!点击下面打赏,您的支持是我最大的动力!

  • 相关阅读:
    EF 学习代码
    VS10 调试 新功能
    高级编程 实验代码
    事务 代码
    ADO.NET的新功能:MARS(Multiple Active Result Set) 及 异步执行命令
    Log4Net
    获得CheckBoxList最后一个被操作的项
    在存储过程中用事务
    ASP.NET服务端添加客户端事件
    GridView遍历各行的控件和控件事件
  • 原文地址:https://www.cnblogs.com/originator/p/15427518.html
Copyright © 2011-2022 走看看