JVM类加载机制 第四节

文章目录

重复了,上次已经学过了,这里主要是去看看书。

JVM类加载机制

类加载是JVM的核心底层机制,也是衔接磁盘.class字节码文件JVM运行时数据区域 的关键桥梁------简单说,类加载就是JVM将磁盘上的.class文件(二进制字节码)加载到内存,完成类元数据解析、存储,并生成可被程序使用的Class对象的全过程。其最终结果是:类的元数据存入方法区(元空间),类的Class对象存入堆内存,这也是后续创建对象、调用静态方法/变量的基础,和之前讲的JVM运行时数据区域强关联。

一、类加载的核心定义(通俗理解)

1. 基本概念

类加载机制:JVM通过类加载器,将编译后的.class二进制字节码文件(磁盘/网络/内存)加载到JVM内存,经过验证、准备、解析、初始化等步骤,最终在方法区存储类的元数据,在堆中生成对应java.lang.Class对象的过程。

  • 加载主体:类加载器(ClassLoader),是JVM实现类加载的具体组件;
  • 核心结果:① 类元数据(类名、父类、方法/字段定义、静态变量等)→ 方法区(元空间);② 代表该类的Class对象 → 堆内存(作为程序访问方法区类元数据的"入口");
  • 核心特点:懒加载(延迟加载) ,JVM不会在启动时加载所有类,而是在程序首次主动使用该类时,才触发类加载(节省内存)。

2. 通俗比喻

把类加载比作**"图书馆引入新书的全过程"**:

  • 磁盘.class文件 → 图书馆未入库的新书;
  • 类加载器 → 图书馆管理员;
  • 方法区(元空间) → 图书馆的藏书目录(存储书籍基本信息:书名、作者、分类、页码等);
  • 堆中Class对象 → 这本书的借阅索引卡(程序通过索引卡,就能查到藏书目录的详细信息,进而"使用"这本书);
  • 类加载的各个步骤 → 管理员验书(验证)、贴书号(准备)、录入目录(解析)、盖章入库(初始化)。

3. 与JVM运行时数据区域的关联(核心衔接)

类加载是之前讲的方法区、堆的"数据来源"之一,核心关联如下:

  1. 类加载的准备阶段 :为静态变量分配内存并赋默认值 → 内存位置是方法区(元空间)
  2. 类加载的初始化阶段:为静态变量赋实际值、执行静态代码块 → 操作的是方法区中的静态变量;
  3. 类加载完成后:类元数据永久存储在方法区,直到类被卸载;堆中的Class对象作为访问入口,被程序引用。

一句话总结 :没有类加载,方法区就没有类元数据,堆也无法创建该类的对象实例,程序所有的new操作、静态方法调用都会失效。

二、类加载的时机:何时触发类加载?

JVM规范严格定义了6种主动引用场景 ,只有当程序首次主动使用 某个类时,才会触发类的加载+链接+初始化;除此之外的被动引用,不会触发类的初始化(可能触发加载/链接,但不会执行初始化)。

1. 触发类加载(初始化)的6种主动引用场景(JVM规范强制要求)

这是面试高频考点,必须熟记,核心是"首次、主动、使用"三个关键词:

  1. 当通过new关键字创建类的实例时(如new User());
  2. 调用类的静态变量 (非final常量)或静态方法 时(如User.countUser.sayHello());
  3. 通过反射机制访问类时(如Class.forName("com.example.User"));
  4. 初始化某个类的子类时(子类首次主动使用,会先触发父类的初始化);
  5. JVM启动时,执行主类(包含main方法的类) (如java Demo,会先初始化Demo类);
  6. 当使用JDK 1.7及以上的动态语言支持时,方法句柄对应的类首次被使用时。

核心规则父类优先于子类初始化,接口不遵循此规则(接口初始化时,不会触发父接口的初始化,只有当使用父接口的静态变量时,才会初始化)。

2. 不触发类初始化的3种常见被动引用场景(新手易混)

被动引用仅可能触发类的加载/链接 ,但不会执行初始化(即不会执行静态代码块、不会给静态变量赋实际值),常见场景:

  1. 子类引用父类的静态变量:仅初始化父类,子类不会被初始化;
  2. 通过数组创建类的引用 :如User[] users = new User[10],仅创建数组对象,不会初始化User类;
  3. 引用类的final常量:编译期常量会被存入运行时常量池,引用时直接取常量池的值,不会初始化类。

3. 代码示例:验证主动/被动引用(直观理解)

java 复制代码
// 父类:用于验证父类优先初始化
class Parent {
    // 静态变量:准备阶段赋默认值0,初始化阶段赋实际值10
    public static int parentNum = 10;
    // 静态代码块:初始化阶段执行,用于标记是否初始化
    static {
        System.out.println("Parent类 初始化");
    }
}

// 子类
class Child extends Parent {
    public static int childNum = 20;
    static {
        System.out.println("Child类 初始化");
    }
}

// 测试类:主类
public class ClassLoadTriggerDemo {
    // 编译期final常量:存入运行时常量池
    public static final String CONST_STR = "hello";

    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println("===== 场景1:子类引用父类静态变量(被动引用) =====");
        System.out.println(Child.parentNum); // 仅初始化Parent,不初始化Child
        System.out.println("===== 场景2:数组创建类引用(被动引用) =====");
        Parent[] parents = new Parent[5]; // 不初始化Parent
        System.out.println("===== 场景3:引用final常量(被动引用) =====");
        System.out.println(ClassLoadTriggerDemo.CONST_STR); // 不初始化当前类
        System.out.println("===== 场景4:new创建实例(主动引用) =====");
        Child child = new Child(); // 触发Child初始化(先初始化Parent)
        System.out.println("===== 场景5:反射访问(主动引用) =====");
        Class.forName("com.example.Child"); // 若未初始化,触发初始化
    }
}

运行结果

复制代码
===== 场景1:子类引用父类静态变量(被动引用) =====
Parent类 初始化
10
===== 场景2:数组创建类引用(被动引用) =====
===== 场景3:引用final常量(被动引用) =====
hello
===== 场景4:new创建实例(主动引用) =====
Child类 初始化
20
===== 场景5:反射访问(主动引用) =====
(无输出,因为Child已在场景4初始化)

核心结论 :只有主动引用才会触发类的初始化,被动引用不会;且子类初始化时,父类一定会先完成初始化。

三、类加载的核心过程:加载→链接→初始化(三步五阶段)

JVM将类从加载到可用,分为三大步骤 ,其中链接步骤 又细分为验证、准备、解析 三个阶段,合称为类加载的"三步五阶段" 。这五个阶段按固定顺序执行(解析阶段可在初始化阶段后执行,为了支持动态绑定),全程由类加载器协调完成。

整体流程:加载(Load)→ 验证(Verify)→ 准备(Prepare)→ 解析(Resolve)→ 初始化(Initialize)

总览:三步五阶段核心职责

大步骤 子阶段 核心职责(通俗解释) 操作的内存区域
加载 - 读取.class文件,生成Class对象 磁盘→堆(Class对象)、方法区(临时元数据)
链接 验证 校验.class文件的合法性(防止篡改、格式错误) 方法区
链接 准备 为静态变量分配内存,赋默认值 方法区(静态变量)
链接 解析 将符号引用转为直接引用(找实际内存地址) 方法区(运行时常量池)
初始化 - 执行静态代码块,为静态变量赋实际值 方法区(静态变量)

1. 加载(Load):读取文件,生成Class对象

核心工作 :类加载器根据类的全限定名 (如com.example.User),找到对应的.class二进制字节码文件(来源:磁盘、网络、内存、动态生成),将其读取到JVM内存,然后在堆中生成一个代表该类的java.lang.Class对象,并将类的初步元数据存入方法区。

  • 关键操作1:查找.class文件:不同类加载器有不同的查找范围(后续讲类加载器时详细说);
  • 关键操作2:生成Class对象 :这是程序访问类元数据的唯一入口,每个类在JVM中只有一个Class对象(保证类的唯一性);
  • 核心特点:加载阶段是类加载器的核心工作阶段,后续的链接、初始化阶段由JVM统一完成,与类加载器无关。

2. 链接(Link):验证+准备+解析,让类"可用"

链接阶段的核心目标:将加载阶段读取的类元数据,进行合法性校验、内存分配、引用解析,确保该类能被JVM正确使用,分为三个子阶段。

(1)验证(Verify):.class文件的"安检"

核心工作 :JVM对加载的.class文件进行严格的合法性校验,防止恶意篡改、格式错误的.class文件被加载,保证JVM的运行安全。

  • 校验内容:文件格式验证(是否符合.class文件规范)、元数据验证(类的继承关系、字段/方法定义是否合法)、字节码验证(字节码指令是否合法,防止无限循环、栈溢出)、符号引用验证(符号引用的目标是否存在);
  • 核心作用:保证JVM安全 ,若验证失败,会抛出java.lang.VerifyError异常,类加载终止。
(2)准备(Prepare):为静态变量分配内存,赋默认值

核心工作 :JVM在方法区 为类的静态变量(static修饰) 分配内存空间,并为其赋JVM默认值(而非代码中定义的实际值)。

  • 关键要点1:仅处理静态变量,实例变量不会在该阶段处理(实例变量在创建对象时,在堆中分配内存);
  • 关键要点2:赋默认值,而非实际值:默认值是JVM为各数据类型定义的初始值(如int→0、String→null、boolean→false);
  • 关键要点3:final静态常量特殊处理 :被static final修饰的常量,在编译期 就会被赋实际值,存入类文件常量池,准备阶段会直接赋实际值(而非默认值)。

代码示例:准备阶段的默认值

java 复制代码
class PrepareDemo {
    public static int a = 10; // 准备阶段:分配内存,赋默认值0;初始化阶段:赋实际值10
    public static String str; // 准备阶段:分配内存,赋默认值null;初始化阶段:若有赋值则赋实际值
    public static final int b = 20; // 编译期常量,准备阶段直接赋实际值20(非0)
}
(3)解析(Resolve):符号引用→直接引用

核心工作 :JVM将方法区运行时常量池 中的符号引用 ,转换为直接引用(即目标的实际内存地址)。

  • 符号引用:之前讲运行时常量池时提到过,是类/方法/字段的"间接标识"(如方法名add(int,int)、类名com.example.User),存储在运行时常量池;
  • 直接引用:是类/方法/字段在JVM内存中的实际内存地址(或指向地址的指针);
  • 解析对象:类、接口、字段、方法、方法句柄等;
  • 核心特点:解析阶段可延迟执行 :JVM规范允许解析阶段在初始化阶段之后 执行(为了支持动态绑定,如多态中的方法调用,运行时才知道实际调用的子类方法)。

3. 初始化(Initialize):执行静态代码,赋实际值

核心工作 :JVM执行类的静态代码块(static{}) ,并为静态变量赋代码中定义的实际值 ,这是类加载过程中唯一由程序员编写的代码执行阶段

  • 执行顺序:① 先执行父类的初始化(递归执行,直到Object类);② 再执行当前类的静态变量赋值语句;③ 最后执行当前类的静态代码块;
  • 核心规则:初始化阶段仅执行一次 :每个类在JVM的生命周期中,只会被初始化一次(保证静态变量只被赋值一次,静态代码块只执行一次);
  • 触发条件:只有主动引用才会触发初始化(之前讲的6种场景);
  • 异常处理:若初始化阶段执行静态代码块/赋值语句时抛出异常,类加载会终止,该类变为不可用状态 ,后续任何使用该类的操作都会抛出java.lang.NoClassDefFoundError异常。

代码示例:初始化阶段的执行顺序

java 复制代码
// 父类
class ParentInit {
    public static int parentA = 10;
    static {
        System.out.println("ParentInit 静态代码块执行");
        parentA = 20;
    }
}

// 子类
class ChildInit extends ParentInit {
    public static int childA = parentA + 10;
    static {
        System.out.println("ChildInit 静态代码块执行");
        childA = 40;
    }
}

// 测试类
public class InitializeDemo {
    public static void main(String[] args) {
        // 主动引用:调用子类静态变量,触发初始化
        System.out.println(ChildInit.childA);
    }
}

运行结果

复制代码
ParentInit 静态代码块执行
ChildInit 静态代码块执行
40

执行流程解析

  1. 调用ChildInit.childA,触发子类初始化,先递归初始化父类ParentInit
  2. 父类初始化:先为parentA赋实际值10 → 执行父类静态代码块,将parentA改为20;
  3. 子类初始化:先为childA赋实际值20+10=30 → 执行子类静态代码块,将childA改为40;
  4. 最终输出childA的实际值40。

四、类加载器(ClassLoader):类加载的"执行者"

类加载器是实现加载阶段 的具体组件,其核心职责是:根据类的全限定名 查找并读取.class文件,生成Class对象。JVM提供了三层内置类加载器 ,并支持开发者实现自定义类加载器,共同构成类加载器的体系结构。

1. 类加载器的核心特性

  • 双亲委派模型:这是JVM类加载器的核心设计原则(后续详细讲),保证类的唯一性;
  • 类的唯一性同一个类,由不同的类加载器加载,在JVM中会被视为两个不同的类(即使.class文件完全相同);
  • 沙箱安全 :通过双亲委派模型,防止自定义类加载器篡改JVM核心类(如java.lang.String);
  • 可扩展性 :开发者可通过继承java.lang.ClassLoader,实现自定义类加载器,加载非磁盘来源的.class文件(如网络、内存、加密文件)。

2. JVM的三层内置类加载器(核心)

JVM默认提供三层内置类加载器,按加载范围从核心到应用 划分,每层加载器有固定的加载路径,互不重叠。

类加载器名称 英文名称 核心职责(加载范围) 实现类 是否继承ClassLoader
启动类加载器 Bootstrap ClassLoader 加载JVM核心类库(JAVA_HOME/jre/lib下的rt.jar、charsets.jar等,如java.lang、java.util包) C/C++实现(JVM内部) 否(不是Java类)
扩展类加载器 Extension ClassLoader 加载JVM扩展类库(JAVA_HOME/jre/lib/ext下的jar包,或java.ext.dirs系统属性指定的路径) sun.misc.Launcher$ExtClassLoader
应用程序类加载器(系统类加载器) Application ClassLoader 加载应用程序的类 (项目src/main/java下的类、第三方依赖包(maven/gradle)、classpath环境变量指定的路径) sun.misc.Launcher$AppClassLoader

3. 自定义类加载器

开发者通过继承java.lang.ClassLoader ,重写其findClass()方法(推荐)或loadClass()方法(不推荐,会破坏双亲委派模型),实现自定义的类加载逻辑。

  • 核心使用场景:加载非磁盘来源的.class文件(如网络下载的加密.class文件、内存中动态生成的.class文件)、实现类的热部署(如Tomcat的WebAppClassLoader)、隔离不同模块的类(如微服务中的类隔离);
  • 核心原则:重写findClass()方法,而非loadClass()方法(loadClass()是实现双亲委派模型的核心方法,重写会破坏模型)。

4. 代码示例:获取类的加载器

java 复制代码
public class ClassLoaderDemo {
    public static void main(String[] args) {
        // 1. 获取应用程序类加载器(加载当前自定义类)
        ClassLoader appClassLoader = ClassLoaderDemo.class.getClassLoader();
        System.out.println("当前类的类加载器:" + appClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2

        // 2. 获取扩展类加载器(应用程序类加载器的父类)
        ClassLoader extClassLoader = appClassLoader.getParent();
        System.out.println("扩展类加载器:" + extClassLoader); // sun.misc.Launcher$ExtClassLoader@1b6d3586

        // 3. 获取启动类加载器(扩展类加载器的父类)
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println("启动类加载器:" + bootstrapClassLoader); // null(C/C++实现,无Java对象)

        // 4. 启动类加载器加载的核心类(如String),getClassLoader()返回null
        ClassLoader stringClassLoader = String.class.getClassLoader();
        System.out.println("String类的类加载器:" + stringClassLoader); // null
    }
}

核心结论

  • 自定义类由应用程序类加载器加载;
  • JVM核心类(如String)由启动类加载器 加载,其getClassLoader()返回null(因为启动类加载器不是Java类);
  • 类加载器的父子关系 :应用程序类加载器 → 扩展类加载器 → 启动类加载器(注意:是逻辑上的父子关系 ,并非继承关系,而是通过getParent()方法关联)。

五、双亲委派模型:类加载器的核心设计原则

双亲委派模型 是JVM类加载器的核心设计原则 ,也是面试最高频的考点之一。它定义了类加载器在加载类时的查找顺序先向上委托,再向下查找 ,简单说就是"孩子找爹,爹找爷爷,爷爷找不到,再自己找"。

1. 双亲委派模型的核心定义

当某个类加载器收到类加载的请求时,它不会立即自己去查找加载 ,而是先将该请求委托给其父类加载器 去执行;父类加载器收到请求后,也会继续向上委托,直到委托给顶层的启动类加载器 ;只有当父类加载器在自己的加载范围内找不到该类 时,才会将请求返回给子类加载器,由子类加载器自己去查找加载。

2. 通俗比喻:公司的"审批流程"

把类加载请求比作员工申请经费 ,类加载器比作员工→部门经理→总经理→董事长(父子关系):

  1. 员工(应用程序类加载器)收到申请,先交给部门经理(扩展类加载器)审批;
  2. 部门经理交给总经理(启动类加载器)审批;
  3. 董事长(启动类加载器)在自己的权限范围内(核心类库)查看,若能审批(找到类),则直接处理;若不能,返回给总经理;
  4. 总经理(扩展类加载器)在自己的权限范围内(扩展类库)查看,若能处理则处理,否则返回给部门经理;
  5. 部门经理(应用程序类加载器)在自己的权限范围内(应用类)查看,若能处理则处理,否则抛出ClassNotFoundException异常。

3. 双亲委派模型的执行流程(图文版)

以加载自定义类com.example.User为例,流程如下:

复制代码
1. 应用程序类加载器收到加载请求 → 委托给父类(扩展类加载器)
2. 扩展类加载器收到请求 → 委托给父类(启动类加载器)
3. 启动类加载器:在JRE/lib下查找,未找到User类 → 返回给扩展类加载器
4. 扩展类加载器:在JRE/lib/ext下查找,未找到User类 → 返回给应用程序类加载器
5. 应用程序类加载器:在classpath下查找,找到User.class → 加载该类,生成Class对象

4. 双亲委派模型的核心优势

(1)保证类的唯一性

同一个类,在JVM中只会被加载一次。因为所有类加载请求都会先委托给顶层的启动类加载器,只有父类加载器找不到时,子类加载器才会自己加载,避免了同一个类被多个类加载器重复加载。

  • 示例:若自定义了一个java.lang.String类,应用程序类加载器收到加载请求后,会委托给启动类加载器,而启动类加载器已经加载了核心的java.lang.String类,因此自定义的String类不会被加载,保证了核心类的唯一性。
(2)保证JVM的沙箱安全

防止恶意代码篡改JVM核心类库。比如,开发者无法自定义一个java.lang.String类来替代JVM的核心String类,因为双亲委派模型会让启动类加载器先加载核心的String类,自定义的String类永远不会被加载。

(3)简化类加载器的设计

每个类加载器只需关注自己的加载范围,无需关心其他加载器的逻辑,降低了类加载器的实现复杂度。

5. 双亲委派模型的"破坏"场景

双亲委派模型是JVM的推荐设计原则 ,并非强制要求,在某些场景下,需要破坏双亲委派模型才能实现特定功能,常见场景:

  1. SPI机制(服务提供者接口) :如JDBC、JNDI等,核心类由启动类加载器加载,而实现类由应用程序类加载器加载,启动类加载器无法加载实现类,因此需要通过线程上下文类加载器(Thread Context ClassLoader)打破双亲委派;
  2. 类的热部署:如Tomcat的WebAppClassLoader,每个Web应用有自己的类加载器,需要加载当前应用的类,而不委托给父类加载器,否则多个Web应用的类会冲突;
  3. 自定义类加载器 :若开发者重写了类加载器的loadClass()方法,会直接破坏双亲委派模型的执行流程。

六、类的完整生命周期

类加载的"三步五阶段"只是类生命周期的前半部分 ,一个类从被加载到被卸载,完整的生命周期分为7个阶段 ,其中加载、链接、初始化、使用、卸载 是按顺序执行的,解析阶段可在初始化阶段后执行(动态绑定)。

类的完整生命周期加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载

  1. 加载/链接/初始化:前面已详细讲解,是类的"初始化阶段";
  2. 使用:程序通过创建类的实例、调用静态方法/变量、反射访问等方式,使用该类;
  3. 卸载 :当该类满足卸载条件时,JVM会将其从方法区中移除(类元数据被回收),堆中的Class对象也会被GC回收,该类的生命周期结束。

类的卸载条件(严格,一般很少触发)

JVM对类的卸载有严格的要求,只有当以下三个条件同时满足时,类才会被卸载:

  1. 该类的所有实例对象都已被GC回收(堆中无该类的任何实例);
  2. 该类的Class对象已被GC回收(无任何程序引用该Class对象);
  3. 加载该类的类加载器 已被GC回收(自定义类加载器才会满足,内置类加载器随JVM启动而存在,随JVM关闭而销毁,因此内置类加载器加载的类永远不会被卸载)。

核心结论 :JVM核心类库(如java.lang.String)由启动类加载器加载,永远不会被卸载;应用程序类加载器/扩展类加载器加载的类,一般也不会被卸载;只有自定义类加载器加载的类,才有可能满足卸载条件,被JVM卸载。

七、面试高频考点与新手易混点

1. 面试高频考点(必背)

  1. 类加载的三步五阶段分别是什么?每个阶段的核心工作?
  2. 触发类初始化的6种主动引用场景?哪些被动引用不会触发初始化?
  3. 双亲委派模型的核心原理?优势?哪些场景会破坏双亲委派模型?
  4. JVM的三层内置类加载器分别是什么?各自的加载范围?
  5. 准备阶段和初始化阶段的区别?static final常量在哪个阶段赋实际值?
  6. Class对象存在哪个内存区域?类的元数据存在哪个内存区域?
  7. 为什么同一个类由不同类加载器加载,会被视为不同的类?
  8. 类的卸载条件是什么?JVM核心类会被卸载吗?

2. 新手易混点(核心区分)

(1)类加载 vs 对象实例化
  • 类加载:加载的是类本身 ,生成Class对象,存储类元数据,是针对类的操作,只执行一次;
  • 对象实例化:通过new关键字,根据类的元数据,在堆中创建对象实例 ,是针对对象的操作,可执行多次;
  • 关系:先有类加载,后有对象实例化,没有类加载,就无法创建对象实例。
(2)准备阶段 vs 初始化阶段
  • 准备阶段:赋默认值,由JVM自动执行,无代码参与;
  • 初始化阶段:赋实际值,执行静态代码块,由程序员编写的代码决定;
  • 关键区别:static final常量在准备阶段 赋实际值,普通静态变量在初始化阶段赋实际值。
(3)类加载器的"父子关系" vs 继承关系
  • 类加载器的父子关系:是逻辑上的关联关系 ,通过getParent()方法实现,并非继承关系;
  • 示例:应用程序类加载器的父类是扩展类加载器,但应用程序类加载器并不继承扩展类加载器,而是通过getParent()方法指向扩展类加载器。
(4)启动类加载器 vs 其他类加载器
  • 启动类加载器:C/C++实现,是JVM内部组件,不是Java类,因此getParent()返回null;
  • 扩展/应用程序类加载器:Java类,继承java.lang.ClassLoader,有对应的Java对象。

八、核心总结

  1. 类加载是JVM将.class文件加载到内存的过程,最终结果是类元数据存入方法区,Class对象存入堆,是衔接磁盘字节码和JVM运行时数据区域的关键;
  2. 类加载遵循懒加载 ,仅在6种主动引用场景下触发初始化,被动引用不会触发;
  3. 类加载的核心流程是三步五阶段 ,其中准备阶段 为静态变量分配内存赋默认值,初始化阶段执行静态代码块赋实际值,是唯一执行程序员代码的阶段;
  4. JVM提供三层内置类加载器 ,遵循双亲委派模型 (先向上委托,再向下查找),核心优势是保证类的唯一性和JVM安全
  5. 类的完整生命周期包含7个阶段,卸载条件严格,只有自定义类加载器加载的类才有可能被卸载,JVM核心类永远不会被卸载;
  6. 类加载是JVM底层的核心机制,也是理解内存区域、GC、反射、动态代理的基础,和之前讲的方法区、堆、运行时常量池强关联,需结合理解。
相关推荐
2401_891450466 小时前
Python上下文管理器(with语句)的原理与实践
jvm·数据库·python
helloworldandy6 小时前
使用Python处理计算机图形学(PIL/Pillow)
jvm·数据库·python
2301_790300967 小时前
Python单元测试(unittest)实战指南
jvm·数据库·python
码农水水7 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
2401_838472518 小时前
使用Scikit-learn构建你的第一个机器学习模型
jvm·数据库·python
u0109272718 小时前
使用Python进行网络设备自动配置
jvm·数据库·python
执草书云8 小时前
项目优化要点
java·jvm
OnYoung8 小时前
自动化机器学习(AutoML)库TPOT使用指南
jvm·数据库·python
2401_836121608 小时前
机器学习与人工智能
jvm·数据库·python
zhiyLt8 小时前
如何从Python初学者进阶为专家?
jvm·数据库·python