• Java进阶之JVM实战


    1. 使用自定义Classloader机制,实现xlass的加载

    1.1 类加载流程

    BootStrap 加载路径

    System.getProperty("sun.boot.class.path")
    
    输出结果如下:
    xxx/jre/lib/resources.jar: 
    xxx/jre/lib/rt.jar: 
    xxx/jre/lib/sunrsasign.jar: 
    xxx/jre/lib/jsse.jar: 
    xxx/jre/lib/jce.jar: 
    xxx/jre/lib/charsets.jar: 
    xxx/jre/lib/jfr.jar: 
    xxx/jre/classes
    

    ExtClassLoader 加载路径

    System.getProperty("java.ext.dirs")
    
    输出结果如下:
    /Users/xxx/Library/Java/Extensions:
    xxx/jre/lib/ext:
    /Library/Java/Extensions:
    /Network/Library/Java/Extensions:
    /System/Library/Java/Extensions:
    /usr/lib/java
    

    AppClassLoader 加载路径

    System.out.println("自定义类加载路径:");
    System.out.println(AXClassLoader.class.getClassLoader());
    System.out.println("对应Parent ClassLoader:");
    System.out.println(AXClassLoader.getSystemClassLoader().getParent());
    System.out.println("对应父类的父类 ClassLoader:");
    System.out.println(AXClassLoader.getSystemClassLoader().getParent().getParent());
    

    1.2 Java Resource 路径

    private static void printResourcePath() {
        System.out.println("AXClassLoader.class.getResource("")):
    " + AXClassLoader.class.getResource(""));
        System.out.println("AXClassLoader.class.getResource("/")):
    " + AXClassLoader.class.getResource("/"));
        System.out.println("AXClassLoader.class.getClassLoader().getResource("")):
    " + AXClassLoader.class.getClassLoader().getResource(""));
        System.out.println("ClassLoader.getSystemResource("))
    " + ClassLoader.getSystemResource(""));
        System.out.println("Thread.currentThread().getContextClassLoader().getResource("")
    " + Thread.currentThread().getContextClassLoader().getResource(""));
    }
    
    输出结果:
    AXClassLoader.class.getResource("")):
    file:/xxx/java-p7-in-action/jvm-base/target/classes/com/holddie/jvm/classloader/v1/
    
    AXClassLoader.class.getResource("/")):
    file:/xxx/java-p7-in-action/jvm-base/target/classes/
    
    AXClassLoader.class.getClassLoader().getResource("")):
    file:/xxx/java-p7-in-action/jvm-base/target/classes/
    
    ClassLoader.getSystemResource("))
    file:/xxx/java-p7-in-action/jvm-base/target/classes/
    
    Thread.currentThread().getContextClassLoader().getResource("")
    file:/xxx/java-p7-in-action/jvm-base/target/classes/
    

    1.3 实现 AXClassLoader 定义

    public class AXClassLoader extends ClassLoader {
    
      @Override
      protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
          byte[] bytes = Files.readAllBytes(Paths.get(getFileName(name)));
          for (int i = 0; i < bytes.length; i++) {
            bytes[i] = (byte) (255 - bytes[i]);
          }
          return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
          e.printStackTrace();
        }
        return super.findClass(name);
      }
    
      private String getFileName(String name) {
        int index = name.lastIndexOf('.');
        if (index == -1) {
          return ClassLoader.getSystemResource("").getPath() + name + ".xlass";
        } else {
          return ClassLoader.getSystemResource("").getPath() + name.substring(index + 1) + ".xlass";
        }
      }
    }
    

    1.4 调用加载

    AXClassLoader axClassLoader = new AXClassLoader();
    try {
        Class<?> hello = axClassLoader.loadClass("Hello");
        if (hello != null) {
            Object obj = hello.newInstance();
            Method method = hello.getDeclaredMethod("hello");
            method.invoke(obj);
        }
    } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException | InstantiationException | NoSuchMethodException e) {
        e.printStackTrace();
    }
    

    2. 实现xlass打包的xar(类似class文件打包的jar)的加载

    2.1 生成 xar 包

    private static void createHelloXar() {
      try {
        XarSink xarSink = new XarSink();
        final XarFileSource fileSource;
        final File fileToCompress = getClasspathResourceAsFile(CLASS_FILE_NAME);
        assert fileToCompress != null;
        fileSource = new XarFileSource(fileToCompress);
        xarSink.addSource(fileSource);
        URL resource = Thread.currentThread().getContextClassLoader().getResource("");
        assert resource != null;
        xarSink.write(FileUtils.openOutputStream(new File(resource.getPath(), XAR_FILE_NAME)));
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
    

    2.2 解析 xar 包

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
      try {
        XarSource xar = new FileXarSource(getClasspathResourceAsFile(name));
        XarEntry entry = xar.getEntry(CLASS_FILE_NAME);
        byte[] bytes = entry.getBytes();
        for (int i = 0; i < bytes.length; i++) {
          bytes[i] = (byte) (255 - bytes[i]);
        }
        return defineClass(CLASS_NAME, bytes, 0, bytes.length);
      } catch (IOException | URISyntaxException e) {
        e.printStackTrace();
      }
      return super.findClass(name);
    }
    

    2.3 加载 xar 包

    /*
     *自定义加载 xar 文件
     */
    AXClassLoader axClassLoader = new AXClassLoader();
    try {
      Class<?> hello = axClassLoader.loadClass(XAR_FILE_NAME);
      if (hello != null) {
        Object obj = hello.newInstance();
        Method method = hello.getDeclaredMethod(METHOD_NAME);
        method.invoke(obj);
      }
    } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException | InstantiationException | NoSuchMethodException e) {
      e.printStackTrace();
    }
    

    3. 基于自定义Classloader实现类的动态加载和卸载

    3.1 理论支撑

    如何实现类的卸载,需要满足哪些条件:

    • 该类的所有实例对象不可达
    • 该类的Class对象不可达
    • 该类的ClassLoader不可达

    使用什么方式监控JVM的加载和卸载过程?

    • -verbose:class :同时追踪类的加载和卸载
    • -XX:+TraceClassLoading:单独跟踪类的加载
    • -XX:+TranceUnloading:单独跟踪累的卸载

    类加载器特征

    • 每个ClassLoader都维护了一份自己的名称空间,同一个名称空间里不能出现两个同名的类;
    • 为了实现Java安全沙箱模型的类加载安全机制,Java默认采用了“双亲委派的加载链”结构;

    3.2 动态加载

    这里我们清楚,类的动态加载,或者说热加载,就是我们程序从一个固定路径读取 class 文件,同样类名称,前后修改方法实现,使用替换手法,就可以立即使用替换类的方法。

    public class AXClassLoader extends ClassLoader {
      /**
        * 基础包路径
        */
      private String basePackagePath;
      /**
        * 需要该类加载器直接加载的类文件的基目录
        */
      private String basedir;
      /**
        * 需要由该类加载器直接加载的类名
        */
      private HashSet<Object> dynaclazns;
    
      public AXClassLoader(String basedir, String[] clazns, String packagePath) {
        super(null);
        this.basePackagePath = packagePath;
        this.basedir = basedir;
        dynaclazns = new HashSet<>();
        loadClassByMe(clazns);
      }
    
      @SneakyThrows
      private void loadClassByMe(String[] clazns) {
        for (int i = 0; i < clazns.length; i++) {
          String fileNamePath = getFileName(clazns[i]);
          byte[] bytes = Files.readAllBytes(Paths.get(fileNamePath));
          defineClass(this.basePackagePath + "." + clazns[i], bytes, 0, bytes.length);
          dynaclazns.add(clazns[i]);
        }
      }
    
      private String getFileName(String name) {
        int index = name.lastIndexOf('.');
        if (index == -1) {
          return this.basedir + name + ".class";
        } else {
          return this.basedir + name.substring(index + 1) + ".class";
        }
      }
    
      @Override
      protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class cls = findLoadedClass(name);
        if (!this.dynaclazns.contains(name) && cls == null) {
          cls = getSystemClassLoader().loadClass(name);
        }
        if (cls == null) {
          throw new ClassNotFoundException(name);
        }
        if (resolve) {
          resolveClass(cls);
        }
        return cls;
      }
    
      public static void main(String[] args) {
        String packagePath = "com.holddie.jvm.classloader.v3";
        new Timer("timer - 1")
          .schedule(
          new TimerTask() {
            @Override
            public void run() {
              try {
                // 每次都创建出一个新的类加载器
                AXClassLoader cl =
                  new AXClassLoader(
                  ClassLoader.getSystemResource("").getPath()
                  + "v3/",
                  new String[] {"Foo"},
                  packagePath);
                Class cls = cl.loadClass(packagePath + ".Foo");
                Object foo = cls.newInstance();
    
                Method m = foo.getClass().getMethod("sayHello", new Class[] {});
                m.invoke(foo, new Object[] {});
              } catch (Exception ex) {
                ex.printStackTrace();
              }
            }
          },
          2000,
          3000);
      }
    }
    
    

    对应这里,我们使用 resources 文件夹下的 v3_2 的 Foo.class 文件替换之前文件夹下的 Foo.class 文件。注意我们为什么要使用一个 Schedule 定时加载,主要目的模拟程序在频繁类加载,方便前后两次热替换之后,方法输出结果展示;

    3.3 卸载

    上述介绍了满足类卸载的三个条件,code show as flow:

    public static void main(String[] args) {
      String packagePath = "com.holddie.jvm.classloader.v3";
      new Timer("timer - 1")
        .schedule(
        new TimerTask() {
          @Override
          public void run() {
            try {
              AXClassLoader cl =
                new AXClassLoader(
                ClassLoader.getSystemResource("").getPath()
                + "v3/",
                new String[] {"Foo"},
                packagePath);
              Class cls = cl.loadClass(packagePath + ".Foo");
              Object foo = cls.newInstance();
    
              Method m = foo.getClass().getMethod("sayHello", new Class[] {});
              m.invoke(foo, new Object[] {});
    
              foo = null;
              cls = null;
              cl = null;
              System.gc();
              System.out.println("GC over");
            } catch (Exception ex) {
              ex.printStackTrace();
            }
          }
        },
        2000,
        3000);
    }
    

    此处注意我们,将对应的引用都置为空,之后手动执行了 System.gc() 方法,对此我们要观察类的加载和卸载需要在程序启动的时候设置JVM参数 -verbose:class 即可看到对应 loading 和 unloading 日志;

    4. 基于自定义 Classloader 实现模块化机制:需要设计模块化机制。

    主要思想:

    • 已经熟悉基于一个类的加载和卸载,在实际使用原理上大同小异;
    • 一个基础模块,提供抽象定义;
    • 两个实现模块,每个模块内部都有自己 module.properties 文件定义自己模块版本、模块入口等,以作为生成jar包运行使用;
    • 一个运行模块,使用两种模块化加载方式;

    4.1 multi-skd 基础模块

    定义模块抽象方法

    public interface IFun {
      void sayHello(String name);
    }
    

    4.2 multi-fz 实现模块

    依赖基础模块,fz 自定义实现

    public class FzFun implements IFun {
      @Override
      public void sayHello(String name) {
        System.out.println("Hi " + name + ", i am FzFun");
      }
    }
    

    模块元数据声明

    # 位置在 resources/META-INF/module.properties 
    module.appCode=fz
    module.jarVersion=1.0
    module.fun.location=com.holddie.jvm.FzFun
    

    4.3 multi-tz 实现模块

    依赖基础模块,tz 自定义实现类

    public class TzFun implements IFun {
      @Override
      public void sayHello(String name) {
        System.out.println("Hi " + name + ", i am TzFun");
      }
    }
    

    模块元数据声明

    # 位置在 resources/META-INF/module.properties 
    module.appCode=tz
    module.jarVersion=1.0
    module.fun.location=com.holddie.jvm.TzFun
    

    4.4 multi-server 运行动态加载模块

    使用两种方式,一种是JDK基于接口的动态代理,一种原生反射实现方法调用,详情在第五小节;

    5. 使用 jar 作为模块,实现xar动态加载和卸载,综合应用前面的内容;

    我们首先需要把实现模块的两个Jar包,打包OK,为下文加载提供方便,此处我们同时加载两个模块,并且指定了加载jar的名称,也可以使用自定义的目录区分;

    5.1 ModuleClassLoader

    public class ModuleClassLoader extends URLClassLoader {
    
      private final List<String> excludePackages;
    
      public ModuleClassLoader(URL[] urls, List<String> excludePackages) {
        super(urls);
        this.excludePackages = excludePackages;
      }
    
      @Override
      public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (isExcludePackage(name)) {
          return super.loadClass(name);
        }
        synchronized (getClassLoadingLock(name)) {
          Class cls = findLoadedClass(name);
          if (cls == null) {
            try {
              cls = findClass(name);
            } catch (ClassNotFoundException e) {
              e.printStackTrace();
            }
          }
          if (cls != null) {
            return cls;
          }
        }
        return super.loadClass(name);
      }
    
      private boolean isExcludePackage(String name) {
        if (StringUtils.isEmpty(name)) {
          return false;
        }
        return this.excludePackages.stream().anyMatch(name::startsWith);
      }
    }
    

    5.2 DynamicProxy

    public class DynamicProxy implements InvocationHandler {
    
        private Object target;
    
        public Object bind(Object target) {
            this.target = target;
            return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Long start = System.currentTimeMillis();
            Object result = method.invoke(target, args);
            Long end = System.currentTimeMillis();
            String format = String.format("执行: class => %s, 方法=> %s, 参数 => %s, 耗时 => %sms, 执行结果 => %s, 当前路径 => %s",
                    target.getClass().getName(),
                    method.getName(),
                    JSON.toJSON(args),
                    end - start,
                    JSON.toJSON(result),
                    target.getClass().getResource("").getPath());
            System.out.println(format);
            return result;
        }
    }
    

    5.3 ModuleJar

    public class ModuleJar {
        private String jarVersion;
        private String appCode;
        private String moduleJarUrl;
        private ModuleClassLoader moduleClassLoader;
        private String moduleFunLocation;
    
        public String getJarVersion() {
            return jarVersion;
        }
    
        public void setJarVersion(String jarVersion) {
            this.jarVersion = jarVersion;
        }
    
        public String getAppCode() {
            return appCode;
        }
    
        public void setAppCode(String appCode) {
            this.appCode = appCode;
        }
    
        public String getModuleJarUrl() {
            return moduleJarUrl;
        }
    
        public void setModuleJarUrl(String moduleJarUrl) {
            this.moduleJarUrl = moduleJarUrl;
        }
    
        public ModuleClassLoader getModuleClassLoader() {
            return moduleClassLoader;
        }
    
        public void setModuleClassLoader(ModuleClassLoader moduleClassLoader) {
            this.moduleClassLoader = moduleClassLoader;
        }
    
        public String getModuleFunLocation() {
            return moduleFunLocation;
        }
    
        public void setModuleFunLocation(String moduleFunLocation) {
            this.moduleFunLocation = moduleFunLocation;
        }
    }
    

    5.4 MultiModuleLoaderMain

    public class MultiModuleLoaderMain {
      private final static String APP_CODE = "module.appCode";
      private final static String JAR_VERSION = "module.jarVersion";
      private final static String MODULE_FUN_LOCATION = "module.fun.location";
    
      public static void main(String[] args) {
    
        HashMap<String, ModuleJar> moduleCache = new HashMap<String, ModuleJar>();
        List<String> excludePackages = Arrays.asList("java", "com.holddie.jvm.sdk");
        List<String> jarUrls = Arrays.asList(
          "file:" + ClassLoader.getSystemResource("").getPath() + "multi-fz-1.0.0-jar-with-dependencies.jar",
          "file:" + ClassLoader.getSystemResource("").getPath() + "multi-tz-1.0.0-jar-with-dependencies.jar");
        jarUrls.forEach(url -> {
          try {
            URL moduleJarUrl = new URL(url);
            ModuleClassLoader moduleClassLoader = new ModuleClassLoader(new URL[]{moduleJarUrl}, excludePackages);
            Properties properties = getProperties(moduleClassLoader.getResourceAsStream("META-INF/module.properties"));
            ModuleJar moduleJar = new ModuleJar();
            moduleJar.setAppCode(properties.getProperty(APP_CODE));
            moduleJar.setJarVersion(properties.getProperty(JAR_VERSION));
            moduleJar.setModuleFunLocation(properties.getProperty(MODULE_FUN_LOCATION));
            moduleJar.setModuleClassLoader(moduleClassLoader);
            moduleJar.setModuleJarUrl(moduleJarUrl.getPath());
            moduleCache.put(moduleJar.getAppCode(), moduleJar);
          } catch (MalformedURLException e) {
            e.printStackTrace();
          }
        });
    
        ModuleJar fzModuleJar = moduleCache.get("fz");
        ModuleJar tzModuleJar = moduleCache.get("tz");
    
        try {
          ModuleClassLoader fzModuleJarModuleClassLoader = fzModuleJar.getModuleClassLoader();
          Object fzInstance = fzModuleJarModuleClassLoader.loadClass(fzModuleJar.getModuleFunLocation()).newInstance();
          Method fzSayHello = fzInstance.getClass().getMethod("sayHello", new Class[]{String.class});
          fzSayHello.invoke(fzInstance, new Object[]{"4456"});
          fzSayHello = null;
          fzInstance = null;
          fzModuleJarModuleClassLoader = null;
          fzModuleJar = null;
          moduleCache.put("fz", null);
          System.gc();
    
          IFun tzFun = (IFun) new DynamicProxy().bind(tzModuleJar.getModuleClassLoader().loadClass(tzModuleJar.getModuleFunLocation()).newInstance());
          tzFun.sayHello("123");
          tzFun.toString();
        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException e) {
          e.printStackTrace();
        }
        System.out.println("----------------------");
        System.out.println();
      }
    
      public static Properties getProperties(InputStream inputStream) {
        Properties properties = new Properties();
        try {
          properties.load(inputStream);
        } catch (IOException e) {
          e.printStackTrace();
        }
        return properties;
      }
    }
    

    5.6 运行日志

    [Opened /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded java.io.Serializable from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded java.lang.Comparable from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded java.lang.CharSequence from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded java.lang.String from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded java.lang.reflect.AnnotatedElement from 
    [Loaded java.util.stream.Sink from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded java.util.stream.MatchOps$BooleanTerminalSink from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded java.util.function.Supplier from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded java.util.stream.MatchOps$$Lambda$3/511754216 from java.util.stream.MatchOps]
    [Loaded java.util.stream.MatchOps$1MatchSink from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded com.holddie.jvm.sdk.IFun from file:/xxx/java-p7-in-action/jvm-base/modularization/multi-sdk/target/classes/]
    [Loaded com.holddie.jvm.FzFun from file:/xxx/java-p7-in-action/jvm-base/modularization/multi-server/target/classes/multi-fz-1.0.0-jar-with-dependencies.jar]
    
    Hi 4456, i am FzFun
     
    [Unloading class com.holddie.jvm.FzFun 0x00000007c0086828]
    [Loaded java.lang.reflect.InvocationHandler from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded com.holddie.jvm.DynamicProxy from file:/xxx/java-p7-in-action/jvm-base/modularization/multi-server/target/classes/]
    [Loaded com.holddie.jvm.TzFun from file:/xxx/java-p7-in-action/jvm-base/modularization/multi-server/target/classes/multi-tz-1.0.0-jar-with-dependencies.jar]
    [Loaded java.lang.reflect.Proxy from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded java.lang.reflect.WeakCache from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    .....
    [Loaded java.lang.reflect.WeakCache$CacheValue from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded java.lang.reflect.UndeclaredThrowableException from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
     
    Hi 123, i am TzFun
    
    [Loaded com.alibaba.fastjson.JSONStreamAware from file:/Users/zeyangg/.m2/repository/com/alibaba/fastjson/1.2.4/fastjson-1.2.4.jar]
    [Loaded com.alibaba.fastjson.JSONAware from file:/xxx/.m2/repository/com/alibaba/fastjson/1.2.4/fastjson-1.2.4.jar]
    [Loaded java.util.HashMap$KeyIterator from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded com.alibaba.fastjson.serializer.ListSerializer from file:/Users/zeyangg/.m2/repository/com/alibaba/fastjson/1.2.4/fastjson-1.2.4.jar]
    [Loaded com.alibaba.fastjson.serializer.SerialContext from file:/Users/zeyangg/.m2/repository/com/alibaba/fastjson/1.2.4/fastjson-1.2.4.jar]
    执行: class => com.holddie.jvm.TzFun, 方法=> sayHello, 参数 => ["123"], 耗时 => 0ms, 执行结果 => null, 当前路径 => /xxx/java-p7-in-action/jvm-base/modularization/multi-server/target/classes/com/holddie/jvm/
    执行: class => com.holddie.jvm.TzFun, 方法=> toString, 参数 => null, 耗时 => 0ms, 执行结果 => com.holddie.jvm.TzFun@6ed3ef1, 当前路径 => /xxx/java-p7-in-action/jvm-base/modularization/multi-server/target/classes/com/holddie/jvm/
    ----------------------
    
    [Loaded sun.misc.VMSupport from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded java.util.Hashtable$KeySet from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded sun.nio.cs.ISO_8859_1$Encoder from /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/rt.jar]
    

    注意,日志中的一些路径,自己做了一些脱敏使用 /xxx 代替,自己在看的时候,切勿当真以为是 /xxx,主要想表达的是一种数据脱敏;

    最终代码参考实现:https://github.com/HoldDie/java-p7-in-action/tree/master/jvm-base/

    参考链接

  • 相关阅读:
    C数据结构2.1-线性表抽象数据类型
    转载的内容
    转载springboot的内容
    jQuery中的load()Failed to load resource: the server responded with a status of 404 Maven框架遇到的问题
    java代码发送邮箱源代码
    Error:(1, 10) java: 需要class, interface或enum的错误
    性能测试系列五 压测常见的关注指标以及监控分析工具
    面试官常考的Selenium Web自动化面试题总结(上篇)
    性能测试系列四 压测指标的来源
    性能测试系列三 压测方式简单总结
  • 原文地址:https://www.cnblogs.com/holddie/p/java-jin-jie-zhijvm-shi-zhan.html
Copyright © 2020-2023  润新知