JVM类加载

文章目录

  • 类加载
    • 1.类的生命周期
      • 加载阶段
      • 连接阶段
      • 初始化阶段
    • 2.类加载器
      • 类加载器的分类
        • 启动类加载器(Bootstarp)
        • 扩展类加载器&应用程序加载器
    • 3.类的双亲委派机制
      • 什么是双亲委派机制?
      • 打破双亲委派机制
        • 自定义类加载器
        • 线程上下文类加载器


类加载

注:本文章基于JDK1.8

1.类的生命周期

在Java中一个类的生命周期为:加载、连接、初始化、使用、卸载5个步骤

加载阶段

在加载阶段会创建两个对象一个Java.lang.class和instanceKlass对象

  1. 第一步

    • 加载阶段第一步是类加载器根据类的全限定名(包名+类名)通过不同的渠道以二进制的方式获取字节码的信息
    • 程序员可以使用Java代码拓展不同的渠道,如磁盘上的字节码文件,动态代理生成(程序运行时使用动态代理生成)等
  2. 第二步

    • 类加载器加载完类之后,Java虚拟机会将字节码文件中的信息保存到方法区中
    • 生成一个InstanceKlass对象,保存类的所有信息,里面还包含特定功能比如多态的信息

    在这里插入图片描述

    • 同时,Java虚拟机还会在堆中生成一份与方法区中数据类似的java.lang.Class对象,作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK8及之后)

    在这里插入图片描述

    • 对于开发者来说,只需要访问堆中的Class对象而不需要访问方法区中的所有信息。这样Java虚拟机就能很好的控制开发者访问数据的范围,instanceKlass对象是使用C/C++编写的Java程序是无法直接访问的,而堆区的Java.lang.class,是把instanceKlass对象进行了封装,可以让Java程序直接访问,且只包含一些开发者会使用的到的信息,去除了一些不需要的信息。

      在这里插入图片描述

连接阶段

连接阶段又分为验证、准备、解析三个步骤

  1. 验证阶段

    • 这个环节主要的目的是验证Java字节码文件是否遵循了《Java虚拟机规范》中的约束,这个阶段一般不需要程序员参与。

    • 比如说验证字节码内容中的魔术也就是文件头是否Java字节码的文件头,以及主次版本号是否满足当前Java虚拟机的要求,主版本号不能高于运行环境主版本号,如果主版本号相等,副版本号也不能超过

    • 还有就是原信息验证,比如一个类必须有父类,也就是super不能为空

      在这里插入图片描述

    • 验证程序执行指令的语义,方法内的指令执行中是否跳转到不正确的位置,比如方法内的指令执行到一半强行跳转到其他方法中

    • 符号引用验证,例如是否访问了其它类中private的方法等

  2. 准备阶段

    public class Demo {public static int num = 10;public static final VALUE = 100;
    }
    
    • 准备阶段为静态变量(static)分配内存并设置赋初始值,静态变量是存在于堆区的class对象中,并为其赋默认值0,b比如说上诉中的num就会将其先赋值为0,注意这里并不是赋值为10,而是0。
    • 而被final修改的静态变量则会直接在准备阶段将其赋值为其设置的值,因为这个值是不会改变的,例如上述代码中的VALUE会被赋值为100。因为这个值被final修饰,编译就会认为这个值以后是不会被改变的
    • 为静态变量赋初值,是防止静态变量指向的内存地址中有残留的数据,为了避免出现随机值。

    而静态变量的初始值,处理boolean的默认值为false,char的默认值为'\u0000',引用类型为null,其它基本类都为0

  3. 解析阶段

    • 将常量池中的符号引用替换成指向内存的直接引用,也就是将编号索引直接替换成内存地址的引用
    • 符号引用就是字节码文件中使用编号来访问常量池中的内容
    • 直接引用不再使用编号,而是使用内存中地址进行访问具体的数据

初始化阶段

  • 初始化阶段会执行静态代码块中的代码,并为静态变量赋值
  • 初始化阶段执行字节码中clinit部分的字节码执行

一下几种方式会导致类初始化:

  1. 访问一个类的静态变量或者静态方法,需要注意的是变量是final修饰的并且等号右边是常量不会触发初始化
  2. 调用Class.forName(String className)
  3. new一个该类的对象时
  4. 执行Main方法的当前类(也就是执行main方法的类)

如下代码value的值为1,因为静态代码的执行顺序是和编写顺序一致的

public class Demo1 {static {value = 2;}public static int value = 1;public static void main(String[] args) {}
}

上面代码的字节码

在这里插入图片描述

如下代码:

public class Test {public static void main(String[] args) {System.out.println("A");new Test();new Test();}public Test() {System.out.println("B");}{System.out.println("C");}static {System.out.println("D");}
}

最后输出:DACBCB,在类初始化的时候会执行静态代码块static,而普通代码块代码最后编译后指令会放在构造方法中,也就是比构造方法先执行,最后再执行构造方法。

clinit指令在特定情况下不会出现,比如:如下几种情况是不会进行初始化指令执行的

  1. 无静态代码块并且无静态变量赋值语句
  2. 有静态变量的声明,但是没有赋值语句
  3. 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化

需要注意的是访问父类的静态变量,只初始化父类

下面代码输出1,因为访问父类的静态变量只会初始化父类

public class Demo {public static void main(String[] args) {System.out.println(B.a);}
}
class A {static int a = 0;static {a = 1;}
}
class B extends A {static {a = 2;}
} 

数组的创建不会导致数组中元素的类进行初始化,代码如下:创建这个Test数组并不会导致类初始化。也就是不会执行Test类中的静态代码块

public class Demo1 {public static void main(String[] args) {Test[] test = new Test[10];}
}class Test{static {System.out.println("1_1");}
}

final修饰的变量如果赋值的内容需要执行指令才能得出结果,会执行clinit方法进行初始化,下面代码则会对Test类进行初始化,并执行静态代码块

public class Demo1 {public static void main(String[] args) {System.out.println(Test.a);}
}class Test{public static final int a = Integer.valueOf(1);static {System.out.println("Test静态代码块");}
}     

2.类加载器

类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术,类加载器只参与加载过程中的字节码获取并加载到内存这一部分。

类加载器的分类

类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的。

  • 虚拟机底层实现:源代码位于Java虚拟机的源码中,实现语言宇虚拟机底层语言一致,比如Hotspot使用C++实现
  • JDK中默认提供或者自定义:JDK中默认提供了多种处理不同渠道的类加载器,程序员也可以自己根据需求定制

JDK8及以前的版本中默认的类加载器有如下几种

  • 虚拟机底层使用C++实现

    • 启动类加载器(Bootstrap):加载Java中最核心的类
  • Java代码实现

    • 扩展类加载器(Extension):允许拓展Java中比较通用的类
    • 应用程序类加载器(Application):加载应用使用的类
启动类加载器(Bootstarp)

启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供、使用C++编写的类加载器。

默认加载Java安装目录/jre/lib下的类文件,通过一下代码尝试获取到启动类加载器。下面的代码打印为null,因为启动类加载器底层是C++实现的,而Java是上层的代码,为了安全考虑Java代码中是或不到底层的类加载器的。

public static void main(String[] args) {ClassLoader classLoader = String.class.getClassLoader();System.out.println(classLoader);
}

所以当获取到的类加载器为空的时候,其实应该就是启动类加载器。

如果我们想扩展jar包使用启动类加载器进行加载,是不建议直接将jar包放入/jre/lib下的,这可能会导致放进去由于文件名不匹配的问题也不会正常地被加载,建议使用一下方法:

  • 使用-Xbootclasspath/a:jar包目录/jar包名 进行扩展
扩展类加载器&应用程序加载器

扩展类加载器和应用程序类加载器都是JDK提供的,使用Java编写的类加载器。它们的源码都位于sun.misc.Launcher中,是一个静态内部类。继承自URLClassLoader。具备通过目录或者指定jar包将字节码文件加载到内存中。

  • 扩展类加载器默认加载Java安装目录/jre/lib/ext下的类文件
  • 应用程序类加载器加载classpath下的类文件

在这里插入图片描述

  • URLClassLoader: 利用URL获取目录下或者指定的jar包进行加载,获取其字节码数据
  • SecureClassLoader:使用证书机制提升类加载的安全性
  • ClassLoader:抽象类,定义了类加载器的具体行为模式,通过JNI调用底层的Java虚拟机方法

3.类的双亲委派机制

什么是双亲委派机制?

由于Java虚拟机中有多个类加载器,双亲委派机制的核心就是解决一个类到底由谁加载的问题。

双亲委派机制有什么作用?

  1. 保证类加载的安全性:通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性安全性
  2. 避免重复加载:双亲委派机制避免同一个类被多次加载

在类加载的过程中,每个类加载器都会先检查是否已经加载了该类,如果已经加载则直接返回,否则会将加载请求委派给父类加载器 。如果一个类加载器 的 parent为null,则会提交给启动类加载器加载

每个Java实现的类加载器中保存了一个成员变量叫parent的双亲加载器,,可以理解为它的上级,并不是继承关系。

在这里插入图片描述

  • 应用程序类加载器的parent父类加载器是扩展类的加载器,而扩展类加载器的parent是空的
  • 启动类加载器是使用C++编写,没有上级类加载器

在这里插入图片描述

双亲委派机制指的是:自底向上查找是否加载过,再由顶向下进行加载

  • 当程序需要进行加载时,首先是从应用程序加载器检查这个类是否被加载,如果没有就会让拓展类加载器进行检查,依此类推,如果这个类被加载过就会直接返回
  • 启动类加载器检查玩这个类也没有被加载时,就会判断这个类是否在它的加载列表中,如果是就直击加载,如果不存在就让下一级检测是否能被加载
  • 如果在加载过程中这3个类加载器都无法去加载这个类,最后就会出现一个类无法找到的错误

小结:

  1. 当一个类加载器去加载某个类的时候,会自底向上查找是否加载过,如果加载过就直接返回,如果一直到最顶层的类加载器都没有加载,再由顶向下进行加载
  2. 应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加
    载器。
  3. 双亲委派机制的好处有两点: 第一是避免恶意代码替换JDK中的核心类库,比java.lang.String,确保核心类库的完整性和安全性。第二是避免一个类重复地被加载

打破双亲委派机制

自定义类加载器

在这里插入图片描述

  • 一个Tomcat程序中是可以运行多个Web应用的,如果这两个应用中出现了相同限定名的类,比如Servlet类,
    Tomcat要保证这两个类都能加载并且它们应该是不同的类。
  • 如果不打破双亲委派机制, 当应用类加载器加载Web应用1中的MyServlet之后, Web应用2中相同限定名的MyServlet类就无法被加载了。
  • Tomcat使用了自定义类加载器来实现应用之间类的隔离,每一个应用会有一个独立的类加载器加载对应的类。

在这里插入图片描述

  • 先来分析ClassLoader的原理,ClassLoader中包含了4个核心方法。
  • 双亲委派机制的核心代码就位于loadClass方法中
//类加载的入口,提供了双亲委派机制。内部会调用 findClass
public Class<?> loadClass(String name)
// 由类加载器子类实现,获取二进制数据调用defineClass ,比如URLClassLoader会根据文件路径去获取类文件中的二进制数据
protected Class<?> findClass(String name)
// 做一些类名的校验,然后调用虚拟机底层的方法将字节码信息加载到虚拟机内存中
protected final Class<?> defineClass(String name,byte[] b,int off, int len)
//执行类生命周期中的连接阶段
protected final void resolveClass(Class<?> c)

来看一下loadClass方法的源码,首先是执行入口方法的内部调用的其重载的loadClass方法,传递了一个false参数,该参数表示 是否执行连接这个过程,默认为false也就是不会执行连接这个过程

public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);
}

接下来看一下调用的重载方法

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{// 类加载之前先加锁synchronized (getClassLoadingLock(name)) {// 检测当前类是否已经被加载过,如果被加载过,就直接返回Class对象Class<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {// 判断父类加载器是否为空if (parent != null) {// 如果父类加载器不为空,就会由父类加载器去调用对应的loadClass方法c = parent.loadClass(name, false);} else {// 如果parent为空说明此时是扩展类加载器,那么就会调用启动类加载器来进行加载,也就是Bootstrapc = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}// 如果此时还为空,说明该类此时并没有被加载过,就会被当前类加载器进行加载if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}

通过上述的源代码来看,其实双亲委派的核心代码就是这么一段:

// 判断父类加载器是否为空
if (parent != null) {// 如果父类加载器不为空,就会由父类加载器去调用对应的loadClass方法c = parent.loadClass(name, false);
} else {// 如果parent为空说明此时是扩展类加载器,那么就会调用启动类加载器来进行加载,也就是Bootstrapc = findBootstrapClassOrNull(name);
}// 如果此时还为空,说明该类此时并没有被加载过,就会被当前类加载器进行加载
if (c == null) {c = findClass(name)
}

所以想要打破双亲委派机制,就可以实现一个自定义的类加载器,也就是 自定义一个类继承ClassLoader,并重写loadClass方法,将其的双亲委派代码给去除掉,这样就打破了双亲委派机制

如果只是实现一个自定义的类加载器,而不需要打破双亲委派机制,就可以重写findClass方法,因为一个类在加载的时候首先是由其加载器的父类加载器进行加载,当父类没有加载时,最后则会调用findClass方法来进行加载,我们就可以重写findClass方法来进行自定义加载,而且不会 打破双亲委派机制

注意:如果一个自定义类加载器不知道父类加载器,默认的父类加载器就是应用程序类加载器

在这里插入图片描述

两个自定义类加载器加载相同限定名的类,是不会冲突的。在同一个Java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类。

线程上下文类加载器

在JDBC中,不希望在代码中出现某一种数据库的语法,他要提高它的通用性,让其对接任何数据库都非常方便。在JDBC中使用了DriverManager来管理项目中引入的不同数据库的驱动,使用不同的数据库,只需要添加对应的数据库驱动即可。DriverManager会把jar包中对应的驱动加载进来,如MySQL驱动。

DriverManager类位于rt.jar包中,由启动类加载器加载。 而用户jar包中的MySQL驱动需要由应用程序类加载器进行加载,就违反了双亲委派机制。

DriverManager类会先由启动类加载器进行加载后,启动类加载器会委派应用程序类加载器去加载Jar包中的MySQL驱动,而正常情况是由下向上委派,而且启动类加载器比应用程序类加载器高的多,这样就打破了双亲委派机制。

在这里插入图片描述

DriverManager怎么知道jar包中要加载的驱动在哪儿?

  • SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,如果你想去加载一个接口的实现类对象,就可以通过SPI快速的去找到
  • SPI的工作原理:
    • 在ClassPath路径下META-INF/services文件夹中,以接口的全限定名来命名文件名,对应的文件里面写接口的实现,
    • 使用ServiceLoader加载实现类
    • 整个SPI机制分为两部分,第一部分需要在驱动的Jar中,去暴露出你要让加载器去加载哪个类放到一个固定的文件中,接下来在DriverManage代码中就会主动去使用ServiceLoader去加载文件中的类名,并且使用类加载器去加载对应的类并创建对象。
    • 通过迭代器去遍历找到Jar包符合条件的类加载并创建对象
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

SPI中是如何获取到应用程序类加载器的?

SPI中使用了线程上下文中保存的类加载器进行类的加载,这个类加载器一般是应用程序类加载器 ,源代码如下

public static <S> ServiceLoader<S> load(Class<S> service) {//获取线程上下文类加载器ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}
  1. 启动类加载器加载DriverManager。
  2. 在初始化DriverManager时,通过SPI机制加载jar包中的myql驱动。
  3. SPI中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。
  4. 这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制

那么JDBC真的打破了双亲委派机制吗?

在这里插入图片描述

  • 首先DriverManager这个类位于rt.jar中,是由启动类加载器来进行加载的,是满足双亲委派机制的
  • 而在Jar包中的MySQL驱动,这些类 位于ClassPath会由应用程序类加载器来进行加载,而应用程序类加载器也是满足双亲委派机制的,首先他会一层一层的向上查找,然后再向下委派,由于这个驱动没有被加载过,由于路径不满足要求,启动类加载器和拓展类加载器都不能加载最后还是有应用程序类加载器,也是满足要求
  • 并且启动也没有重写loadClass方法,重写loadClass方法才会 打破双亲 委派机制

JDBC只是在DriverManager加载完之后,通过初始化阶段触发了驱动类的加载,类的加载依然遵循双亲委派机制,所以并没有打破双亲委派机制


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/330830.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Springboot+Vue项目-基于Java+MySQL的酒店管理系统(附源码+演示视频+LW)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;Java毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计 &…

为什么选择 Flink 做实时处理

优质博文&#xff1a;IT-BLOG-CN 为什么选择 Flink 【1】流数据更真实地反映了我们的生活方式&#xff08;实时聊天&#xff09;&#xff1b; 【2】传统的数据架构是基于有限数据集的&#xff08;Spark 是基于微批次数据处理&#xff09;&#xff1b; 【3】我们的目标&#xf…

Python协程的作用

过分揣测别人的想法&#xff0c;就会失去自己的立场。大家好&#xff0c;当代软件开发领域中&#xff0c;异步编程已成为一种不可或缺的技术&#xff0c;用于处理大规模数据处理、高并发网络请求、实时通信等应用场景。而Python协程&#xff08;Coroutine&#xff09;作为一种高…

【数据结构】数据结构中的隐藏玩法——栈与队列

前言&#xff1a; 哈喽大家好&#xff0c;我是野生的编程萌新&#xff0c;首先感谢大家的观看。数据结构的学习者大多有这样的想法&#xff1a;数据结构很重要&#xff0c;一定要学好&#xff0c;但数据结构比较抽象&#xff0c;有些算法理解起来很困难&#xff0c;学的很累。我…

【力扣刷题笔记第三期】Python 数据结构与算法

先从简单的题型开始刷起&#xff0c;一起加油啊&#xff01;&#xff01; 点个关注和收藏呗&#xff0c;一起刷题鸭&#xff01;&#xff01; 第一批题目 1.设备编号 给定一个设备编号区间[start, end]&#xff0c;包含4或18的编号都不能使用&#xff0c;如&#xff1a;418、…

安全访问python字典:避免空键错误的艺术

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言 二、直接访问字典键的问题 三、使用get方法安全访问字典键 四、get方法的实际应…

个人IV代码签名证书1000

代码签名证书是一种用来验证代码来源、完整性和是否被篡改的数字证书。这款数字证书可以对软件代码进行数字签名&#xff0c;使得代码的发布者和接收者能够确认代码的真实性和完整性。通常代码签名证书只支持企事业单位申请&#xff0c;Certum个人IV代码签名证书是专为个人软件…

刷代码随想录有感(76):回溯算法——全排列

题干&#xff1a; 代码&#xff1a; class Solution { public:vector<int> tmp;vector<vector<int>> res;void backtracking(vector<int> nums, vector<int> used){if(tmp.size() nums.size()){res.push_back(tmp);return;}for(int i 0; i &l…

罗德里格斯公式(旋转矩阵)推导

文章目录 1. 推导2. 性质3. 参考 1. 推导 r r r为旋转轴&#xff0c; θ \theta θ为旋转角度。 先将旋转轴单位化 u r ∣ ∣ r ∣ ∣ u\frac{r}{||r||} u∣∣r∣∣r​ 旋转可以被分为垂直和旋转两个方向&#xff0c; 我们求沿轴方向的分量其实就是在求 p p p向量在 u u u方…

Mujava 工具的简单使用

首先下载openjava.jar和mujava.jar&#xff0c;以及自己手写一个mujava.config指向存放mujava的目录&#xff0c;并将这些文件放在mujava目录下。此时&#xff0c;基本的mujava环境就搭建好了。 分别创建src&#xff08;存放源码文件&#xff09;、classes&#xff08;存放源码…

从零搭建python环境:深入解析虚拟环境与Python版本管理

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言&#xff1a;为何需要虚拟环境&#xff1f; 二、虚拟环境的创建与命名 1. 虚拟环境…

CTFHUB技能树——SSRF(二)

目录 上传文件 ​FastCGI协议 Redis协议 上传文件 题目描述&#xff1a;这次需要上传一个文件到flag.php了.祝你好运 index.php与上题一样&#xff0c;使用POST请求的方法向flag.php传递参数 //flag.php页面源码 <?phperror_reporting(0);if($_SERVER["REMOTE_ADDR&…

2024“电工杯”数学建模A题《园区微电网风光储协调优化配置》思路和代码分享

A 题&#xff1a;园区微电网风光储协调优化配置 这个题目整体就是一个优化问题&#xff0c;可以采用MatlabYalmipGurobi求解器进行求解&#xff0c;持续更新中&#xff0c;敬请关注&#xff01;&#xff01; 园区微电网由风光发电和主电网联合为负荷供电&#xff0c;为了尽量提…

【Docker】Linux 系统(CentOS 7)安装 Docker

文章目录 对 VMware 软件的建议官方说明文档Docker安装卸载旧版本docker设置仓库开始安装 docker 引擎最新版 Docker 安装指定版本 Docker 安装&#xff08;特殊需求使用&#xff09; 启动 Docker查看 Docker 版本查看 Docker 镜像设置 Docker 开机自启动 验证开机启动是否生效…

牛皮!亚信安全《2024国家级攻防演练100+必修高危漏洞合集》.pdf

上次分享了2023攻防演练高危漏洞&#xff0c;获得了很多粉丝的好评。 今天再分享一份由亚信安全服务团队结合自身的“外部攻击面管理”服务能力和专业的红队能力&#xff0c;最新发布的《2024攻防演练必修高危漏洞合集》&#xff0c;一共108页&#xff0c;非常详细&#xff0c…

windows、mac、linux中node版本的切换(nvm管理工具),解决项目兼容问题 node版本管理、国内npm源镜像切换

文章目录 在工作中&#xff0c;我们可能同时在进行2个或者多个不同的项目开发&#xff0c;每个项目的需求不同&#xff0c;进而不同项目必须依赖不同版本的NodeJS运行环境&#xff0c;这种情况下&#xff0c;对于维护多个版本的node将会是一件非常麻烦的事情&#xff0c;nvm就是…

014_C标准库函数之<stdio.h>

【背景】 今天这个主要说的是<stdio.h>头文件&#xff0c;大家众所周知&#xff0c;这个是我们学习C语言时第一个接触到的头文件了&#xff0c;那么为什么我不一开始就介绍这个头文件呢&#xff1f;我觉得有两个原因&#xff0c;如下&#xff1a; 1.一开始大家的编程思…

spring boot整合j2cache 关闭二级缓存

我们整合了 j2cache 的项目启动 日志会输出 一级缓存 二级缓存 一级是 EhCacheProvider 二级是 SpringRedisProvider 如果 我们不想用二级缓存 在 j2cache.properties 中 加上 j2cache.12-cache-open配置 值为 true/false true是启用二级缓存 false 是不起用 默认 true 所以 …

其它高阶数据结构⑦_Skiplist跳表_概念+实现+对比

目录 1. Skiplist跳表的概念 2. Skiplist跳表的效率 3. Skiplist跳表的实现 3.1 力扣1206. 设计跳表 3.2 Skiplist的初始化和查找 3.3 Skiplist的增加和删除 3.4 Skiplist的源码和OJ测试 4. 跳表和平衡搜索树/哈希表的对比 本篇完。 1. Skiplist跳表的概念 skiplist是…

AWTK实现汽车仪表Cluster/DashBoard嵌入式GUI开发(七):快启

前言: 汽车仪表是人们了解汽车状况的窗口,而仪表中的大部分信息都是以指示灯形式显示给驾驶者。仪表指示灯图案都较为抽象,对驾驶不熟悉的人在理解仪表指示灯含义方面存在不同程度的困难,尤其对于驾驶新手,如果对指示灯的含义不求甚解,有可能影响驾驶的安全性。即使是对…