zoukankan      html  css  js  c++  java
  • JVM@JVM基础

    JVM虚拟机:JVM上不只能运行java程序,scalar等其他语言也可以在jvm上运行,只要能生成jvm上可以理解的字节码文件就行。

    类的生命周期

    加载、连接、初始化、使用和卸载

    类的加载

    加载:查找并加载类的二进制数据,class文件,类型的加载就是将类型所在的class文件/字节码文件从磁盘上加载到内存里面

    什么是类型 ?定义的classinterface枚举,并不是对象

    在java代码中,类型的加载、连接与初始化过程都是在程序运行期间完成的

    连接

    连接:将类与类之间的关系处理好,对于字节码的处理验证校验都是在加载连接上完成的

    • 验证:字节码文件可以被人为操纵,存在错误的可能,需要校验,确保被加载类的正确性
    • 准备:为类的静态变量分配内存,并将其初始化为默认值(false,0)
    • 解析:把类中的符号引用转换为直接引用

    初始化

    初始化:为类的静态变量分配正确的初始值,即对于静态变量进行赋值

    提供了更大的灵活性,增加了更多的可能性

    静态代码块在程序初始化的时候会被执行的

    如下:

    使用

    java程序对类的使用方式可分为两种:

    1.主动使用

    2.被动使用

    所有的java虚拟机实现必须在每个类或接口被java程序首次主动使用时才初始化他们

    主动使用

    主动使用的方式有以下7种:

    1. 创建类的实例

    2. 访问某个类或接口的静态变量,或者对该静态变量赋值

    3. 调用类的静态方法

    4. 反射(如Class.forName(“com.test.Test”))

    5. 初始化一个类的子类。当我初始化child类时,也会对parent进行初始化

    6. java虚拟机启动时被表明为启动类的类,包含main方法的类

    7. JDK1.7开始提供的动态语言支持。

    被动使用

    除了上面七种情况,其他使用java类的方式,都被看作是对类的被动使用,会导致类的加载,但导致类的初始化

    如下:

     

    mychild1没有被初始化,但是有没有被加载呢,使用-XX:+TraceClassLoading,用于追踪类的加载信息并打印出来。打印出来发现,虽然没有被初始化,但是被加载

    总结:

      对于静态字段来说,只有直接定义了该字段的类才会被初始化;

      初始化一个类的子类时,父类也会被初始化;

      在一个类初始化时,要求其父类全部都已经初始化完毕了

     另外一个例子,加有final关键字 的常量:

    常量在编译阶段会存入到调用这个常量的方法所在的类的常量池当中(本例子是MyTest1中),

    本质上,调用类MyTest1并没有直接引用到定义常量的类MyParent,因此不会触发定义常量的类Myparent的初始化

    这里指的是将常量存放到MyTest1的常量池中,之后MyTest1与MyParent就没有关系了

    甚至可以将out文件夹下的MyParent.class文件删除

    反编译MyTest1.class

    classes>javap -c com.test.MyTest1
    Compiled from "MyTest1.java"
    public class com.test.MyTest1 {
      public com.test.MyTest1();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public static void main(java.lang.String[]);
        Code:
           0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
           3: ldc           #4                  // String hello world,此处可以看出实际上没有加载MyParent类,直接使用了字符串常量
           5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
           8: return
    }

    小知识:助记符

    ldc表示将int 、float或是String类型的常量值从常量池中推送至栈顶

    bipush表示将单字节的(-128~127)的常量值推送到栈顶

    public static final short s = 7;

    sipush表示将一个短整型常量值-32768~32767推送至栈顶

    public static final int s = 128;

    iconst_1表示将int类型1推送至栈顶(iconst_1~iconst_5)

    类加载器

    每个类型都是由类加载器加载到内存当中的
    在下面几种情况,java虚拟机将结束生命周期
    1.执行System.exit();
    2.程序正常执行结束
    3.程序在执行过程中遇到了异常或错误而异常终止
    4.由于操作系统出现错误而导致java虚拟机进程终止


    * Java虚拟机基本概念
    * Java虚拟机的基本结构
    1. 类加载子系统(类加载器)
    负责从文件系统或者网络中加载class信息,加载的信息存放在方法区中
    2. 方法区(一个内存空间)
    存放类信息,常量信息,常量池信息,包括字符串字面量和数字常量等(.class文件)
    3. Java堆
    在虚拟机启动的时候建立Java堆,它是Java程序最主要的内存工作区域,几乎所有的对象实例都存放在Java堆中,堆空间是所有Java线程共享的
    4. 直接内存
    JavaNIO库运行Java程序使用直接内存,从而提高性能,通常直接内存会优于Java堆,适合读写频繁的场合
    5. Java栈
    每个虚拟机线程都有一个私有的栈,一个线程的Java栈在线程被创建的时候创建,Java栈中保存着局部变量,方法参数,Java的方法调用,返回值等
    6. 本地方法栈
    和Java栈非常类似,最大的不同在于,本地方法栈用于本地方法的调用,Java虚拟机允许Java直接调用本地方法(通常使用C语言编写)
    7. 垃圾回收系统
    垃圾收集系统是Java的核心,Java有一套自己的垃圾收集机制,开发人员无需手动进行对象清理
    8. PC寄存器(中间产物,如临时变量存放的区域)
    * PC寄存器也是每一个线程私有的空间,Java虚拟机会为每一个线程创建PC寄存器
    * 在任意时刻,一个线程总是在执行一个方法,这个方法被称为当前方法,
    如果当前方法不是本地方法,PC寄存器就会执行当前正在被执行的指令
    如果是本地方法,则PC寄存器为undefined
    * 寄存器存放如当前执行环境指针,程序计数器,操作栈指针,计算的变量指针等信息
    9. 执行引擎
    虚拟机最核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般先编译成机器码,再执行

    * 堆、栈、方法区
    * 堆解决的是数据存储的问题,即数据对象怎么放,放在哪
    * 栈解决的是程序的运行问题,即程序如何执行,或者说如何处理数据对象
    * 方法区则是辅助堆栈的块永久区(Perm),解决堆栈信息的产生,是先决条件
    * 实例:
    新建一个对象User,那么:
    * User类的一些信息,如类信息,静态信息(包括方法信息,静态变量等)都会存放在方法区中
    * User对象被实例化后,就会被存放在堆空间中
    * 然后我们可以使用栈中User对象的引用来操作这个对象(存放局部变量)
    * 详析Java堆
    * 几乎所有的对象都存放在Java堆中,并且堆完全是自动化管理的,通过垃圾回收机制,垃圾对象会自动清理,不需要显示的释放
    * 根据垃圾回收机制的不同,Java堆可能拥有不同的结构。
    * 最为常见的就是将整个Java堆分为新生代和老年代,其中新生代存放新生的对象或者年龄不大的对象,老年代则存放老年对象
    * 新生代分为eden区,s0区,s1区,s0区和s1区被称为from和to区域,它们是两块大小相等并且可以互换角色的空间
    * 绝大多数情况下,对象首先分配在eden区,在一次新生代回收后,如果对象还存活,则会进入s0或s1区,之后每经过一次新生代回收,如果对象存活则年龄加1,当对象达到一定的年龄,进入老年代
    * 详析Java栈
    * Java栈是一块线程私有的内存空间
    * 栈由3部分组成:局部变量表,操作数栈,帧数据区
    * 局部变量表:用于保存函数的参数和局部变量
    * 操作数栈:主要用于保存操作过程的中间结果,同时作为计算过程中的临时变量的存储空间
    * 帧数据区:
    栈需要一些数据来支持常量池的解析,帧数据区保存着访问常量池的指针,方便程序访问常量池
    另外,当函数返回或者出现异常,虚拟机必须有个异常处理表,方便发送异常的时候找到异常的代码,因此异常处理表也是帧数据区的一部分
    * 详析方法区
    * Java方法区和堆一样,方法区是一块所有线程共享的内存区域,它保存系统的类信息,比如类的字段、方法、常量池等
    * 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机会抛出内存溢出错误。
    * 方法区可以理解为永久区(Perm)

    * 了解虚拟机参数[主要围绕堆、栈、方法区进行配置]
    * 堆分配参数一
    * -XX:+PringGC
    使用这个参数,虚拟机启动后,只要遇到GC就会打印日志
    * -XX:+UseSerialGC
    配置串行回收器
    * -XX:+PrintGCDetails
    可以参看详细信息,包括各个区情况
    * -Xms:
    设置Java程序启动时初始堆大小,如-Xms5m 程序初始化的时候分配堆大小为5M
    * -Xmx:
    设置Java程序能获得的最大堆大小,如-Xmx20m 最大堆大小为20M
    * -Xmx20m -Xms5m -XX:+PrintCommandLineFlags
    可以将隐式或者显示传递给虚拟机的参数打印输出
    * 总结:在实际工作中,可以将初始堆大小与最大堆大小设置为相等,这样的好处是可以减少程序运行时垃圾回收次数,从而提高性能
    * 堆分配参数二:新生代的配置
    * -Xmn:可以设置新生代的大小,设置一个比较大的新生代会减少老年代的大小,这个参数对系统性能以及GC行为由很大的影响,新生代一般会设置整个堆空间的1/4到1/3左右
    * -XX:SurvivorRatio: 用来设置新生代中eden区和from/to区的空间比例。含义:
    -XX:SurvivorRatio=eden/from=eden/to
    * 总结:
    1. 不同的堆分布情况,堆系统执行会产生一定的影响,实际工作中应该合理配置,基本策略:尽可能将对象预留在新生代,减少老年代的GC次数
    2. 除了可以设置新生代的绝对大小(-Xmn),还可以使用(-XX:NewRatio)设置新生代和老年代的比例,含义:
    -XX:NewRatio=老年代/新生代
    * 英语单词
    Eden: 伊甸
    Survivor: 残存[from区+to区]
    Ratio: 比例,比值
    Tenure: 占有[JDK中指老年代]
    Threshold:门槛,门限
    * 垃圾回收算法
    * 1. 理论、过时算法:
    * 引用计数算法
    * 对象被引用时,计数器加1,当引用失效时,计数器减1。
    * 缺点:
    无法处理循环引用的问题
    每次引用都要进行加减操作,浪费系统性能
    * 标记清除算法
    * 分为标记和清除两个阶段进行处理内存中的对象
    * 缺点:
    空间碎片问题,垃圾回收后的空间是不连续的,不连续的内存空间的工作效率要低于连续的内存空间
    * 2. 流行、合理算法:【JDK采用中】
    * 复制算法 * [用到两块内存空间,且需要频繁复制,所以不适合于老年代对象]
    * Java新生代中from和to区就是使用这个算法
    * 核心思想:
    1. 将内存分为两块,每次只使用一块
    2. 在垃圾回收时,将正在使用的内存中存留的对象复制到未被使用的内存块中
    3. 之后清除之前正在使用的内存块中所有的对象,反复去交换两个内存角色,完成垃圾收集
    * 标记压缩算法 *
    * Java老年代使用的就是这个算法进行GC
    * 标记压缩法在标记清除法的基础上进行优化,把存活的对象压缩到内存的另外一端,而后进行垃圾清理
    * 分代算法
    * 根据对象的特点把内存分成N块, 根据每个内存的特点使用不同的算法[每回收一次,存活的对象年龄+1]
    * 3. 摸索中的算法
    * 分区算法
    * 将整个内存分成N个独立的内存空间,每个空间都可以独立使用,每次根据需要[数据变化],回收指定的几个小空间,而不是对整个内存进行GC,从而提高性能,并减少GC的停顿时间
    * 4. 想一想:为什么新生代和老年代不使用同一个回收算法呢?
    * 对于新生代来说,新生代回收频率很高,每次回收耗时很短
    * 对于老年代老说,老年代回收频率很低,每次回收耗时很长,所以应该尽量减少老年代的GC
    * 反例:如果对于老年代使用复制算法,我们可以知道,每次GC老年代保留的对象很多,也就意味着使用复制算法需要复制很多对象,而清除的垃圾对象很少,这样就很浪费性能了

    * 对象的分代转换
    * 新生代什么时候进入老年代?
    对象创建完成后,就会进入eden区,如果没有进行GC,它就一直停留在eden区,当进行一次GC后,如果对象还存活,就会进入from区或者to区,年龄+1,之后每一次GC如果它还存活,它都会在from区和to区来回切换,每进行一次GC,存活的对象年龄+1,当年龄到达一定的程度,它就会离开新生代,进入老年代
    * 配置参数
    -XX:MaxTenuringThreshold,默认值为15,它指定新生代经过几次GC就可以进入老年代
    * 特殊情况
    对于很大的对象,当新生代的eden区无法装入的时候,它会直接进入老年代
    配置参数:
    -XX:PretenureSizeThreshold
    总结:使用PretenureSizeThreshold可以指定直接进入老年代的对象大小,但必须注意:TLAB区域优先分配空间
    * 特殊情况的特殊情况:当对象大于eden区,但是体积又不是特别大的时候,虚拟机会优先把对象分配到TLAB区域,因此就失去了分配到老年代的机会
    * TLAB(Thread Local Allocation Buffer)
    * 什么是TLAB
    TLAB又称线程本地分配缓存,从名字上看是一个线程专用的内存分配区域,是为了加速对象分配而生的。每一个线程都会产生一个TLAB,该线程独享的工作区域,Java线程使用这种TLAB区域来避免多线程冲突的问题,提高了对象分配的效率。TLAB空间一般不会太大,当“大对象”无法在TLAB分配时,会分配到堆上的老年代
    * 相关参数
    * -XX:+UseTLAB 使用TLAB
    * -XX:+TLABSize 设置TLAB大小
    * -XX:TLABRefillWasteFraction 设置维护进入TLAB空间的单个对象的大小,他是一个比例值,默认值为64,即如果对象大于整个空间的1/64,则在堆上老年代创建对象
    * -XX:+PrintTLAB 查看TLAB信息
    * -XX:ResizeTLAB 自调整TLABRefillWasteFraction阀值
    * 垃圾收集器
    * 串行垃圾回收器
    * 指的是使用单线程进行垃圾回收的回收器。每次回收时,串行回收器只有一个工作线程,对于并行能力较弱的计算机,串行回收器的专注性和独占性往往有更好的性能表现。
    * 串行回收器可以在新生代和老年代使用,根据作用于不同的堆空间,分为新生代串行回收器和老年代串行回收器
    * 参数配置:-XX:+UseSerialGC 参数可以设置使用新生代串行回收器和老年代串行回收器
    * 并行垃圾回收器(ParNew和ParallelGC和ParallelOldGC)
    * 在串行回收器的基础上进行改进,使用多线程进行垃圾回收,可以有效减少GC实际时间
    * ParNew回收器
    * ParNew回收器是一个工作在新生代的垃圾回收器,它只是简单的将串行回收器多线程化,它的回收策略和算法和串行回收器是一样的
    * 参数配置:
    -XX:+UseParNewGC 新生代ParNew回收器,老年代则使用串行回收器
    -XX:ParallelGCThreads 指定ParNew回收器工作时启用的线程数,[必须合理配置]
    * ParallelGC回收器
    * 新生代Parallel回收器,使用了复制算法的收集器,也是多线程独占形式的收集器,但ParallelGC回收器有个非常重要的特点:它非常关注系统的吞吐量
    * 参数配置:
    -XX:MaxGCPauseMillis
    * 设置最大垃圾收集停顿时间,可以把虚拟机在GC停顿的时间控制在MaxGCPauseMillis范围内
    * 应该合理设置这个参数,这个参数减小会减小GC停顿时间,同时会增加GC次数,从而增加GC总时间,降低了吞吐量
    -XX:GCTimeRatio
    * 设置吞吐量大小,它是一个0到100的整数,默认值为99,那么系统将花费不超过1/(1+n)的时间用于垃圾收集,也就是1/(1 + 99) = 1%的时间
    -XX:+UseAdaptiveSizePolicy
    * 打开自适应模式,在这种模式下,新生代的大小,eden、from/to的比例,以及晋升为老年代对象的年龄参数都会被自动调整,以达到堆大小、吞吐量和停顿事件之间的平衡点
    * ParallelOldGC回收器
    * 老年代ParallelOldGC回收器,也是一种多线程回收器,和新生代ParallelGC回收器一样,也是一种关注吞吐量的回收器,它使用了标记压缩算法实现
    * 参数配置:
    -XX:+UseParallelOldGC 使用老年代ParallelOldGC回收器
    -XX:+ParallelGCThreads 设置垃圾回收时线程数量
    * CMS回收器[标记清除法]
    * Concurrent Mark Sweep 并发标记清除,它使用的是标记清除法,主要关注系统停顿时间
    * 参数配置:
    -XX:+UseConcurrentMarkSweepGC 开启使用
    -XX:ConcGCThreads 设置并发线程数量
    * CMS并不是独占的回收器,也就是说CMS进行垃圾回收的过程中,应用程序仍然在不停的工作,又会有新的垃圾不断的产生,所以在使用CMS的过程中应该确保应用程序的内存足够可用
    * CMS不会等到应用程序饱和的时候才去回收垃圾,而是在某一阀值的时候开始回收,回收阀值可以用以下参数进行配置:
    -XX:CMSInitiatingOccupancyFranction来指定,默认值为68
    * 也就是说,当老年代的空间使用率达到68%的时候,会执行CMS回收
    * 如果内存使用率增长的很快,在CMS过程中,已经出现了内存不足的情况,此时CMS回收就会失败,虚拟机将启动老年代串行回收器进行垃圾回收,这会导致应用程序中断,直到垃圾回收完毕后才继续工作。这个过程GC停顿的时间可能较长,所以该阀值应该合理设置
    * 标记清除法有个大问题是内存碎片的问题,CMS有个参数设置:
    -XX:+UseCMSCompactAtFullCollection 可以使CMS完成垃圾回收会进行一次碎片整理
    -XX:CMSFullGCsBeforeCompaction 设置进行多少次CMS回收之后,对内存进行一次压缩
    * G1回收器[复制算法]--建议别用
    * Garbage-First,属于分代垃圾回收器,区分新生代和老年代,依然由eden区和from/to区,它并不要求eden区或者新生代、老年代的空间都连续,它使用了分区算法
    * 并行性:G1回收期间可多线程同时工作
    * 并发性:G1拥有与应用程序交替执行能力,部分工作可与应用程序同时执行,在整个GC期间不会完全阻塞应用程序
    * 分代GC: G1依然是个分代收集器,但是它是个兼顾新生代和老年代一起工作,之前的垃圾收集器它们或是在新生代工作,或是在老年代工作,因此这是个很大的不同
    * 空间整理:G1在回收过程中不会像CMS那样在若干次GC后需要进行碎片整理,G1采用了有效复制对象的方式,减少空间碎片
    * 可预见性:由于分区的原因,G1可以只选取部分区域进行回收,缩小了回收的范围,提升了性能
    * 参数配置:
    * -XX:+UseG1GC 使用G1收集器
    * -XX:MaxGCPauseMillis 指定最大停顿时间
    * -XX:ParallelGCThreads 设置并行回收的线程数量
    * 建议组合:ParallelGC + ParallelOldGC
    * 提升性能的方式
    * 扩大内存Xmx
    * 调整初始堆大小Xms[增大]
    扩大初始堆大小,则初始堆可以容纳更多对象,则初始堆的GC次数会减小,性能提高
    老年代的大小会变小,存留在老年代的对象存活率更高,也会减小老年代GC的次数,则性能也会提高
    * 更换合理的垃圾收集器,如ParallelGC + ParallelOldGC

    * 锁
    * 对象头Mark
    * Mark Word,对象头的标记,32位
    * 描述对象的hash,锁信息,垃圾回收标记,年龄
    - 指向锁记录的指针
    - 指向monitor的指针
    - GC标记
    - 偏向锁线程ID
    * 偏向锁
    * 大部分情况是没有竞争的,可以通过偏向锁来提高性能
    * 所谓的偏向,就是偏心,即锁会偏向当前已经占有锁的线程
    * 将对象头mark的标记设置为偏向,并将线程id写入对象头mark
    * 只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步
    * 当其他线程请求相同的锁时,偏向模式结束
    * 参数配置: -XX:+UseBiasedLocking 默认启用[biase贝叶斯]
    * 在竞争激烈的场合,偏向锁会增加系统负担
    * 轻量级锁
    * 自旋锁
    * 线程自转,进行空循环
    * 当竞争存在时,如果线程可以很快获得锁,则OS不将它挂起,它原地自旋等待获得锁继续执行
    * 参数配置: -XX:+UseSpinning 开启自旋锁[jdk7以上后无需配置,默认开启]
    * 如果同步块很长,自旋失败,会降低系统性能
    * 如果同步块很短,自旋成功,则省略OS挂起和切换时间,提高性能

    * 锁优化
    * 一、JVM层面
    * 偏向锁可用会先尝试使用偏向锁
    * 轻量级锁可用会先尝试使用轻量级锁
    * 以上都失败,会尝试使用自旋锁
    * 还是失败,则使用普通锁,即OS挂起当前线程
    * 二、Java语言层面
    * 1. 减少锁持有时间[较少同步时间,同时还能提高自旋成功率]
    ** 2. 减小锁粒度 **
    * 将大对象拆分成小对象,大大提高并行度,降低锁竞争
    * 偏向锁、轻量级锁成功率提高
    * 实例: ConcurrentHashMap
    - ConcurrentHashMap内部分了15个segment,Segment<k, v>[] segment
    - Segment对象维护着一个HashEntry<k, v>
    - put操作时
    先定位到Segment,锁定一个Segment,执行put,
    因为锁定的是特定的Segment,对于其他的就不会锁定,支持并发
    - 缺点:[哪些情况下反而不好?影响性能]

    ** 3. 锁分离 **
    * ReadWriteLock: 读读共享、写写互斥、读写互斥
    * 读多写少,可以提高性能
    * 4. 锁粗化
    * 对于频繁使用加锁释放锁但执行时间又不长的代码,进行统一加锁
    for(int i = 0; i < n; i++) 》 synchronized()
    { 》 {
    synchronized(){ 》 for(int i = 0; i < n; i++){

    } 》 }
    } 》 }
    * 5. 锁消除
    * 6. 无锁
    * 乐观的操作
    * 无锁的实现由很多种方式,以下为一种
    - CAS(Compare And Swap)
    - 非阻塞的同步
    - CAS(V,E,N)
    V:要更新的值,E:预估值,N:新值
    当且仅当V=E的时候,将N的值赋值给V,然后返回V[旧值],否则什么都不操作。

    一、
    -Xms@-XX:InitialHeapSize 初始堆内存,默认是物理内存的1/64
    -Xmx@-XX:MaxHeapSize 最大堆内存,默认是物理内存的1/4
    -Xss@-xx:ThreadStackSize 运行时单个线程栈大小,默认值写的是0,是因为它不固定,和环境有关
    -XX:MetaspaceSize 元空间大小,调大,默认是20m

    二、
    一般不需要调整:
    -Xmn 年轻代大小,默认是128k
    -XX:SurvivorRatio 设置eden区和s0/s1区的比例,默认比例是8:1:1,这里写8,如果是4,则表示比例是4:1:1
    -XX:NewRatio 设置新生代和老年代的比例,默认是1:2,这里写2,如果是4,则表示比例是1:4
    -XX:MaxTenuringThreshold 最大老年区的吞吐量,即经过多少次才从年轻代进入老年代,默认是15次,这里写15,备注:取值只能是[0, 15]

    三、
    -XX:+UseSerialGC 串行垃圾回收器
    -XX:+UseParallelGC 并行垃圾回收器

    四、
    启动加载 java
    java -XX:+PrintGCDetails [param,eg:-version] 打印运行GC明细
    java -XX:+PrintFlagsInitial [param,eg:-version] 打印jvm出厂设置
    java -XX:+PrintFlagsFinal [param,eg:-version] 打印修改后的jvm设置,注意:=和=
    java -XX:+PrintCommandLineFlags [param,eg:-version] 打印运行时手工添加的参数,最后一个参数为当前使用的垃圾回收器
    res: -XX:InitialHeapSize=10 -XX:MaxHeapSize=10 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC


    运行查看 jinfo/jps/jstack
    jinfo -flags pid
    jinfo -flag param pid

    jps -l

    jstack pid

  • 相关阅读:
    【转+补充】在OpenCV for Android 2.4.5中使用SURF(nonfree module)
    Delphi StarOffice Framework Beta 1.0 发布
    Angular ngIf相关问题
    angularjs文档下载
    公众号微信支付开发
    公众号第三方平台开发 教程六 代公众号使用JS SDK说明
    公众号第三方平台开发 教程五 代公众号处理消息和事件
    公众号第三方平台开发 教程四 代公众号发起网页授权说明
    公众号第三方平台开发 教程三 微信公众号授权第三方平台
    公众号第三方平台开发 教程二 component_verify_ticket和accessToken的获取
  • 原文地址:https://www.cnblogs.com/qq438649499/p/12111831.html
Copyright © 2011-2022 走看看