JVM 基础篇:类加载器
一.了解JVM
1.1什么是JVM
JVM是Java Virtual Machine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
虚拟机可以分为系统虚拟机和程序虚拟机
- 系统虚拟机是一种虚拟化技术,它模拟整个计算机硬件环境,包括处理器、内存、存储和外部设备。它的主要目标是在单个物理计算机上同时运行多个操作系统。每个虚拟机都具有独立的操作系统和应用程序,就像在不同的物理计算机上运行一样。系统虚拟机的例子包括VMware、VirtualBox和Hyper-V。
- 程序虚拟机是一种虚拟化技术,它仅模拟计算机上的一个单独的应用程序运行环境,而不是整个操作系统。它的主要目标是提供一个独立的运行环境,使应用程序能够在不同的操作系统上运行而无需修改。程序虚拟机通常用于解决跨平台兼容性的问题,模拟一个应用程序的运行环境,使应用程序能够跨平台运行。常见的程序虚拟机包括Java虚拟机(JVM)。
1.2JRE/JDK/JVM
- JDK(Java Development Kit) 是整个JAVA的核心,包括了Java运行环境JRE(Java Runtime Envirnment),一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API)。
- JRE(Java Runtime Environment,Java运行环境), 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)。JRE是Java运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器和调试器)
- JVM(Java Virtual Mechinal)是JRE的一部分,叫做JAVA虚拟机,它是整个java实现跨平台的最核心的部分,负责解释执行并运行字节码文件(.class)。
1.3JVM的功能
1.4常见的JVM
1.5JVM的整体结构
二.字节码文件
2.1字节码文件的组成
这里重点看一下基本信息、常量池和方法
2.1.1基本信息
Magic魔数
主副版本号
主版本号不兼容会引发以下错误:
2.1.2常量池
字节码文件中常量池的作用:避免相同的内容重复定义,节省空间。
我们看下面一个案例
public class ConstantPoolTest2 {public static final String a1 = "abc";public static final String a2 = "abc";public static final String abc = "abc";public static void main(String[] args) {ConstantPoolTest2 constantPoolTest = new ConstantPoolTest2();}
}
首先看字段,我们看a1的常量值实际上指向了常量池中的8号
这实际上是常量池中的一个String_info,但是里面并没有存储真正的字符串字面量,而是一个10号常量的索引
10号常量存储的就是真正的字符串字面量
为什么字段不直接存储常量池里字符串字面量的索引?而要先找String_info,然后再找字符串字面量
因为字节码文件被加载的时候会把常量池中String_info加载到字符串常量池中,所以String_info需要存一份引用
那为什么String_info里不直接存储字符串字面量,而是存一份索引?
字段中的变量名也可能要引用常量池里的字符串字面量,如果用String_info存储字符串字面量则不合理,因为字段中的变量名并不是一个字符串的对象
符号引用
2.1.3方法
字节码中的方法区域是存放字节码指令的核心位置,字节码指令的内容存放在方法的Code属性中。
注意:
- iload是将值复制一份,局部变量表中的值还在
- istore是将操作数栈的栈顶弹出,并放入局部变量表的某个位置,此时操作数栈的值是不存在的
i=i++的执行流程
iinc x by y:将局部变量表的x号位置增加y
很明显操作数栈中的值一直都是0,只要istore,那么局部变量表中的值也会被覆盖,所以最终i为0
i=++i的执行流程
由于iinc指令在iload指令之前,所以i的最终值是1
2.2字节码文件常用工具
2.2.1 Java 字节码的字节码查看器:javap -v
2.2.2 阿里arthas
Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,大大提升线上问题排查效率。
详细请看官网:简介 | arthas (aliyun.com)
dashboard显示当前系统的实时数据面板,-i刷新实时数据的时间间隔 (ms),-n刷新实时数据的次数
dump已加载类的字节码文件到特定目录:
dump -d 特定目录 类的全限定名(即包名+类名)
反编译已加载类,得到源码:
jad 类的全限定名(即包名+类名)
三.类的生命周期
3.1加载阶段
- 类加载器ClassLoader根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息
- 类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到内存的方法区中。生成一个InstanceKlass对象,保存类的所有信息,里边还包含实现特定功能比如多态的信息。
- 同时,Java虚拟机还会在堆中生成一份与方法区中数据类似的java.lang.Class对象。作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中)
对于开发者来说,只需要访问堆中的Class对象而不需要访问方法区中所有信息。这样Java虚拟机就能很好地控制开发者访问数据的范围。
加载阶段过后,字节码文件就已经被读取到了内存中,并且会创建一个代表该类的Class
对象。
3.2连接阶段
- 第一个环节是验证,验证的主要目的是检测Java字节码文件是否遵守了《Java虚拟机规范》中的约束。这个阶段一般不需要程序员参与。
- 准备阶段为静态变量(static)分配内存并设置初始值。准备阶段只会给静态变量赋初始值,而每一种基本数据类型和引用数据类型都有其初始值。但注意如果字段是
final
修饰的基本类型或者字符串常量(经过编译器优化),则会在准备阶段直接赋予最终值。 - 解析阶段主要是将常量池中的符号引用替换为直接引用。 符号引用就是在字节码文件中使用编号来访问常量池中的内容。 直接引用不再使用编号,而是使用内存中地址进行访问具体的数据。
3.3初始化阶段
初始化阶段会执行字节码文件中 clinit 部分的字节码指令。
<clinit>方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
以下几种方式会导致类的初始化:
- 访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化。
- 调用Class.forName(String className),注意 Class<?> forName(String name, boolean initialize, ClassLoader loader) 这个构造器可以指定不初始化
- new一个该类的对象时。
- 执行Main方法的当前类。
我们可以在JVM设置里添加 -XX:+TraceClassLoading 参数,这样可以看到有哪些类被加载
通过测试以下程序可以发现
类被加载时不一定会被初始化,而是在需要初始化的时候才初始化。
类加载但不初始化的情况
- Class<?> forName(String name, boolean initialize, ClassLoader loader) 这个构造器可以指定不初始化
- ClassLoader的loadClass(String className);方法也只会加载并编译某类,并不会对其执行初始化
- 类名.class
面试题分析
clinit指令在以下情况下不会出现
- 无静态代码块且无静态变量赋值语句。
- 有静态变量的声明,但是没有赋值语句。如 public static int a;
- 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化。如 public final static int a = 1;
当出现继承关系时
- 直接访问父类的静态变量,不会触发子类的初始化。
- 子类的初始化clinit调用之前,会先调用父类的clinit初始化方法。
如果把new B02()去掉会怎么样呢?
数组的创建不会导致数组中元素的类进行初始化。
四.类加载器
类加载器的设计JDK8和8之后的版本差别较大,JDK8及之前的版本中默认的类加载器有如下几种:
4.1启动类加载器(引导类加载器,根加载器,Bootstrap)
启动类加载器是最底层的类加载器,是虚拟机的一部分,它是由C++语言实现的,无法在Java代码中直接获取到,且没有父加载器(这里形容的是父子关系的层次结构,并非继承关系),也没有继承java.lang.lassLoader类。
它主要负责加载由系统属性 “sun.boot.cass.path” 指定的路径下的核心类库(即<JAVA_HOME>/jre/lib),包含了Object、String、Math、装箱类型、日期类等核心类
public class Demo {public static void main(String[] args) {//Bootstrap 引导类加载器//打印为null,是因为Bootstrap是C++实现的ClassLoader classLoader = Object.class.getClassLoader();System.out.println(classLoader);//查看引导类加载器会加载那些jar包URL[] urLs = Launcher.getBootstrapClassPath().getURLs();for (URL urL : urLs) {System.out.println(urL);}}
}
4.2扩展类加载器(ExtClassLoader)
- 全类名:sum.misc.Launch$ExtClassLoader,Java语言实现。
- 扩展类加载器的父加载器是Bootstrap启动类加载器 (注:不是继承关系)
- 扩展类加载器负责加载<JAVA_HOME>\jre\lib\ext目录下的类库。
注: JDK9是jdk.internal.loader.ClassLoaders$PlatformClassLoader类
4.3应用程序类加载器(系统类加载器,AppClassLoader)
- 全类名: sun.misc.Launcher$AppClassLoader
- 系统类加载器的父加载器是ExtClassLoader扩展类加载器 (注:不是继承关系)
- 系统类加载器负责加载 classpath环境变量所指定的类库,包括项目中自己编写的类文件以及第三方jar包中的类文件,是用户自定义类的默认类加载器。
注: JDK9是jdk.internal.loader.ClassLoaders$AppClassLoader类
4.4三者之间的关系
- AppClassLoader的父加载器是ExtClassLoader
- ExtClassLoader的父加载器是Bootstrap
- Bootstrap是根加载器
- AppClassLoader和ExtClassLoader都实现了抽象类ClassLoader
三者之间是没有继承关系的,而是一种组合关系
- 抽象类ClassLoader有一个字段parent, AppClassLoader和ExtClassLoader通过设置该字段引用,指定父加载器。(是组合关系)
- AppClassLoader 的parent指向 ExtClassLoader
- ExtClassLoader 的parent指向 null,(null的原因是因为Bootstrap是C++实现的,通过代码中逻辑判断来转向Bootstrap)
4.5双亲委派机制
双亲委派机制指的是:自底向上查找是否加载过,再由顶向下进行加载。
双亲委派机制的好处
- 避免类的重复加载:当父加载器已经加载该类时,就没有必要子加载器再加载一遍,保证被加载类的唯一性。
- 避免核心类篡改:通过双亲委派机制,让顶层的类加 载器去加载核心类,避免恶意代码 替换JDK中的核心类库,比如 java.lang.String,确保核心类 库的完整性和安全性。
我们可以看看下面的程序
这里我自定义了一个String类,并且类的全限定名和JDK内置的String类完全一样。但运行结果如下:
异常提示:在类 java.lang.String 中找不到 main 方法。
why?这里程序在执行时识别的是src中的java.lang.String,src就是classpath,因此会调用系统加载器。但根据双亲委派机制,系统加载器会逐层委派上层加载器来加载此类,在委派的时候,最上层的加载器是根加载器,即根加载器优先级最高。而根加载器能够在jre\lib\rt.jar包中找到一个重名的java.lang.String(即jdk自带的String),因此根据双亲委派最终会由最顶层的根加载器来执行jdk自带的java.lang.String。显然,jdk中的String并没有main()方法,因此报错找不到main()
4.6自定义类加载器
Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。
loadClass默认实现如下:
再看看loadClass(String name, boolean resolve)函数,双亲委派机制的核心代码就位于这里
从上面代码可以明显看出,loadClass(String, boolean)函数即实现了双亲委派模型!整个大致过程如下:
- 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
- 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);)或者是调用bootstrap类加载器来加载。
- 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。
整个调用过程如下图所示
在自定义ClassLoader的子类时候,我们常见的会有两种做法:
- 重写loadClass()方法:这样会打破双亲委派模型,可能会导致一些Java的核心类无法加载,不建议重写
- 重写findClass()方法:是在双亲委派模型的框架下进行小范围的改动,建议重写
实战代码如下:
public class MyClassLoader extends ClassLoader {private String root;@Overrideprotected 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 = root + File.separatorChar+ className.replace('.', File.separatorChar) + ".class";try {InputStream ins = Files.newInputStream(Paths.get(fileName));ByteArrayOutputStream baos = new ByteArrayOutputStream();int bufferSize = 1024;byte[] buffer = new byte[bufferSize];int length = 0;while ((length = ins.read(buffer)) != -1) {baos.write(buffer, 0, length);}return baos.toByteArray();} catch (IOException e) {e.printStackTrace();}return null;}public String getRoot() {return root;}public void setRoot(String root) {this.root = root;}public static void main(String[] args) {MyClassLoader classLoader = new MyClassLoader();classLoader.setRoot("D:\\");Class<?> testClass = null;try {//需要为com.字节码文件.classloader.A 格式,否则defineClass方法会抛异常testClass = classLoader.loadClass("com.字节码文件.classloader.A");Object object = testClass.newInstance();System.out.println(object.getClass().getClassLoader());} catch (Exception e) {e.printStackTrace();}}
}
注意
- 实例中的 class文件不能放在classpath下,否则根据双亲委派机制会被应用程序类加载器加载,而不会通过我们自定义类加载器来加载。
- 这里传递的文件名需要是类的全限定性名称,即 com.字节码文件.classloader.A 格式的,因为 defineClass 方法是按这种格式进行处理的。
4.7Launcher类
AppClassLoader和ExtClassLoader是Launcher的静态内部类,在程序启动时JVM会创建Launcher对象,Launcher构造器会同时会创建扩展类加载器和应用类加载器。
相关文章:

JVM 基础篇:类加载器
一.了解JVM 1.1什么是JVM JVM是Java Virtual Machine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需…...

文本批量处理,高效便捷的管理利器!
您是否曾经为了批量处理文本数据而烦恼?冗长的文本文件,繁琐的处理步骤,让您的工作变得异常困难。现在,我们向您推荐一款文本批量处理工具,它能够快速、准确地处理大量文本数据,让您的管理工作更加高效便捷…...

百度松果20231022作业
越狱 盒子与球 斯特林第二类数(用dp求)*盒子的阶乘 int dp[11][11]; //n>k int A(int x){int res1;fer(i,2,x1)res*i;return res; } signed main(){IOS;dp[2][1]dp[2][2]dp[1][1]1;fer(i,3,11){dp[i][1]1;fer(j,2,i){dp[i][j]j*dp[i-1][j]dp[i-1][j-…...

cropper+jq(图片裁剪上传)
<link rel"stylesheet" href"../../cropper/cropper.css"> <script type"text/javascript" src"../../cropper/cropper.js"></script> 没有引入jquery的原因 引入jquery <script src"../jquery-1.10.2.js…...
运行 `npm install` 时的常见问题与解决方案
运行 npm install 时的常见问题与解决方案 问题一:网络连接问题 描述: 运行 npm install 时,可能会遇到网络连接问题,导致无法正常下载依赖包。 报错示例: npm ERR! network connection timed outnpm ERR! connect…...

【2023年11月第四版教材】软考高项极限冲刺篇笔记(1)
1 你要接受一些观点 1、不明白的不要去试图理解了,死记硬背 2、要快速过知识点,卡住是不行的,慢也是没有任何作用的。 3、将厚厚的知识,变为薄薄的重点。标红必背 4、成熟度等级,很多知识领域都有,就是评价在一个领域达到的级别。 5、记得搜一下当年的高频科技词汇 6、选…...

http post协议发送本地压缩数据到服务器
1.客户端程序 import requests import os # 指定服务器的URL url "http://192.168.1.9:8000/upload"# 压缩包文件路径 folder_name "upload" file_name "test.7z" headers {Folder-Name: folder_name,File-Name: file_name } # 发送POST请求…...

系列十三、Redis的哨兵机制
一、概述 Sentinel(哨兵)是Redis的高可用解决方案,由一个或者多个Sentinel实例组成集群,可以监视任意多个主服务器,以及这些服务器下属的所有从服务器,并在被监视的主服务器下线或者宕机时,自动…...
设置Unity URP管线中的渲染开关
在上一节中,我们添加了外轮廓线,但这个外轮廓线在所有使用该Shader的网格上是始终存在的。 如果我们想做一个开关,决定是否打开外轮廓线时,我们可以使用一个新的Uniform bool值,然后判断bool是否为true来开启外轮廓线…...

神器抓包工具 HTTP Analyzer v7.5 的下载,安装,使用,破解说明以及可能遇到的问题
文章目录 1、HTTP Analyzer 工具能干什么?2、HTTP Analyzer 如何下载?3、如何安装?4、如何使用?5、如何破解?6、Http AnalyzerStd V7可能遇到的问题 1、HTTP Analyzer 工具能干什么? A1:HTTP A…...

虚幻引擎:代理
一、代理类型 1.单薄代理 特点:允许有返回值,允许有参数,只可以一对一的传递消息就算绑定多个,但是总会被最后一个覆盖 2.多播代理 特点:不允许有返回值,允许有参数允许一对多传递消息 3.动态代理 …...

Openssl数据安全传输平台004:Socket C-API封装为C++类 / 服务端及客户端代码框架和实现
文章目录 0. 代码仓库1. 客户端C API2. 客户端C API的封装分析2.1 sckClient_init()和sckClient_destroy()2.2 sckClient_connect2.3 sckClient_closeconn()2.4 sckClient_send()2.5 sckClient_rev()2.6 sck_FreeMem 3. 客户端C API4. 服务端C API5. 服务端C6. 客户端和服务端代…...

网络协议--Traceroute程序
8.1 引言 由Van Jacobson编写的Traceroute程序是一个能更深入探索TCP/IP协议的方便可用的工具。尽管不能保证从源端发往目的端的两份连续的IP数据报具有相同的路由,但是大多数情况下是这样的。Traceroute程序可以让我们看到IP数据报从一台主机传到另一台主机所经过…...

Centos磁盘问题小纪
场景说明 放个windows的图片镇楼,在给一个centos的来说明问题,咋了,好好的系统,啥也不能干了 来先上一波命令分析下问题 查看挂载 mount 重新挂载数据 mount -o remount, rw / 查看磁盘 df -h 查看分区挂载详情 rw读写权限 mount …...

计算机网络第三章习题
1.假定1km长的CSMA/CD网络的数据率为1Gb/s。设信号在网络上的传播 速率为200000km/s。求能够使用此协议的最短帧长. 问题刨析: 逻辑链: 最短帧长数据传输率x争用期2τ(2倍端到端所需要的时间) 题目已经给出数据率为1Gb/s,所以我们要知道争用期2τ是多少. 端到端所需要的时间信…...
Layui弹出层关闭后页面自动刷新的用法以及建议
首先在HTML中定义一个查询按钮 <div class"layui-inline"><button class"layui-btn" id"searchBtn" lay-submit lay-filter"data-search-btn" > <i class"layui-icon layui-icon-search">查询</i&…...

PHP 在线考试管理系统mysql数据库web结构layUI布局apache计算机软件工程网页wamp
一、源码特点 PHP 在线考试管理系统是一套完善的web设计系统 layUI技术布局 ,对理解php编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。 PHP 在线考试系统1 代码 https://download.csdn.net/download/qq_41…...

【茫茫架构路】1. Class File字节码文件解析
本文所有内容的JDK版本为 OpenJDK 11 JDK11 Class File官方说明。 Java解析字节码文件源码参考,以下为部分字节码解析源码展示。 public ClassFile(DataInputStream in) {try {//magic: 0xCAFEBABEthis.magic ClassReader.readInt(in);System.out.println("m…...

Golang笔记
01 = 和 := 的区别? 前者是赋值变量,后者是定义变量 02 指针的作用 指针指向变量的地址,在64位机器上占8个字节 【1 字节(Byte)= 8 位(bit) 1 千字节(KB,Kilobyte)= 1,024 字节(2^10 字节)】 作用 取址然后取值swap函数 交换变量的值指针接收器来改变结构体里面…...

在Linux上安装RStudio工具并实现本地远程访问【内网穿透】
文章目录 前言1. 安装RStudio Server2. 本地访问3. Linux 安装cpolar4. 配置RStudio server公网访问地址5. 公网远程访问RStudio6. 固定RStudio公网地址 前言 RStudio Server 使你能够在 Linux 服务器上运行你所熟悉和喜爱的 RStudio IDE,并通过 Web 浏览器进行访问…...

基于Flask实现的医疗保险欺诈识别监测模型
基于Flask实现的医疗保险欺诈识别监测模型 项目截图 项目简介 社会医疗保险是国家通过立法形式强制实施,由雇主和个人按一定比例缴纳保险费,建立社会医疗保险基金,支付雇员医疗费用的一种医疗保险制度, 它是促进社会文明和进步的…...

dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...

ElasticSearch搜索引擎之倒排索引及其底层算法
文章目录 一、搜索引擎1、什么是搜索引擎?2、搜索引擎的分类3、常用的搜索引擎4、搜索引擎的特点二、倒排索引1、简介2、为什么倒排索引不用B+树1.创建时间长,文件大。2.其次,树深,IO次数可怕。3.索引可能会失效。4.精准度差。三. 倒排索引四、算法1、Term Index的算法2、 …...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...

QT: `long long` 类型转换为 `QString` 2025.6.5
在 Qt 中,将 long long 类型转换为 QString 可以通过以下两种常用方法实现: 方法 1:使用 QString::number() 直接调用 QString 的静态方法 number(),将数值转换为字符串: long long value 1234567890123456789LL; …...

深度学习习题2
1.如果增加神经网络的宽度,精确度会增加到一个特定阈值后,便开始降低。造成这一现象的可能原因是什么? A、即使增加卷积核的数量,只有少部分的核会被用作预测 B、当卷积核数量增加时,神经网络的预测能力会降低 C、当卷…...

网站指纹识别
网站指纹识别 网站的最基本组成:服务器(操作系统)、中间件(web容器)、脚本语言、数据厍 为什么要了解这些?举个例子:发现了一个文件读取漏洞,我们需要读/etc/passwd,如…...
代码随想录刷题day30
1、零钱兑换II 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

Proxmox Mail Gateway安装指南:从零开始配置高效邮件过滤系统
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「storms…...