zoukankan      html  css  js  c++  java
  • JVM系列(五)

    前言

    本文将由浅及深,介绍Java类加载的过程和原理,进一步对类加载器的进行源码分析,完成一个自定义的类加载器。

    正文

    (一). 类加载器是什么

    类加载器简言之,就是用于把.class文件中的字节码信息转化为具体的java.lang.Class对象的过程的工具。

    具体过程:

    1. 在实际类加载过程中,JVM会将所有的.class字节码文件中的二进制数据读入内存中,导入运行时数据区的方法区中。
    2. 当一个类首次被主动加载被动加载时,类加载器会对此类执行类加载的流程 – 加载连接验证准备解析)、初始化
    3. 如果类加载成功,堆内存中会产生一个新的Class对象,Class对象封装了类在方法区内的数据结构

    Class对象的创建过程描述:

    (二). 类加载的过程

    类加载的过程分为三个步骤(五个阶段) :加载 -> 连接验证准备解析)-> 初始化

    加载验证准备初始化这四个阶段发生的顺序是确定的,而解析阶段可以在初始化阶段之后发生,也称为动态绑定晚期绑定

    类加载的过程描述:

    1. 加载

    加载:查找并加载类的二进制数据的过程。

    加载的过程描述:

    1. 通过类的全限定名定位.class文件,并获取其二进制字节流
    2. 把字节流所代表的静态存储结构转换为方法区运行时数据结构
    3. Java中生成一个此类的java.lang.Class对象,作为方法区中这些数据的访问入口

    2. 连接

    连接:包括验证准备解析三步。

    a). 验证

    验证:确保被加载的类的正确性。验证是连接阶段的第一步,用于确保Class字节流中的信息是否符合虚拟机的要求。

    具体验证形式:

    1. 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
    2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
    3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
    4. 符号引用验证:确保解析动作能正确执行。

    b). 准备

    准备:为类的静态变量分配内存,并将其初始化为默认值。准备过程通常分配一个结构用来存储类信息,这个结构中包含了类中定义的成员变量方法接口信息等。

    具体行为:

    1. 这时候进行内存分配的仅包括类变量(static),而不包括实例变量实例变量会在对象实例化时随着对象一块分配在Java中。
    2. 这里所设置的初始值通常情况下是数据类型默认的零值(如00Lnullfalse等),而不是被在Java代码中被显式赋值

    c). 解析

    解析:把类中对常量池内的符号引用转换为直接引用

    解析动作主要针对类或接口字段类方法接口方法方法类型方法句柄调用点限定符等7类符号引用进行。

    3. 初始化

    初始化:对类静态变量赋予正确的初始值 (注意和连接时的解析过程区分开)。

    初始化的目标

    1. 实现对声明类静态变量时指定的初始值的初始化;
    2. 实现对使用静态代码块设置的初始值的初始化。

    初始化的步骤

    1. 如果此类没被加载连接,则先加载连接此类;
    2. 如果此类的直接父类还未被初始化,则先初始化其直接父类;
    3. 如果类中有初始化语句,则按照顺序依次执行初始化语句。

    初始化的时机

    1. 创建类的实例(new关键字);
    2. java.lang.reflect包中的方法(如:Class.forName(“xxx”));
    3. 对类的静态变量进行访问或赋值;
    4. 访问调用类的静态方法
    5. 初始化一个类的子类父类本身也会被初始化;
    6. 作为程序的启动入口,包含main方法(如:SpringBoot入口类)。

    (三). 类的主动引用和被动引用

    主动引用

    主动引用:在类加载阶段,只执行加载连接操作,不执行初始化操作。

    主动引用的几种形式

    1. 创建类的实例(new关键字);
    2. java.lang.reflect包中的方法(如:Class.forName(“xxx”));
    3. 对类的静态变量进行访问或赋值;
    4. 访问调用类的静态方法
    5. 初始化一个类的子类父类本身也会被初始化;
    6. 作为程序的启动入口,包含main方法(如:SpringBoot入口类)。

    主动引用1 - main方法在初始类中

    代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class OptimisticReference0 {
    static {
    System.out.println(OptimisticReference0.class.getSimpleName() + " is referred!");
    }

    public static void main(String[] args) {
    System.out.println();
    }
    }

    运行结果:

    OptimisticReference0 is referred!

    主动引用2 – 创建子类会触发父类的初始化

    代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class OptimisticReference1 {
    public static class Parent {
    static {
    System.out.println(Parent.class.getSimpleName() + " is referred!");
    }
    }

    public static class Child extends Parent {
    static {
    System.out.println(Child.class.getSimpleName() + " is referred!");
    }
    }

    public static void main(String[] args) {
    new Child();
    }
    }

    运行结果:

    Parent is referred!
    Child is referred!

    主动引用3 – 访问一个类静态变量

    代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class OptimisticReference2 {
    public static class Child {
    protected static String name;
    static {
    System.out.println(Child.class.getSimpleName() + " is referred!");
    name = "Child";
    }
    }

    public static void main(String[] args) {
    System.out.println(Child.name);
    }
    }

    运行结果:

    Child is referred!
    Child

    主动引用4 – 对类的静态变量进行赋值

    代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class OptimisticReference3 {
    public static class Child {
    protected static String name;
    static {
    System.out.println(Child.class.getSimpleName() + " is referred!");
    }
    }

    public static void main(String[] args) {
    Child.name = "Child";
    }
    }

    运行结果:

    Child is referred!

    主动引用5 – 使用java.lang.reflect包提供的反射机制

    代码示例:

    1
    2
    3
    4
    5
    public class OptimisticReference4 {
    public static void main(String[] args) throws ClassNotFoundException {
    Class.forName("org.ostenant.jdk8.learning.examples.reference.optimistic.Child");
    }
    }

    运行结果:

    Child is referred!

    被动引用

    被动引用: 在类加载阶段,会执行加载连接初始化操作。

    被动引用的几种形式:

    1. 通过子类引用父类的的静态字段,不会导致子类初始化;
    2. 定义类的数组引用不赋值,不会触发此类的初始化;
    3. 访问类定义的常量,不会触发此类的初始化。

    被动引用1 – 子类引用父类的的静态字段,不会导致子类初始化

    代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class NegativeReference0 {
    public static class Parent {
    public static String name = "Parent";
    static {
    System.out.println(Parent.class.getSimpleName() + " is referred!");
    }
    }

    public static class Child extends Parent {
    static {
    System.out.println(Child.class.getSimpleName() + " is referred!");
    }
    }

    public static void main(String[] args) {
    System.out.println(Child.name);
    }
    }

    运行结果:

    Parent is referred!
    Parent

    被动引用2 – 定义类的数组引用而不赋值,不会触发此类的初始化

    代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class NegativeReference1 {
    public static class Child {
    static {
    System.out.println(Child.class.getSimpleName() + " is referred!");
    }
    }

    public static void main(String[] args) {
    Child[] childs = new Child[10];
    }
    }

    运行结果:

    无输出

    被动引用3 – 访问类定义的常量,不会触发此类的初始化

    示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class NegativeReference2 {
    public static class Child {
    public static final String name = "Child";
    static {
    System.out.println(Child.class.getSimpleName() + " is referred!");
    }
    }

    public static void main(String[] args) {
    System.out.println(Child.name);
    }
    }

    运行结果:

    Child

    (四). 三种类加载器

    类加载器:类加载器负责加载程序中的类型(类和接口),并赋予唯一的名字予以标识。

    类加载器的组织结构

    类加载器的关系

    1. Bootstrap Classloader 是在Java虚拟机启动后初始化的。
    2. Bootstrap Classloader 负责加载 ExtClassLoader,并且将 ExtClassLoader的父加载器设置为 Bootstrap Classloader
    3. Bootstrap Classloader 加载完 ExtClassLoader 后,就会加载 AppClassLoader,并且将 AppClassLoader 的父加载器指定为 ExtClassLoader

    类加载器的作用

    Class Loader实现方式具体实现类负责加载的目标
    Bootstrap Loader C++ 由C++实现 %JAVA_HOME%/jre/lib/rt.jar以及-Xbootclasspath参数指定的路径以及中的类库
    Extension ClassLoader Java sun.misc.Launcher$ExtClassLoader %JAVA_HOME%/jre/lib/ext路径下以及java.ext.dirs系统变量指定的路径中类库
    Application ClassLoader Java sun.misc.Launcher$AppClassLoader Classpath以及-classpath-cp指定目录所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器

    类加载器的特点

    • 层级结构:Java里的类装载器被组织成了有父子关系的层级结构。Bootstrap类装载器是所有装载器的父亲。
    • 代理模式: 基于层级结构,类的代理可以在装载器之间进行代理。当装载器装载一个类时,首先会检查它在父装载器中是否进行了装载。如果上层装载器已经装载了这个类,这个类会被直接使用。反之,类装载器会请求装载这个类
    • 可见性限制:一个子装载器可以查找父装载器中的类,但是一个父装载器不能查找子装载器里的类。
    • 不允许卸载:类装载器可以装载一个类但是不可以卸载它,不过可以删除当前的类装载器,然后创建一个新的类装载器装载。

    类加载器的隔离问题

    每个类装载器都有一个自己的命名空间用来保存已装载的类。当一个类装载器装载一个类时,它会通过保存在命名空间里的类全局限定名(Fully Qualified Class Name) 进行搜索来检测这个类是否已经被加载了。

    JVM 及 Dalvik 对类唯一的识别是 ClassLoader id + PackageName + ClassName,所以一个运行程序中是有可能存在两个包名类名完全一致的类的。并且如果这两个不是由一个 ClassLoader 加载,是无法将一个类的实例强转为另外一个类的,这就是 ClassLoader 隔离性。

    为了解决类加载器的隔离问题JVM引入了双亲委托机制

    (五). 双亲委托机制

    核心思想:其一,自底向上检查类是否已加载;其二,自顶向下尝试加载类

    具体加载过程

    1. AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派父类加载器ExtClassLoader去完成。
    2. ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派BootStrapClassLoader去完成。
    3. 如果BootStrapClassLoader加载失败(例如在%JAVA_HOME%/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
    4. 如果ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException

    源码分析

    ClassLoader.class

    1. loadClass():通过指定类的全限定名称,由类加载器检测装载创建并返回该类的java.lang.Class对象。

      ClassLoader通过loadClass()方法实现了双亲委托机制,用于类的动态加载

    loadClass()本身是一个递归向上调用的过程。

    • 自底向上检查类是否已加载

      1. 先通过findLoadedClass()方法从最底端类加载器开始检查类是否已经加载。
      2. 如果已经加载,则根据resolve参数决定是否要执行连接过程,并返回Class对象。
      3. 如果没有加载,则通过parent.loadClass()委托其父类加载器执行相同的检查操作(默认不做连接处理)。
      4. 直到顶级类加载器,即parent为空时,由findBootstrapClassOrNull()方法尝试到Bootstrap ClassLoader中检查目标类。
    • 自顶向下尝试加载类

      1. 如果仍然没有找到目标类,则从Bootstrap ClassLoader开始,通过findClass()方法尝试到对应的类目录下去加载目标类。
      2. 如果加载成功,则根据resolve参数决定是否要执行连接过程,并返回Class对象。
      3. 如果加载失败,则由其子类加载器尝试加载,直到最底端类加载器也加载失败,最终抛出ClassNotFoundException
    1. findLoadedClass()

      查找当前类加载器的缓存中是否已经加载目标类。findLoadedClass()实际调用了底层的native方法findLoadedClass0()

    1. findBootstrapClassOrNull()

      查找最顶端Bootstrap类加载器的是否已经加载目标类。同样,findBootstrapClassOrNull()实际调用了底层的native方法findBootstrapClass()

    1. findClass()

      ClassLoaderjava.lang包下的抽象类,也是所有类加载器(除了Bootstrap)的基类,findClass()ClassLoader对子类提供的加载目标类的抽象方法。

      注意Bootstrap ClassLoader并不属于JVM的层次,它不遵守ClassLoader的加载规则,Bootstrap classLoader并没有子类。

    1. defineClass()

      defineClass()ClassLoader向子类提供的方法,它可以将.class文件的二进制数据转换为合法的java.lang.Class对象。

    (六). 类的动态加载

    类的几种加载方式

    • 通过命令行启动时由JVM初始化加载;
    • 通过Class.forName()方法动态加载;
    • 通过ClassLoader.loadClass()方法动态加载。

    Class.forName()和ClassLoader.loadClass()

    • Class.forName():把类的.class文件加载到JVM中,对类进行解释的同时执行类中的static静态代码块
    • ClassLoader.loadClass():只是把.class文件加载到JVM中,不会执行static代码块中的内容,只有在newInstance才会去执行。

    (七). 对象的初始化

    对象的初始化顺序

    静态变量/静态代码块 -> 普通代码块 -> 构造函数

    1. 父类静态变量静态代码块(先声明的先执行);
    2. 子类静态变量静态代码块(先声明的先执行);
    3. 父类普通成员变量普通代码块(先声明的先执行);
    4. 父类的构造函数
    5. 子类普通成员变量普通代码块(先声明的先执行);
    6. 子类的构造函数

    对象的初始化示例

    Parent.java

    Children.java

    Tester.java

    测试结果:

    测试结果表明:JVM在创建对象时,遵守以上对象的初始化顺序。

    (八). 自定义类加载器

    编写自己的类加载器

    在源码分析阶段,我们已经解读了如何实现自定义类加载器,现在我们开始自己的类加载器。

    Step 1:定义待加载的目标类Parent.javaChildren.java

    Parent.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    package org.ostenant.jdk8.learning.examples.classloader.custom;

    public class Parent {
    protected static String CLASS_NAME;
    protected static String CLASS_LOADER_NAME;
    protected String instanceID;

    // 1.先执行静态变量和静态代码块(只在类加载期间执行一次)
    static {
    CLASS_NAME = Parent.class.getName();
    CLASS_LOADER_NAME = Parent.class.getClassLoader().toString();
    System.out.println("Step a: " + CLASS_NAME + " is loaded by " + CLASS_LOADER_NAME);
    }

    // 2.然后执行变量和普通代码块(每次创建实例都会执行)
    {
    instanceID = this.toString();
    System.out.println("Step c: Parent instance is created: " + CLASS_LOADER_NAME + " -> " + instanceID);
    }

    // 3.然后执行构造方法
    public Parent() {
    System.out.println("Step d: Parent instance:" + instanceID + ", constructor is invoked");
    }

    public void say() {
    System.out.println("My first class loader...");
    }
    }

    Children.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package org.ostenant.jdk8.learning.examples.classloader.custom;

    public class Children extends Parent {
    static {
    CLASS_NAME = Children.class.getName();
    CLASS_LOADER_NAME = Children.class.getClassLoader().toString();
    System.out.println("Step b: " + CLASS_NAME + " is loaded by " + CLASS_LOADER_NAME);
    }

    {
    instanceID = this.toString();
    System.out.println("Step e: Children instance is created: " + CLASS_LOADER_NAME + " -> " + instanceID);
    }

    public Children() {
    System.out.println("Step f: Children instance:" + instanceID + ", constructor is invoked");
    }

    public void say() {
    System.out.println("My first class loader...");
    }
    }

    Step 2:实现自定义类加载器CustomClassLoader

    CustomClassLoader.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public class CustomClassLoader extends ClassLoader {
    private String classPath;

    public CustomClassLoader(String classPath) {
    this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    Class<?> c = findLoadedClass(name); // 可省略
    if (c == null) {
    byte[] data = loadClassData(name);
    if (data == null) {
    throw new ClassNotFoundException();
    }
    return defineClass(name, data, 0, data.length);
    }
    return null;
    }

    protected byte[] loadClassData(String name) {
    try {
    // package -> file folder
    name = name.replace(".", "//");
    FileInputStream fis = new FileInputStream(new File(classPath + "//" + name + ".class"));
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    int len = -1;
    byte[] b = new byte[2048];
    while ((len = fis.read(b)) != -1) {
    baos.write(b, 0, len);
    }
    fis.close();
    return baos.toByteArray();
    } catch (IOException e) {
    e.printStackTrace();
    }
    return null;
    }
    }

    Step 3:测试类加载器的加载过程

    CustomerClassLoaderTester.java

    1. 测试程序启动时,逐一拷贝加载待加载的目标类源文件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      private static final String CHILDREN_SOURCE_CODE_NAME = SOURCE_CODE_LOCATION + "Children.java";
      private static final String PARENT_SOURCE_CODE_NAME = SOURCE_CODE_LOCATION + "Parent.java";
      private static final List<String> SOURCE_CODE = Arrays.asList(CHILDREN_SOURCE_CODE_NAME, PARENT_SOURCE_CODE_NAME);

      static {
      SOURCE_CODE.stream().map(path -> new File(path))
      // 路径转文件对象
      .filter(f -> !f.isDirectory())
      // 文件遍历
      .forEach(f -> {
      // 拷贝后源代码
      File targetFile = copySourceFile(f);
      // 编译源代码
      compileSourceFile(targetFile);
      });
      }
    2. 拷贝单一源文件到自定义类加载器的类加载目录

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      protected static File copySourceFile(File f) {
      BufferedReader reader = null;
      BufferedWriter writer = null;
      try {
      reader = new BufferedReader(new FileReader(f));
      // package ...;
      String firstLine = reader.readLine();

      StringTokenizer tokenizer = new StringTokenizer(firstLine, " ");
      String packageName = "";
      while (tokenizer.hasMoreElements()) {
      String e = tokenizer.nextToken();
      if (e.contains("package")) {
      continue;
      } else {
      packageName = e.trim().substring(0, e.trim().length() - 1);
      }
      }

      // package -> path
      String packagePath = packageName.replace(".", "//");
      // java file path
      String targetFileLocation = TARGET_CODE_LOCALTION + "//" + packagePath + "//";

      String sourceFilePath = f.getPath();
      String fileName = sourceFilePath.substring(sourceFilePath.lastIndexOf("\") + 1);

      File targetFile = new File(targetFileLocation, fileName);
      File targetFileLocationDir = new File(targetFileLocation);
      if (!targetFileLocationDir.exists()) {
      targetFileLocationDir.mkdirs();
      }
      // writer
      writer = new BufferedWriter(new FileWriter(targetFile));
      // 写入第一行
      writer.write(firstLine);
      writer.newLine();
      writer.newLine();

      String input = "";
      while ((input = reader.readLine()) != null) {
      writer.write(input);
      writer.newLine();
      }

      return targetFile;
      } catch (FileNotFoundException e) {
      e.printStackTrace();
      } catch (IOException e) {
      e.printStackTrace();
      } finally {
      try {
      reader.close();
      writer.close();
      } catch (IOException e) {
      e.printStackTrace();
      }
      }
      return null;
      }
    3. 对拷贝后的.java源文件执行手动编译,在同级目录下生成.class文件。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      protected static void compileSourceFile(File f) {
      try {
      JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
      StandardJavaFileManager standardFileManager = javaCompiler.getStandardFileManager(null, null, null);
      Iterable<? extends JavaFileObject> javaFileObjects = standardFileManager.getJavaFileObjects(f);

      // 执行编译任务
      CompilationTask task = javaCompiler.getTask(null, standardFileManager, null, null, null, javaFileObjects);
      task.call();
      standardFileManager.close();

      } catch (Exception e) {
      e.printStackTrace();
      }
      }
    4. 通过自定义类加载器加载Childrenjava.lang.Class<?>对象,然后用反射机制创建Children的实例对象。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      @Test
      public void test() throws Exception {
      // 创建自定义类加载器
      CustomClassLoader classLoader = new CustomClassLoader(TARGET_CODE_LOCALTION); // E://myclassloader//classpath
      // 动态加载class文件到内存中(无连接)
      Class<?> c = classLoader.loadClass("org.ostenant.jdk8.learning.examples.classloader.custom.Children");
      // 通过反射拿到所有的方法
      Method[] declaredMethods = c.getDeclaredMethods();
      for (Method method : declaredMethods) {
      if ("say".equals(method.getName())) {
      // 通过反射拿到children对象
      Object children = c.newInstance();
      // 调用children的say()方法
      method.invoke(children);
      break;
      }
      }
      }

    测试编写的类加载器

    (一). 测试场景一

    1. 保留static代码块,把目标类Children.javaParent.java拷贝到类加载的目录,然后进行手动编译
    2. 保留测试项目目录中的目标类Children.javaParent.java

    测试结果输出:

    测试结果分析:

    我们成功创建了Children对象,并通过反射调用了它的say()方法。
    然而查看控制台日志,可以发现类加载使用的仍然是AppClassLoaderCustomClassLoader并没有生效。

    查看CustomClassLoader的类加载目录:

    类目录下有我们拷贝编译ParentChidren文件。

    分析原因:

    由于项目空间中的Parent.javaChildren.java,在拷贝后并没有移除。导致AppClassLoader优先在其Classpath下面找到并成功加载了目标类。

    (二). 测试场景二

    1. 注释掉static代码块(类目录下有已编译的目标类.class文件)。
    2. 移除测试项目目录中的目标类Children.javaParent.java

    测试结果输出:

    测试结果分析:

    我们成功通过自定义类加载器加载了目标类。创建了Children对象,并通过反射调用了它的say()方法。

    至此,我们自己的一个简单的类加载器就完成了!

    参考书籍

    周志明,深入理解Java虚拟机:JVM高级特性与最佳实践,机械工业出版社


    欢迎关注技术公众号: 零壹技术栈

    零壹技术栈零壹技术栈

    本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。

  • 相关阅读:
    MyEclipse 2014 破解图文详细教程
    Eclipse构建Maven项目
    Eclipse构建Maven项目
    Spring--环境配置
    Spring--环境配置
    Sencha Touch2 -- 11.1:定义具有关联关系的模型
    Sencha Touch2 -- 11.1:定义具有关联关系的模型
    Android百度定位API的使用
    Android百度定位API的使用
    Java基础学习总结(64)——Java内存管理
  • 原文地址:https://www.cnblogs.com/ostenant/p/9695247.html
Copyright © 2011-2022 走看看