我们在学习 java 基础的时候,从宏观上了解了一个类到运行大致是:.java文件通过javac编译器编译得到.class文件,在用到该类时,jvm 会加载该 class 文件,并创建对应的 class 对象,将 class 文件加载到 jvm 的内存当中,这个过程也被称之为类加载过程

下面我们将详细了解这个过程,本篇过长建议先收藏。

1、类加载过程

其实关于类加载过程是分为5个阶段的:

加载,验证,准备,解析,初始化


接下来我们看一下这五个阶段:

1.1加载

JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class 对象。

1.2验证

这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查。

  • 确保二进制字节流格式符合预期(比如说是否以 cafe bene 咖啡北鼻开头)。

  • 是否所有方法都遵守访问控制关键字的限定。

  • 方法调用的参数个数和类型是否正确。

  • 确保变量在使用之前被正确初始化了。

  • 检查变量是否被赋予恰当类型的值。

1.3准备

JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:

public static int value1 = 123;

实际上变量 value1 在准备阶段过后的初始值为 0 而不是 123(如果是String类型,初始值为null),将 value1 赋值为 123 的 putstatic 指令是程序被编译后,存放于类构造器方法之中。但是注意如果声明为:

public static final int value2 = 123;

在编译阶段会为 value2 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 value2 赋值为 123。

也就是,static final 修饰的变量被称作为常量,和类变量不同。常量一旦赋值就不会改变了,所以 value2 在准备阶段的值为 123 而不是 0。

1.4解析

该阶段将常量池中的符号引用转化为直接引用。

what?符号引用,直接引用?

符号引用以一组符号(任何形式的字面量,只要在使用时能够无歧义的定位到目标即可)来描述所引用的目标。

在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如 club.sscai.Test1 类引用了 club.sscai.Test2 类,编译时 Test1 类并不知道 Test2 类的实际内存地址,因此只能使用符号 club.sscai.Test2。

直接引用通过对符号引用进行解析,找到引用的实际内存地址。

1.5初始化

该阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。

上面这段话说得比较抽象,不好理解,我来举个例子。

String niceyoo = new String("感谢关注");

上面这段代码使用了 new 关键字来实例化一个字符串对象,那么这时候,就会调用 String 类的构造方法对 niceyoo 进行实例化,怎么个实例化?就是赋值呗。

本节点总结

其实看完类加载过程,由于大部分偏理论,乏味的同时又很难理解,也不容易记忆。所以将类加载过程结合面试题来进一步扩展,如下:

建议先思考后再看答案

题目一:如下代码中,执行 main 函数会通过编译吗?如果可以通过,打印结果是什么呢?

public class A {

    public static void fun1(){        System.out.println("fun1");    }

    public void fun2(){        System.out.println("fun2");    }

    public static void main(String[] args){        ((A) null).fun1();        ((A) null).fun2();    }

}

答案:首先代码是可以通过编译的,null 可以强制转为任意类型,调用其类中的静态方法 fun1 不报异常,调用其类中的非静态方法 fun2 会报空指针异常。


分析:编译是否正常通过最大的干扰项应该是 null 强转吧,估计有的小伙伴都不一定见过,null 可以被强制类型转换成任意类型的对象,知识点,下次要考。

关于打印结果则主要是类加载过程的考察:当加载类对象时,首先初始化静态属性,然后静态代码块;当实例化对象时,首先执行构造块(直接写在类中的代码块{ xxx }),然后执行构造方法。至于各静态块和静态属性初始化哪个先执行,是按代码的先后顺序。属性、构造块、构造方法之间的执行顺序(但构造块一定会在构造方法前执行),也是按代码的先后顺序。

综上,对象即便被将转为空时,静态方法也是可以被调用的,这也是我们平时在使用一些工具类时,直接通过对象.来访问其方法的原因。


题目二:请指出下面程序的运行结果。

class A {

    static {        System.out.print("1");    }

    public A() {        System.out.print("2");    }}

class B extends A {

    static {        System.out.print("a");    }

    public B() {        System.out.print("b");    }}

public class Hello {

    public static void main(String[] args) {        A ab = new B();        ab = new B();    }}

分析:通过上一题目的分析中,我们可能机智的得到了静态代码块是优于构造方法的执行的,但是这个题目中出现了A\B类的继承关系,所以可能带来困扰,但是没关系,静态代码块就是优于构造方法的,只是父类优先级相对高一级罢了,比如 new B() 会先调用父类 A 的静态代码块,其次是 B 的静态代码块,然后是 A 的构造方法,最后是 B 的构造方法。

汇总:执行顺序是先执行父类的静态代码块,然后执行子类的静态代码块;然后执行父类的非静态代码块,再执行父类的构造方法;之后再执行子类的非静态代码块,再执行子类的构造方法。静态代码块>非静态代码块>构造方法。

再就是对象的创建只会调用一次静态代码块,因为类初始化信息是存在方法区里,当加载类的时候去检查,第二次的时候它会发现已经初始化过了,就不会再执行,所以再去 new B() 的时候,是不会再去打印 1a 的。

如果觉得比较绕,再举个例子,就好比你玩王者荣耀的时候,有个赵云的6元首充礼包,你第一次充钱,创建了这个首充礼包的对象,当你第二次充钱时就不会再有首充礼包了。

答案:1a2b2b

2、类加载器

聊完类加载过程的五个阶段,我们再来看看加载阶段用到的类加载器。

系统运行时,是由类加载器将 .class 文件的二进制数据从外部存储器(如光盘,硬盘)调入内存中,CPU再从内存中读取指令和数据进行运算,并将运算结果存入内存中的,显然类加载器是很重要的第一步。


一般来说,Java 程序员并不需要直接同类加载器进行交互。JVM 默认的行为就已经足够满足大多数情况的需求了。不过,如果遇到了需要和类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就不得不花大量的时间去调试 ClassNotFoundException 和 NoClassDefFoundError 等异常。

对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 equals)。

Java 类加载器可以分为三种:

1)启动类加载器(Bootstrap Class-Loader),加载 jre/lib 包下面的 jar 文件,比如说常见的 rt.jar。

启动类加载器主要加载的是JVM自身需要的类,这个类加载使用 C++ 语言实现的,是虚拟机自身的一部分,它负责将 /lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

2)扩展类加载器(Extension or Ext Class-Loader),加载 jre/lib/ext 包下面的 jar 文件。

扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

3)应用类加载器(Application or App Clas-Loader),根据程序的类路径(classpath)来加载 Java 类。

也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。

来来来,通过一段简单的代码了解下。

public class Test1 {

    public static void main(String[] args){        ClassLoader currentLoader = Test.class.getClassLoader();        System.out.println(currentLoader.toString());        ClassLoader parentLoader = currentLoader.getParent();        System.out.println(parentLoader.toString());        ClassLoader parentParentLoader = parentLoader.getParent();        System.out.println(parentParentLoader);    }}

每个 Java 类都维护着一个指向定义它的类加载器的引用,通过类名.class.getClassLoader()可以获取到此引用;然后通过.getParent()可以获取类加载器的上层类加载器。

这段代码的输出结果如下:

sun.misc.Launcher$AppClassLoader@18b4aac2sun.misc.Launcher$ExtClassLoader@4554617cnull

第一行输出为 Test 的类加载器,即应用类加载器,它是sun.misc.Launcher$AppClassLoader类的实例;第二行输出为扩展类加载器,是sun.misc.Launcher$ExtClassLoader类的实例。那启动类加载器呢?按理说,扩展类加载器的上层类加载器是启动类加载器,但在我这个版本的 JDK 中, 扩展类加载器的 getParent() 返回 null。

在 Java 的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,需要注意的是,Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的 class 文件加载到内存生成 class 对象,而且加载某个类的 class 文件时, Java 虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,下面我们进一步了解它。

别放弃,加油!

3、双亲委派模型


双亲委派模式是在 Java 1.2 后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的实力坑爹啊?那么采用这种模式有啥用呢?

双亲委派模式优势

采用双亲委派模式的是好处是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一次。

其次是考虑到安全因素,java 核心 api 中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的 java.lang.Integer,而直接返回已加载过的 Integer.class,这样便可以防止核心API库被随意篡改。

可能你会想,如果我们在 classpath 路径下自定义一个名为 java.lang.SingleInterge 类(该类是胡编的)呢?该类并不存在 java.lang 中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为 java.lang 是核心 API 包,需要访问权限,强制加载将会报出如下异常:

java.lang.SecurityException: Prohibited package name: java.lang

文字内容太乏味,上个例子吧,我们通过自定义类加载器去证实双亲委派模式。

先简单了解一下这个类加载器的主要方法:

loadClass:该方法中的逻辑就是双亲委派模式的实现,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载。

findClass:findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。

defineClass:通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。

resolveClass:使用该方法可以使用类的Class对象创建完成也同时被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。

自定义类加载器 MyClassLoader :

package club.sscai.test7;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.io.InputStream;

/*** author:  niceyoo* blog:    https://cnblogs.com/niceyoo* desc:    自定义类加载器*/public class MyClassLoader extends ClassLoader {

    private String path;/* 加载器的路径 */    private String name;/* 类加载器名称 */

    public MyClassLoader(String path,String name){        super();/* 让起同类加载器成为该类的父加载器 */        this.name = name;        this.path = path;    }

    /**     * 父类加载器构造方法     * @param parent     * @param path     * @param name     */    public MyClassLoader(ClassLoader parent,String path,String name){        super(parent);/* 显示指定父类加载器 */        this.name = name;        this.path = path;    }

    /**     * 加载我们自己定义的类,通过我们自定义的这个 ClassLoader     * 例如:club.sscai.test7.Demo     * @param name     * @return     * @throws ClassNotFoundException     */    @Override    protected Class> findClass(String name) throws ClassNotFoundException {        /* 读取 class 文件,转换成二进制数组 */        byte[] data = readClassFileToByteArray(name);        return this.defineClass(name,data,0,data.length);    }

    @Override    public String toString() {        return this.name;    }

    /**     * 获取 .class 字节数组     * 【读取 class 文件,将类转换成二进制数组】     *  club.sscai.test7.Demo >     *  F:/idea_workspace/test/Demo.class     * @param name     * @return     */    private byte[] readClassFileToByteArray(String name) {

        InputStream is = null;        byte[] returnData = null;        name = name.replace("\\.","/");        String filePath = this.path + name + ".class";        File file = new File(filePath);

        ByteArrayOutputStream os = new ByteArrayOutputStream();

        try {            is = new FileInputStream(file);            int tep = 0;            while ((tep = is.read()) != -1){                os.write(tep);            }            returnData = os.toByteArray();        } catch (Exception e) {            e.printStackTrace();        } finally {            try {                is.close();                os.close();            } catch (IOException e) {                e.printStackTrace();            }        }        return returnData;    }

    @Override    protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {        return super.loadClass(name, resolve);    }}

当前项目位于 F:\idea_workspace\Demo


当前项目中的干扰项 Demo.java:

package club.sscai.test7;

/*** author:  niceyoo* blog:    https://cnblogs.com/niceyoo* desc:    干扰项Demo*/public class Demo {    public Demo() {        System.out.println("我是父加载器加载的Demo:"+Demo.class.getClassLoader());    }}

存放在F:/idea_workspace/test/目录下的 Demo.java,注意如下代码需要通过 javac 编译成 Demo.class


public class Demo {    public Demo(){        System.out.println("Demo:" + this.getClass().getClassLoader());    }}

测试代码 TestDemo.java 如下:

public class TestDemo {    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {        /* 参数一为读取class路径,参数二为自定义类加载器名称 */        MyClassLoader xwLoader = new MyClassLoader("F:/idea_workspace/test/","xiaowang");        Class> demo = xwLoader.loadClass("Demo");        demo.newInstance();    }}

执行 main 方法后会打印什么呢?

Demo:xiaowang

我们改一下测试代码:

public class TestDemo {

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {        MyClassLoader xwLoader = new MyClassLoader("F:/idea_workspace/test/","xiaowang");        Class> demo = xwLoader.loadClass("club.sscai.test7.Demo");        demo.newInstance();    }}

再次执行会打印什么呢?

我是父加载器加载的Demo:sun.misc.Launcher$AppClassLoader@18b4aac2

显然第二次并没有加载  F:/idea_workspace/test/目录下的 Demo,而是执行了当前项目中的 Demo,为什么?

这就是双亲委派模式,由于当前启动类 TestDemo 的父级是 AppClassLoader,显然该包下已经加载过 Demo 类了,所以不会再去加载目标 Demo

4、热部署与热加载(扩展)

上边算是说了一堆理论吧,热部署、热加载则算是实际应用了,相信这两者应该并不陌生,或多或少的应该也有所了解吧。

热加载的实现原理主要依赖java的类加载机制,在实现方式可以概括为在容器启动的时候起一条后台线程,定时的检测类文件的时间戳变化,如果类的时间戳变掉了,则将类重新载入。

热部署原理类似,但它是直接重新加载整个应用,这种方式会释放内存,比热加载更加干净彻底,但同时也更费时间。

简单总结一下两者的区别与联系:

Java热部署与热加载的联系

  1. 不重启服务器编译/部署项目
  2. 基于Java的类加载器实现
Java热部署与热加载的区别:
  • 部署方式               -- 热部署在服务器运行时重新部署项目               -- 热加载在运行时重新加载class
  • 实现原理               -- 热部署直接重新加载整个应用               -- 热加载在运行时重新加载class
  • 使用场景               -- 热部署更多的是在生产环境使用               -- 热加载则更多的是在开发环境使用
想要实现热部署可以分以下三个步骤:
  1. 销毁该自定义ClassLoader
  2. 更新class类文件
  3. 创建新的ClassLoader去加载更新后的class类文件。
相关代码:

User没有被修改类:

public class User {

 public void add() {  System.out.println("addV1,没有修改过..."); }

}

User更新类

public class User {

 public void add() {  System.out.println("我把之前的user add方法修改啦!"); }

}

自定义类加载器:

public class MyClassLoader extends ClassLoader {

 @Override protected Class> findClass(String name) throws ClassNotFoundException {  try {   /* 文件名称 */   String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";   /* 获取文件输入流 */   InputStream is = this.getClass().getResourceAsStream(fileName);   /* 读取字节 */   byte[] b = new byte[is.available()];   is.read(b);   /* 将byte字节流解析成jvm能够识别的Class对象 */   return defineClass(name, b, 0, b.length);  } catch (Exception e) {   throw new ClassNotFoundException();  }

 }

}

更新代码:

public class Hotswap {

     public static void main(String[] args)throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException,               SecurityException, IllegalArgumentException, InvocationTargetException, InterruptedException {

          loadUser();

          System.gc();

          Thread.sleep(1000);/* 等待资源回收 */

          /* 需要被热部署的class文件  */

          File file1 = new File("F:\\test\\User.class");

          /* 之前编译好的class文件  */

          File file2 = new File(

                    "F:\\idea_workspace\\target\\classes\\club\\sscai\\User.class");

           /* 删除旧版本的class文件 */          boolean isDelete = file2.delete();

          if (!isDelete) {

               System.out.println("热部署失败.");

               return;

          }

          file1.renameTo(file2);

          System.out.println("update success!");

          loadUser();

     }

     public static void loadUser() throws ClassNotFoundException, InstantiationException, IllegalAccessException,               NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException {

          MyClassLoader myLoader = new MyClassLoader();

          Class> class1 = myLoader.findClass("club.sscai.User");

          Object obj1 = class1.newInstance();

          Method method = class1.getMethod("add");

          method.invoke(obj1);

          System.out.println(obj1.getClass());

          System.out.println(obj1.getClass().getClassLoader());

     }

}

5、最后

本篇有点过长了,其实大致看下来,类加载无非也就那么回事。

类加载机制:JVM 将类的信息动态添加到内存并使用的一种机制。

---END---

推荐阅读:为什么有这么多领优惠劵的群?

喜欢文章,点个在看 

system类的 静态方法可以启动垃圾回收器。_跟小伟一起学习类加载机制相关推荐

  1. system类的 静态方法可以启动垃圾回收器。_Java—System类入门学习

    第三阶段 JAVA常见对象的学习 System类 System类包含一些有用的字段和方法,他不能被实例化 //用于垃圾回收public static void gc()​//终止正在运行的java虚拟 ...

  2. Java基础知识强化83:System类之gc()方法(垃圾回收)以及和finalize()区别

    1. System概述: System类包含一些有用的类字段和方法.它不能被实例化. 2. gc()方法:垃圾回收器 1 public static void gc()       调用gc方法暗示着 ...

  3. 谈谈System类,再细细品味 垃圾回收机制System.gc()

    目录 一.嵌套类汇总 1.为什么需要有垃圾回收机制? 2.System.gc(); 3.finalize()方法 System类:系统类,主要用于获取系统的属性数据,没有构造方法. System类包含 ...

  4. 面向对象,类,对象,GC垃圾回收器,private关键字

    一.面向对象 是一种编程思想 1.三大特征 封装.继承.多态 二.类 class 类是一类事物的抽象 属性 --事物的特征 方法 --事物的行为 三.对象 是一类事物中具体的个体 对象类型 对象名 = ...

  5. c++ 多线程 垃圾回收器_新一代垃圾回收器ZGC的探索与实践

    很多低延迟高可用Java服务的系统可用性经常受GC停顿的困扰,作为新一代的低延迟垃圾回收器,ZGC在大内存低延迟服务的内存管理和回收方面,有着非常不错的表现. 本文从GC之痛.ZGC原理.ZGC调优实 ...

  6. java hotspot 默认垃圾回收器_怎么查看服务器默认的垃圾的收集器是哪个?生产环境上如何配置垃圾回收收集器?谈谈你对垃圾收集器的理解?...

    上篇:https://zhuanlan.zhihu.com/p/165998261​zhuanlan.zhihu.com 一.查看默认的垃圾收集器 1.如何查看默认的垃圾收集器 (1)代码演示: pa ...

  7. java不同垃圾回收器_细述 Java垃圾回收机制→Types of Java Garbage Collectors

    本文非原创,翻译自Types of Java Garbage Collectors 在Java中为对象分配和释放内存空间都是由垃圾回收线程自动执行完成的.和C语言不一样的是Java程序员不需要手动写垃 ...

  8. 垃圾回收算法以及垃圾回收器_什么是垃圾回收?

    垃圾回收算法以及垃圾回收器 以下是我们的垃圾收集手册中的一个示例,该手册将在接下来的几周内发布. 同时,花点时间熟悉垃圾收集的基础知识-这将是本书的第一章. 乍一看,垃圾收集应该处理顾名思义的问题–查 ...

  9. c++ 多线程 垃圾回收器_并行并发CMS垃圾回收器:-XX:+UseConcMarkSweepGC

    概述 CMS为基于标记清除算法实现的多线程老年代垃圾回收器.CMS为响应时间优先的垃圾回收器,适合于应用服务器,如网络游戏,电商等和电信领域的应用. 为了实现这个目的,与其他垃圾回收器不同的是,CMS ...

最新文章

  1. 在ML中缺乏数据可是个大问题,亲测有效的5种方法帮您解决
  2. 2019ICPC(南京) - super_log(欧拉降幂)
  3. 小程序richtext_用于基于SWT的应用程序的RichText编辑器组件
  4. 贺利坚老师汇编课程52笔记:汇编语言模块化程序设计
  5. 关于Linux进程优先级数字混乱的彻底澄清
  6. (转)神秘的比特币地址详解
  7. 学习笔记五:xss.tv通关笔记
  8. QCC3024/QCC3034/QCC3020/QCC3021/QCC3031 USB AUDIO USB声卡 双向动态切换功能
  9. ios签名php在线监控,IOS无需签名无需越狱H5网页在线封装APP教程
  10. 为了防止世界被破坏,春运的抢票攻略在等着你们
  11. 最新八个免费Logo设计工具灵感网站,帮你搞定logo设计难题!
  12. 奥斯汀大学计算机专业怎么样,德克萨斯大学奥斯汀分校计算机工程排名
  13. mac 远程连接win7桌面
  14. 中转网关 (Transit Gateway) Connect连接类型集成FortiGate安全服务
  15. 学习PPT与Excel的各种高级应用并掌握相关技巧
  16. Android Toolbar设置向上箭头,标题等
  17. VS/MFC - 无法启动此程序因为计算机中丢失mfc140d.dll.
  18. 通信工程与计算机考研学校排名,2019-2020信息与通信工程专业考研学校排名
  19. 微信支付服务商模式说明
  20. 基于客户端与服务器的管理系统,基于客户端和服务器的点菜系统.doc

热门文章

  1. python 创建工具包_使用Python工具建立网站
  2. scratch绘制多边形_如何使用Scratch 3绘制矢量图形
  3. opensource项目_最佳Opensource.com:艺术与设计
  4. duckduckgo 国内_DuckDuckGo的Instant Answers项目的7课
  5. 第十一章:李淳风的秘谋
  6. node全局对象 文件系统
  7. linux从Mac下载文件,如何将命令的输出保存到Bash中的文件(也称为Linux和macOS终端) | MOS86...
  8. 视觉SLAM笔记(12) 四元数
  9. sql like 多个条件_都9012年啦,不懂得这些SQL语句优化,你是要吃大亏的
  10. 24-[模块]-re