zoukankan      html  css  js  c++  java
  • jvm基础知识—垃圾回收机制

    1、首先类的实例化、static、父类构造函数执行顺序

    我们来看下面的程序代码:

    public class A {
    
        int a1 = 8;
        {
            int a3 = 9;
            System.out.println("top of A() a1=" + a1 + " a2=" + "  a3=" + a3);
        }
    
        
        int a2 = getA2();
    
        public A() {
            this(66);
            System.out.print("A 构造函数
    ");
        }
    
        
    
        public A(int num) {
            System.out.print("A 带参数构造函数: " + num + "
    ");
        }
    
        static {
            System.out.println("I`m a static {} from class A..");
        }
    
        int getA2() {
            System.out.println("getA2..");
            return 7;
        }
    
        public void methodA() {
            System.out.println("methodA");
        }
        
        
        {
            System.out.println("below A()..has start");
        }
        
        int a3 = getA2();
    
    }
    public class B extends A {
    
        int b1 = 0;
        int b2 = getB2();
        {
            int b3 = 5;
            System.out.println("top of B() b1=" + b1 + " b2=" + b2 + " b3=" + b3);
    
        }
    
        public B() {
            this(33);
            // super(44);//添加super语句,会导致实例化时直接执行父类带参数的构造函数
            System.out.print("B 构造函数
    ");
        }
    
        public B(int num) {
            // 添加super语句,会导致实例化时直接执行父类带参数的构造函数
            // 前提是带参数的构造函数B会被运行(new实例化或this)
            // super(77);
    
            System.out.print("B 带参数构造函数:" + num + "
    ");
        }
    
        {
            System.out.println("below B()..has start");
        }
        static {
            System.out.println("I`m a static {} from class B..");
        }
    
        int getB2() {
            System.out.println("getB2..");
            return 33;
    
        }
    
        @Override
        public void methodA() {
            System.out.println("methoaA int class B");
            super.methodA();
    
        }
    
    }
    public class mymain {
    
        /**
         * @param args
         */
        public static void main(String[] args) {
            // TODO Auto-generated method stub
            System.out.println("main app run..");
            B b = new B();
    //        B b = new B(22);
            b.methodA();
        }
    
    }

    程序的运行结果是:

    main app run..
    I`m a static {} from class A..
    I`m a static {} from class B..
    top of A() a1=8 a2= a3=9
    getA2..
    below A()..has start
    getA2..
    A 带参数构造函数: 66
    A 构造函数
    getB2..
    top of B() b1=0 b2=33 b3=5
    below B()..has start
    B 带参数构造函数:33
    B 构造函数
    methoaA int class B
    methodA

    总结:

    1、先静态 先执行父类的静态代码块,然后执行子类的静态代码块
    2、然后执行父类的普通成员变量的初始化,执行父类的普通代码块,执行按照顺序依次执行,先初始化成员变量a1,再按照顺序继续执行

    {
    int a3 = 9;
    System.out.println("top of A() a1=" + a1 + " a2=" + " a3=" + a3);
    }


    3、然后执行父类的构造函数,把父类创建出来
    4、然后执行子类的然后执行子类类的普通成员变量的初始化,执行子类的普通代码块,执行按照顺序依次执行
    5、然后然后执行子类的构造函数,把子类创建出来
    6、最后调用子类对应的函数

    七.jvm内存结构
    1.方法区和对是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。
    2.Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
    3.方法区(Method Area),方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    4.程序计数器(Program Counter Register),程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
    5.JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    6.本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

    九.GC算法
    GC最基础的算法有三种:标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。
    1.标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
    2.复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
    3.标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
    4.分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

     

    在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

    1. 引用计数法
    所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.
    引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。

    2.标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

    这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:1.效率不高,标记和清除的效率都很低;2.会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作。

    3、.复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。比如内存块a和b,内存a中要进行垃圾回收

    将内存a中不需要被回收的对象复制到内存b中,然后将内存a中的所有对象全部回收,就是这样的一个效果

    java堆区域分为新生代带区域和老年区域,新生代区分又分为eden区域,From Survivor空间,To Survivor空间,其中from又叫s0区域,to区域又叫s1区域,s0区域和s1区域大小相等,所有新创建的对象都会存储在eden区域,对象如果经过垃圾

    在JVM中共享数据空间划分如下图所示

    上图中,刻画了Java程序运行时的堆空间,可以简述成如下2条

    1.JVM中共享数据空间可以分成三个大区,新生代(Young Generation)、老年代(Old Generation)、永久代(Permanent Generation),其中JVM堆分为新生代和老年代

    2.新生代可以划分为三个区,Eden区(存放新生对象),两个幸存区(From Survivor和To Survivor)(存放每次垃圾回收后存活的对象)

    3.永久代管理class文件、静态对象、属性等(JVM uses a separate region of memory, called the Permanent Generation (orPermGen for short), to hold internal representations of java classes. PermGen is also used to store more information )

    4.JVM垃圾回收机制采用“分代收集”:新生代采用复制算法,老年代采用标记清理算法。

    复制(Copying)算法

    将内存平均分成A、B两块,算法过程:

    1. 新生对象被分配到A块中未使用的内存当中。当A块的内存用完了, 把A块的存活对象对象复制到B块。
    2. 清理A块所有对象。
    3. 新生对象被分配的B块中未使用的内存当中。当B块的内存用完了, 把B块的存活对象对象复制到A块。
    4. 清理B块所有对象。
    5. goto 1。

    优点:简单高效。缺点:内存代价高,有效内存为占用内存的一半。

    图解说明如下所示:(图中后观是一个循环过程)

    对复制算法进一步优化:使用Eden/S0/S1三个分区

    平均分成A/B块太浪费内存,采用Eden/S0/S1三个区更合理,空间比例为Eden:S0:S1==8:1:1,有效内存(即可分配新生对象的内存)是总内存的9/10。

    算法过程:

    1. Eden+S0可分配新生对象;
    2. 对Eden+S0进行垃圾收集,存活对象复制到S1。清理Eden+S0。一次新生代GC结束。
    3. Eden+S1可分配新生对象;
    4. 对Eden+S1进行垃圾收集,存活对象复制到S0。清理Eden+S1。二次新生代GC结束。
    5. goto 1。

    默认Eden:S0:S1=8:1:1,因此,新生代中可以使用的内存空间大小占用新生代的9/10,那么有人就会问,为什么不直接分成两个区,一个区占9/10,另一个区占1/10,这样做的原因大概有以下几种

    1.S0与S1的区间明显较小,有效新生代空间为Eden+S0/S1,因此有效空间就大,增加了内存使用率
    2.有利于对象代的计算,当一个对象在S0/S1中达到设置的XX:MaxTenuringThreshold值后,会将其分到老年代中,设想一下,如果没有S0/S1,直接分成两个区,该如何计算对象经过了多少次GC还没被释放,你可能会说,在对象里加一个计数器记录经过的GC次数,或者存在一张映射表记录对象和GC次数的关系,是的,可以,但是这样的话,会扫描整个新生代中的对象, 有了S0/S1我们就可以只扫描S0/S1区了~~~

    在复制交换的过程仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中

    参看:http://www.cnblogs.com/SaraMoring/p/5713732.html

    下面这篇文章也相当的经典:

    http://blog.csdn.net/wy5612087/article/details/52369677

    聊聊JVM的年轻代
    1.为什么会有年轻代
    我们先来屡屡,为什么需要把堆分代?不分代不能完成他所做的事情么?其实不分代完全可以,分代的唯一理由就是优化GC性能。你先想想,如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
    2.年轻代中的GC
    HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。
    因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
    在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

    3.一个对象的这一辈子
    我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。
    4.有关年轻代的JVM参数
    1)-XX:NewSize和-XX:MaxNewSize
    用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。
    2)-XX:SurvivorRatio
    用于设置Eden和其中一个Survivor的比值,这个值也比较重要。
    3)-XX:+PrintTenuringDistribution
    这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。
    4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold
    用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。

    3、对于老年代的的gc采用的是标记-压缩算法 

    原理:第一阶段标记活的对象,第二阶段把为标记的对象压缩到堆的其中一块,按顺序放。
    优点:1、避免标记扫描的碎片问题;2、避免停止复制的空间问题。

    具体使用什么方法GC,Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率低的话,就切换到“标记-扫描”方式;同样,Java虚拟机会跟踪“标记-扫描”的效果,要是堆空间碎片出现很多碎片,就会切换回“停止-复制”模式。这就是自适应的技术。

    复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。而标记--清除算法会产生内部碎片,所以JVM的设计者们在此基础上进行了改进,标记--压缩算法由此诞生,被应用于老年代内存回收。

    标记--压缩算法的标记阶段和标记--清除算法的标记阶段是一致的,就不再重复。使用标记--压缩算法时,标记完可达对象之后,我们不再遍历所有对象清扫垃圾了,我们只需要将所有存活对象向“左”靠齐,让不连续的空间变成连续的,这样就没有内存碎片了。不仅如此,因为不再连续的空间变成连续的,内存分配也更快速了。

    对于标记--清除算法来说,因为内存中有碎片,空闲内存不再连续,为了分配内存,系统内可能要维护着一个空闲内存空间的链表。当需要分配内存时,会遍历这个链表,找到一个够大的内存块,然后将其分成两份,一份用作当前的分配,另一份放回链表(这样有造成更多的内存碎片,也有一些策略并不是按顺序查找,找到够大的就好,有可能是找到一个更好的空闲内存块为止)。而对于标记--压缩算法,内存空间是连续的,我们只需要一个指针标记出下一次分配工作要从哪里开始就可以了,分配后将指针递增所分配对象的大小,这个工作是非常快速的,而且不用维护那个空间内存链表了。

    这样一看好像标记--压缩算法绝对的优于标记--清除算法,那标记--清除还有啥存在的必要了呢?不过要记住的一点是标记--压缩算法为了达到压缩的目的,是需要移动对象的,这会有性能消耗的,这样所有对象的引用都必须更新。看来有利必有弊。

    6. 你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms,g1

    对象优先在新生代区中分配,若没有足够空间,Minor GC;
    大对象(需要大量连续内存空间)直接进入老年态;长期存活的对象进入老年态。如果对象在新生代出生并经过第一次MGC后仍然存活,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。

    我们来看下面的代码:

    package com.bjsxt.base001;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class Test06 {
    
        public static void main(String[] args) {
            
            //参数:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000*1024
            Map<Integer, byte[]> m = new HashMap<Integer, byte[]>();
            //每次创建1M数据,因为设置了PretenureSizeThreshold=1000*1024,该数据是小于1M(1024*1024)的,所以产生的数据是不能放在新生代的,只能存储在老年区中
            for(int i=0; i< 5; i++){
                byte[] b = new byte[1024*1024];
                m.put(i, b);
            }
        }
    }


    我们来看下面的案例:
    package com.bjsxt.base001;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class Test06 {
    
        public static void main(String[] args) {
            
            //参数:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000
            Map<Integer, byte[]> m = new HashMap<Integer, byte[]>();
          
            for(int i=0; i< 5*1024; i++){
                byte[] b = new byte[1024];//每次产生1K数据
                m.put(i, b);
            }
        }
    }
    
    

     初始化堆的大小是30M,最大堆是30M,使用串行的垃圾回收器,详细打印出GC的回收信息,

     每次创建1K数据,因为设置了PretenureSizeThreshold=1000,该数据是大于PretenureSizeThreshold=1000的,所以产生的数据是不能放在新生代的,只能存储在老年区中,但是我们来看看运行的结果

    Heap
    def new generation total 9216K, used 6426K [0x00000000f9000000, 0x00000000f9a00000, 0x00000000f9a00000)
    eden space 8192K, 78% used [0x00000000f9000000, 0x00000000f96468e8, 0x00000000f9800000)
    from space 1024K, 0% used [0x00000000f9800000, 0x00000000f9800000, 0x00000000f9900000)
    to space 1024K, 0% used [0x00000000f9900000, 0x00000000f9900000, 0x00000000f9a00000)
    tenured generation total 20480K, used 16K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
    the space 20480K, 0% used [0x00000000f9a00000, 0x00000000f9a04010, 0x00000000f9a04200, 0x00000000fae00000)
    compacting perm gen total 21248K, used 2524K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
    the space 21248K, 11% used [0x00000000fae00000, 0x00000000fb077198, 0x00000000fb077200, 0x00000000fc2c0000)
    No shared spaces configured.

    发现数据都存在新生代区的eden中,而没有存储在老年区中

    出现上面的问题的原因在于虚拟机会把体积不大的对象分配到TLAB,1K对象很小就默认分配到了TLAB区域了,我们把配置改成下面的就可以了

    -Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000 -XX:-UseTLAB

    -XX:-UseTLAB表示禁用TLAB区域,我们来看看效果

    
    

    Heap
    def new generation total 9216K, used 760K [0x00000000f9000000, 0x00000000f9a00000, 0x00000000f9a00000)
    eden space 8192K, 9% used [0x00000000f9000000, 0x00000000f90be040, 0x00000000f9800000)
    from space 1024K, 0% used [0x00000000f9800000, 0x00000000f9800000, 0x00000000f9900000)
    to space 1024K, 0% used [0x00000000f9900000, 0x00000000f9900000, 0x00000000f9a00000)
    tenured generation total 20480K, used 5416K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
    the space 20480K, 26% used [0x00000000f9a00000, 0x00000000f9f4a070, 0x00000000f9f4a200, 0x00000000fae00000)
    compacting perm gen total 21248K, used 2524K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
    the space 21248K, 11% used [0x00000000fae00000, 0x00000000fb077198, 0x00000000fb077200, 0x00000000fc2c0000)
    No shared spaces configured.

    老年代的使用效率就已经上来了

    Java内存模型的主要目标: 定义程序中各个变量的访问规则。
    Java线程之间的通信由Java内存模型(本文简称为JMM)控制。
    所有变量的存储都在主内存,每条线程还都有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作必须在工作内存完成,而不能直接读取主内存中的变量。不同的线程直接无法访问对方工作内存中的变量,线程间变量的传递均需要通过主内存来完成。

    voliate能够保证变量在线程中的可见性,一个线程修改了该变量,在另外一个线程中可以立刻得到最新的值,不清楚的看voliate关键字的使用

     在JDK 6之后支持对象的栈上分析和逃逸分析,在JDK 7中完全支持栈上分配对象。 其是否打开逃逸分析依赖于以下JVM的设置:

    -XX:+DoEscapeAnalysis

    import java.lang.management.ManagementFactory;
    import java.util.List;
    
    
    /**
     * 逃逸分析优化-栈上分配
     * 栈上分配,意思是方法内局部变量(未发生逃逸)生成的实例在栈上分配,不用在堆中分配,分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。
     * 一般生成的实例都是放在堆中的,然后把实例的指针或引用压入栈中。
     * 虚拟机参数设置如下,表示做了逃逸分析  消耗时间在10毫秒以下
     * -server -Xmx10m -Xms10m
       -XX:+DoEscapeAnalysis -XX:+PrintGC
     * 
     * 虚拟机参数设置如下,表示没有做逃逸分析 消耗时间在1000毫秒以上
     * -server -Xmx10m -Xms10m
       -XX:+DoEscapeAnalysis -XX:+PrintGC
     * @author 734621
     *
     */
    public class OnStack {
      public static void alloc(){
          byte[] b = new byte[2];
          b[0] = 1;
      }
      
      public static void main(String [] args){
          long b = System.currentTimeMillis();
          for(int i=0;i<100000000;i++){
              alloc();
          }
          long e = System.currentTimeMillis();
          System.out.println("消耗时间为:" + (e - b));
          
          List<String> paramters = ManagementFactory.getRuntimeMXBean().getInputArguments();
          for(String p : paramters){
              System.out.println(p);
          }
         
      }
    }
    进行逃逸分析之后,产生的后果是所有的对象都将由栈上分配,而非从JVM内存模型中的堆来分配。

     不清楚的看马士兵的java虚拟机视频在哔哩哔哩上面

  • 相关阅读:
    Docker运行python容器
    SharePoint Online 创建门户网站系列之定制栏目
    SharePoint Online 创建门户网站系列之创建栏目
    SharePoint Online 创建门户网站系列之图片滚动
    SharePoint Online 创建门户网站系列之导航
    SharePoint Online 创建门户网站系列之首页布局
    SharePoint Online 创建门户网站系列之母版页
    SharePoint Online 创建门户网站系列之准备篇
    SharePoint 2013 数据库中手动更新用户信息
    SharePoint 2013 新建项目字段自动加载上次保存值
  • 原文地址:https://www.cnblogs.com/kebibuluan/p/7736720.html
Copyright © 2011-2022 走看看