JVM | 基于类加载的一次完全实践

引言

我在上篇文章:JVM | 类加载是怎么工作的 中为你介绍了Java的类加载器及其工作原理。我们简单回顾下:我用一个易于理解的类比带你逐步理解了类加载的流程和主要角色:引导类加载器扩展类加载器应用类加载器。并带你深入了解了这些“建筑工人”如何从底层工作,搬运原材料(类)并将其完整地构建在Java虚拟机(JVM)的“建筑工地”上。然后,我们跟随一个具体的Building类,亲眼目睹了其在JVM中的生命周期。我在文章末尾留了几个问题,你还记得吗?

本篇文章,我将带你了解自定义类加载器的创建和使用。我们还将探索Java的SPI机制,了解它如何利用类加载器实现服务的动态发现和加载。接着,我们再来看下Tomcat的类加载机制,尤其是它的热部署多版本共存的实现,了解类加载机制在现实世界中的高级应用。


自定义类加载器的创建和使用

当我们的类涉及到一些安全的操作,或者我们想从网络或者其它地方加载类。这种情况,我们就会创建自定义的类加载器,重写findClass方法来完成这个特殊的加载逻辑。

沿用上篇文章的例子:假如工地来活了,要求建造一个复杂的建筑物,这个建筑物不仅包括了普通的房间(普通的类),还包括了一些特殊设计的房间(特殊的类)。
在这个情况下,你可能会需要一位专门的工人来处理这些特殊的房间。这位工人需要有特殊的技能和工具,才能按照设计图纸(类的字节码)正确地建造出房间。
接下来,我们来看下类加载器怎么创建与使用的。


创建类加载器

我们来实现一个类加载器,代码如下:


public class CustomClassLoader extends ClassLoader {
    private String classPath;
    public CustomClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 先检查类是否已经被加载
        Class<?> cls = findLoadedClass(name);
        if (cls != null) {
            return cls;
        }

        try {
            // 如果类还未被加载,尝试使用父类加载器加载(不破坏双亲委派机制)
            cls = getParent().loadClass(name);
        } catch (ClassNotFoundException e) {
            // 父类加载器无法加载该类,那么就调用 findClass 尝试自己加载
            cls = findClass(name);
        }

        return cls;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 你的类加载逻辑...
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = getFileName(className);
        try {
            InputStream is = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead;
            while ((bytesNumRead = is.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String getFileName(String name) {
        int index = name.lastIndexOf('.');
        // 如果没有找到'.'则直接在classPath中查找
        if (index == -1) { 
            return classPath + name + ".class";
        } else {
            return classPath + name.substring(index + 1) + ".class";
        }
    }
}

上面的类加载器CustomClassLoader 通过构造的方式传入文件路径。当我们要加载类时,它会调用loadClass方法从我们定义的类路径下读取字节流。好,
接下来,我们来使用我们自己定义的类加载器。

使用类加载器

代码如下:

// 把上篇文章的Building类放在桌面
CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\xxx\\Desktop\\");

try {
    // 使用自定义类加载器加载 Building 类,若你的包名不叫这个,请更换。
    Class<?> cls = Class.forName("org.kfaino.jvm.Building", true, customClassLoader);

    // 创建类的实例
    cls.newInstance();

    System.out.println("实例名:" + cls.getName() + " 被加载器:" + cls.getClassLoader() + "创建");

} catch (Exception e) {
    e.printStackTrace();
}

执行!

Connected to the target VM, address: '127.0.0.1:4706', transport: 'socket'
建筑蓝图已被创建!
实例名:org.kfaino.jvm.Building 被加载器:sun.misc.Launcher$AppClassLoader@3f3fdda9创建

Disconnected from the target VM, address: '127.0.0.1:4706', transport: 'socket'
Process finished with exit code 0

类被AppClassLoader加载了,为啥? 原因是我的项目中有Building类, 这个类可以被应用类加载器加载,因此就轮不到·CustomClassLoader 加载了。我们把项目内的Building先改名一下。然后执行!

Connected to the target VM, address: '127.0.0.1:5032', transport: 'socket'
建筑蓝图已被创建!
实例名:org.kfaino.jvm.Building 被加载器:org.kfaino.jvm.CustomClassLoader@6379b5ed创建
Disconnected from the target VM, address: '127.0.0.1:5032', transport: 'socket'

Process finished with exit code 0

这次,我们的类成功被CustomClassLoader加载,并且加载的是我们桌面上的字节码文件。


使用Java自带的类加载器工具类

当然,如果你想要从外部加载字节码文件,可以不必这么繁琐。JDK提供了一个功能更强大的URLClassLoader。我们一起来看下它怎么用:

        // 把Building放在桌面
        URL[] urls = new URL[] {new URL("file:C:\\Users\\xxx\\Desktop\\")};
        URLClassLoader customClassLoader = new URLClassLoader(urls);

        try {
            // 使用自定义类加载器加载 Building 类
            Class<?> cls = Class.forName("org.kfaino.jvm.Building", true, customClassLoader);

            // 创建类的实例
            cls.newInstance();

            System.out.println("实例名:" + cls.getName() + " 被加载器:" + cls.getClassLoader() + "创建");

        } catch (Exception e) {
            e.printStackTrace();
        }

我们执行看下:

Connected to the target VM, address: '127.0.0.1:9734', transport: 'socket'
建筑蓝图已被创建!
实例名:org.kfaino.jvm.Building 被加载器:java.net.URLClassLoader@28634811创建
Disconnected from the target VM, address: '127.0.0.1:9734', transport: 'socket'

Process finished with exit code 0

没有问题,本地的字节码文件Building 被成功读取。因此,当你有从外部读取字节码文件的需求,可以试试用JDK自带的·URLClassLoader类加载器。同时,它还提供了其它更强大的功能。

从网络URL加载类和资源

若你想从网络加载字节码文件,你可以这么做:

URL url = new URL("http://www.github.com/xxx/");
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
Class<?> clazz = urlClassLoader.loadClass("包名.类名");

更多的URL加载类和资源

细心的你肯定发现URLClassLoader的构造入参是数组类型,也就意味着可以传入多个URL,具体用法如下:

URL url1 = new URL("http://www.github.com/xxx1/");
URL url2 = new URL("http://www.github.com/xxx2/");
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url1, url2});
Class<?> clazz = urlClassLoader.loadClass("包名.类名");

从JAR文件加载类和资源

它可以从完整的jar包中读取字节码文件,代码如下:

File file = new File("/xxx/jarfile.jar");
URL url = file.toURI().toURL();
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
Class<?> clazz = urlClassLoader.loadClass("包名.类名");

加载外部配置文件

它可以从外部读取配置文件,代码如下:

File file = new File("/xxx/resources/");
URL url = file.toURI().toURL();
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
URL resourceUrl = urlClassLoader.getResource("xxx.properties");
InputStream stream = resourceUrl.openStream();
Properties properties = new Properties();
properties.load(stream);

自定义类加载器的注意事项

类加载器在类的加载过程中起着至关重要的作用。因此,在使用时,我们必须倍加警惕。接下来,让我们来看下哪些需要注意的问题:

内存泄漏

长期存活的类加载器持有类的引用就会导致内存泄露。为了避免这个问题,我建议你在关键代码处适当使用如下代码,让旧的类加载器和类实例进行解绑:

// 释放对 ClassLoader 的引用,使其有可能被垃圾回收
classLoader = null;
System.gc();

当然,每个问题都需要我们针对性地分析。我这里只是提供可能导致内存泄漏的一个说法。实际上,引发内存泄漏的原因有很多,如果你在工作中遇到了这个问题,可以使用一些可视化分析工具来综合性的分析。

不要轻易破坏双亲委派机制

双亲委派模型是为了保证Java核心类库的安全性。当然,我们也可以选择破坏双亲委派模型,前提是,你已考虑好这些风险并规避。

在上述代码中,我们没有违背双亲委派模型的原则。回顾一下我们在之前文章中提到的双亲委派模型的概念:在类加载的过程中,我们首先会让父类加载器进行加载,只有在父类加载器无法加载的情况下,我们才会使用自定义的类加载器进行加载。顺便我把上篇缺少的自定义类加载器也补充进去,你可以看下:
在这里插入图片描述

线程安全问题

如果我们在多线程中使用类加载器,可能会导致类被重复加载多次。除了会浪费资源外,还会导致我们一些静态初始化代码被执行多次,造成一些诡异的问题。我在上篇专栏中说到,解决线程安全的方式有多种。为了保险起见,你可以采用同步方案来解决它。


自定义类加载器使用场景

在上面的例子中,我为你展示如何从外部加载字节码文件。接下来,我们来看下还有哪些使用场景:

安全检查

安全,是软件工程中永恒的话题。为了防止第三方的潜在干扰,我们通常在获取外部文件的同时,做一些过滤的机制。你看代码:

public class SecurityCheckingClassLoader extends ClassLoader {
    private static final String CLASS_NAME_PREFIX = "Safe";

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
    	// 加上你想要的安全校验逻辑
        if (!name.startsWith(CLASS_NAME_PREFIX)) {
            throw new ClassNotFoundException("不安全的类: " + name);
        }

        return super.loadClass(name);
    }
}

上面,我为你举了一个简单的例子。我在加载类方法loadClass前校验类名的前缀,如果你不是Safe开头的类,我们就不予放行。

解密加密的类文件

网络环境充满不确定性,如果你选择从网络获取字节码文件,我建议你首先做好加密工作。既然是从外部获取文件,我们可以通过继承URLClassLoader来实现。代码如下:

import java.net.URL;
import java.net.URLClassLoader;

public class DecryptingURLClassLoader extends URLClassLoader {
    public DecryptingURLClassLoader(URL[] urls) {
        super(urls);
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    	// 获取字节码文件
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        byte[] decryptedClassData = decrypt(classData);
        return defineClass(name, decryptedClassData, 0, decryptedClassData.length);
    }

    private byte[] loadClassData(String className) {
    	// 从网络中获取字节码文件
    }

    private byte[] decrypt(byte[] classData) {
		// 解密字节码文件
    }
}

上面两个是工作中相对比较容易用到的两种场景,还有没有其它类加载器的优秀案例呢?


基于自定义类加载器的其它实现

我们从官网文档中得知,Tomcat会为每个web应用创建类加载器。图片如下:
在这里插入图片描述
现在,我们来看下Tomcat用自定义加载器做了哪些事情。

Tomcat中的热部署

先来解释下什么是热部署? 热部署是指我们的应用在运行过程中,可以在不关闭应用的前提下更新应用。
假如你想开启热部署,你可以在context.xml里面设置reloadable="true”。限于篇幅有限,我在这里只是为你说明Tomcat热部署到底是怎么实现的,如果你感兴趣,建议您亲自动手实操。

热部署实现原理

Tomcat通过一个BackgroundProcessor 后台线程周期性的检查web 应用的 WEB-INF/classes 和 WEB-INF/lib 目录下的 class 文件或 jar 文件是否有变化。具体做法是比较文件的最后修改时间和上次记录的最后修改时间是否一致。如果有变化,就触发 web 应用的重载。
Tomcat重新加载一个web应用时,会创建一个新的WebappClassLoader实例,并使用这个新的类加载器来加载web应用的类。这样,新的类加载器就会加载最新版本的类,而旧的类加载器加载的旧版本的类会在它们不再被引用时被垃圾回收。这就是Tomcat的热部署。

Tomcat中的多版本共存

那什么是多版本共存? 我们在上面说到,每一个web应用都有自己独立的类加载器,这就意味着每一个web应用都有自己的类和库的命名空间。即使同一Tomcat实例中运行的多个web应用使用了同名的类和库,它们也不会相互干扰。
也就是说Tomcat的多版本共存关键也在于每个应用都有不同的类加载器。
限于篇幅有限,更多细节,建议你移步到官方文档,我在文末参考文献中为你贴出官方地址。


ServiceLoader和SPI

我们经常会听到许多Java框架包括Dubbo、Spring等都使用了SPI这个机制,SPI究竟是什么东西?ServiceLoader和我们今天讲的类加载器又有什么关系?

SPI(Service Provider Interface)是什么?

我从服务提供方和服务调用方两个视角来为你讲解:

服务提供方

对于服务提供方而言,它只需要根据接口实现对应方法。并且把配置文件放在META-INF/services/ 告知服务调用方。

服务调用方

服务调用方根据约定,使用ServiceLoader来遍历实现该约定的实现类。加载进内存中供服务提供方调用。

为了加深理解,我为你画了一张图:
在这里插入图片描述

ServiceLoader和类加载器的关系

它和类加载器又有什么关系?我通过代码为你分析,你看:

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

如果你不指定类加载器,load方法默认获取当前线程的类加载器去加载该类。它通过扫描META-INF/services/目录下的配置文件,找到服务接口的实现类的全限定名。有了全限定名就等于掌握了花名册,当我们遍历ServiceLoader时,服务加载器会通过类加载器将这些服务提供者实例化,这样我们就可以使用这些服务了。


文中重要部分解析

命名空间

我在上面Tomcat多版本共存中提到命名空间,什么是命名空间?每个类加载器实例其实就是一个命名空间。也就是说,在一个应用程序中允许一个类被多个类加载器实例加载,并且共存于应用程序中。暂停30秒,思考一下,这样会出现什么问题。
好,在回答这个问题之前,我为你展示一个代码:

try {
    URL[] urls = new URL[] {new URL("file:C:\\Users\\xxx\\Desktop\\")};
    URLClassLoader customClassLoader1 = new URLClassLoader(urls);
    URLClassLoader customClassLoader2 = new URLClassLoader(urls);

    Class cls1 = customClassLoader1.loadClass("org.kfaino.jvm.Building");
    Class cls2 = customClassLoader2.loadClass("org.kfaino.jvm.Building");

    Object obj1 = cls1.newInstance();
    Object obj2 = cls2.newInstance();

    System.out.println("obj1 class: " + obj1.getClass());
    System.out.println("obj2 class: " + obj2.getClass());

    System.out.println("obj1 class loader: " + obj1.getClass().getClassLoader());
    System.out.println("obj2 class loader: " + obj2.getClass().getClassLoader());
	// Building对象已经重写hashCode和equals方法
    System.out.println("obj1 equals obj2: " + obj1.equals(obj2));

} catch (Exception e) {
    e.printStackTrace();
}

在Building已经重写hashCode和equals方法的前提下,obj1 equals obj2: 会是true吗?我们看下结果:

建筑蓝图已被创建!
建筑蓝图已被创建!
obj1 class: class org.kfaino.webTemplate.jvm.Building
obj2 class: class org.kfaino.webTemplate.jvm.Building
obj1 class loader: java.net.URLClassLoader@da236ecf
obj2 class loader: java.net.URLClassLoader@8e02f9da
obj1 equals obj2: false

Process finished with exit code 0

结果是否定的,和我之前说的吻合。也就是说使用不同的类加载器,不同类加载器的对象(命名空间不同),在JVM中就是类型不一致的。

生产环境中的热部署

BackgroundProcessor 后台线程,需要周期性地检查(checkResources())文件的状态。处于对性能方面的考虑,在生产环境中,通常会关闭 Tomcat 的热部署功能。

SPI配置文件存放位置META-INF/services/可以更改吗?

查阅官方文档,我们可以知道SPI是JDK内置的一种服务提供发现机制。在SPI机制中,服务提供者的配置文件默认放在META-INF/services/目录下。这是Java SPI规范的一部分,无法更改。


总结

至此,本篇完结。我们来回顾下:首先,我带你创建并使用了类加载器完成从本地文件夹下加载自己的类。这些工作我们可以通过Java自带的类加载器来简化,我也为你演示其用法。当然,我们在使用自定义类加载器要格外注意,因为涉及到类初始化往往你会碰到一些不可预见的诡异BUG。然后,我为你介绍自定义类加载器场景的使用场景。顺便看一下Tomcat和Java是怎么用自定义类加载器的特性实现高级功能的。


常见面试题

如何自定义类加载器?

在什么情况下会需要自定义类加载器?

Tomcat的类加载器有什么特点?如何实现热部署和多版本共存?#### 什么是ServiceLoader和SPI,它们如何利用类加载器?

类加载器可能存在的问题有哪些?


参考文献

  1. Java Guide SPI机制详解
  2. class-loader-howto
  3. baeldung-java-classloaders
  4. Inside the Java Virtual Machine
  5. 老大难的 Java ClassLoader 再不理解就老了

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

苍穹外卖-day09

苍穹外卖-day09 本项目学自黑马程序员的《苍穹外卖》项目&#xff0c;是瑞吉外卖的Plus版本 功能更多&#xff0c;更加丰富。 结合资料&#xff0c;和自己对学习过程中的一些看法和问题解决情况上传课件笔记 视频&#xff1a;https://www.bilibili.com/video/BV1TP411v7v6/?sp…

优化企业集成架构:iPaaS集成平台助力数字化转型

前言 在数字化时代全面来临之际&#xff0c;企业正面临着前所未有的挑战与机遇。技术的迅猛发展与数字化转型正在彻底颠覆各行各业的格局&#xff0c;不断推动着企业迈向新的前程。然而&#xff0c;这一数字化时代亦衍生出一系列复杂而深奥的难题&#xff1a;各异系统之间数据…

山水TW91耳机恢复出厂设置

当出现左右耳不互联或者设备无法配对时&#xff0c;可按照以下操作进行重置&#xff1a; ①先在手机端删除“SANSUI”耳机的配对信息。 ②耳机开机未连接状态下&#xff0c;连击4次左/右耳功能键后复位&#xff0c;耳机自动关机清除配对。&#xff08;提示已关机表示操作正确&a…

如何快速用Go获取短信验证码

要用Go获取短信验证码&#xff0c;通常需要连接到一个短信服务提供商的API&#xff0c;并通过该API发送请求来获取验证码。由于不同的短信服务提供商可能具有不同的API和授权方式&#xff0c;我将以一个简单的示例介绍如何使用Go语言来获取短信验证码。 在这个示例中&#xff0…

使用 OpenCV 和 GrabCut 算法进行交互式背景去除

一、说明 我想&#xff0c;任何人都可以尝试从图像中删除背景。当然&#xff0c;有大量可用的软件或工具能够做到这一点&#xff0c;但其中一些可能很昂贵。但是&#xff0c;我知道有人使用窗口绘画3D魔术选择或PowerPoint背景去除来删除背景。 如果您是计算机视觉领域的初学者…

C#时间轴曲线图形编辑器开发1-基本功能

目录 一、前言 1、简介 2、开发过程 3、工程下载链接 二、基本功能实现 1、绘图面板创建 &#xff08;1&#xff09;界面布置 &#xff08;2&#xff09;显示面板代码 &#xff08;3&#xff09; 面板水平方向、竖直方向移动功能实现 &#xff08;4&#xff09;面板放…

2024届IC秋招兆易创新数字IC后端笔试面试题

数字IC后端实现PR阶段设计导入需要哪些文件&#xff1f; 设计导入需要的文件如下图所示。这个必须熟练掌握。只要做过后端训练营项目的&#xff0c;对这个肯定是比较熟悉的。大家还要知道每个input文件的作用是什么。 在吾爱IC后端训练营Cortexa7core项目中&#xff0c;你认为…

华为刷题:HJ3明明随机数

import java.util.Scanner;// 注意类名必须为 Main, 不要有任何 package xxx 信息 public class Main {public static void main(String[] args) {Scanner scan new Scanner(System.in);int N scan.nextInt();int[] arr new int[N];for (int i 0; i < N; i) {int n sca…

数据库压力测试方法小结

一、前言 在前面的压力测试过程中&#xff0c;主要关注的是对接口以及服务器硬件性能进行压力测试&#xff0c;评估请求接口和硬件性能对服务的影响。但是对于多数Web应用来说&#xff0c;整个系统的瓶颈在于数据库。 原因很简单&#xff1a;Web应用中的其他因素&#xff0c;…

基于ChatGPT聊天的零样本信息提取7.25

基于ChatGPT聊天的零样本信息提取 摘要介绍ChatIE用于零样本IE的多轮 QA 实验总结 摘要 零样本信息提取&#xff08;IE&#xff09;旨在从未注释的文本中构建IE系统。由于很少涉及人类干预&#xff0c;因此具有挑战性。 零样本IE减少了数据标记所需的时间和工作量。最近对大型…

【Git】Git的概念安装工作区-暂存区-版本库

文章目录 Git概念-版本控制器Git安装Centos平台&ubuntu Git基本操作创建Git本地仓库配置Git 认识⼯作区、暂存区、版本库添加文件查看.git文件总结添加文件场景2 Git概念-版本控制器 引入 我们在编写各种⽂档时&#xff0c;为了防⽌⽂档丢失&#xff0c;更改失误&#xff0…

sql server表值函数

一、创建测试表 Employees 二、创建表值函数 -- DROP FUNCTION TableIntSplit;CREATE FUNCTION TableIntSplit(Text NVARCHAR(4000),Sign NVARCHAR(4000)) RETURNS tempTable TABLE(Id INT ) AS BEGIN DECLARE StartIndex INT DECLARE FindIndex INT DECLARE Content VARCHAR(…

Spring之IoC源码分析及设计思想(一)——BeanFactory

关于Spring的IOC Spring 是一个开源的 Java 平台&#xff0c;它提供了一种简化应用程序开发的框架。它是一个分层的框架&#xff0c;包括两个主要的内核&#xff1a;控制反转&#xff08;IOC&#xff09;和面向切面编程&#xff08;AOP&#xff09;。IOC 允许应用程序将组件之…

STM32MP157驱动开发——按键驱动(POLL 机制)

文章目录 “POLL ”机制&#xff1a;APP执行过程驱动使用的函数应用使用的函数pollfd结构体poll函数事件类型实现原理 poll方式的按键驱动程序(stm32mp157)gpio_key_drv.cbutton_test.cMakefile修改设备树文件编译测试 “POLL ”机制&#xff1a; 使用休眠-唤醒的方式等待某个…

BOB_1.0.1靶机详解

BOB_1.0.1靶机详解 靶机下载地址&#xff1a;https://download.vulnhub.com/bob/Bob_v1.0.1.ova 这个靶机是一个相对简单的靶机&#xff0c;很快就打完了。 找到ip地址后对IP进行一个单独的扫描&#xff0c;发现ssh端口被改到25468了&#xff0c;等会儿登陆时候需要用到。 目…

SQL注入原理分析

前言 order by的作用及含义 order by 用于判断显示位&#xff0c;order by 原有的作用是对字段进行一个排序&#xff0c;在sql注入中用order by 来判断排序&#xff0c;order by 1就是对一个字段进行排序&#xff0c;如果一共四个字段&#xff0c;你order by 5 数据库不知道怎么…

物联网场景中的边缘计算解决方案有哪些?

在物联网场景中&#xff0c;边缘计算是一种重要的解决方案&#xff0c;用于在物联网设备和云端之间进行实时数据处理、分析和决策。HiWoo Box作为工业边缘网关设备&#xff0c;具备边缘计算能力&#xff0c;包括单点公式计算、Python脚本编程以及规则引擎&#xff0c;它为物联网…

使用Kmeans算法完成聚类任务

聚类任务 聚类任务是一种无监督学习任务&#xff0c;其目的是将一组数据点划分成若干个类别或簇&#xff0c;使得同一个簇内的数据点之间的相似度尽可能高&#xff0c;而不同簇之间的相似度尽可能低。聚类算法可以帮助我们发现数据中的内在结构和模式&#xff0c;发现异常点和离…

原生求生记:揭秘UniApp的原生能力限制

文章目录 1. 样式适配问题2. 性能问题3. 原生能力限制4. 插件兼容性问题5. 第三方组件库兼容性问题6. 全局变量污染7. 调试和定位问题8. 版本兼容性问题9. 前端生态限制10. 文档和支持附录&#xff1a;「简历必备」前后端实战项目&#xff08;推荐&#xff1a;⭐️⭐️⭐️⭐️…

服务网格技术对比:深入比较Istio、Linkerd和Envoy等服务网格解决方案的优缺点

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…