JVM-Java字节码技术笔记
Java字节码技术
Java字节码是java代码编译后的中间代码格式,JVM需要读取并解析字节码才能执行相应的任务
-
获取字节码简介:由单字节(
byte
)的指令组成- 操作码(
指令
), 主要由类型前缀
和操作名称
两部分组成。 - 根据指令的性质,主要分为四个大类:
- 栈操作指令,包括与局部变量交互的指令
- 程序流程控制指令
- 对象操作指令,包括方法调用指令
- 算术运算以及类型转换指令
- 操作码(
-
获取字节码清单
-
用
javap
工具来获取 class 文件中的指令清单,专门用于反编译 class 文件。 -
Compiled from "HelloByteCode.java" public class demo.jvm0104.HelloByteCode {public demo.jvm0104.HelloByteCode();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: new #2 // class demo/jvm0104/HelloByteCode3: dup4: invokespecial #3 // Method "<init>":()V7: astore_18: return }
-
-
解读字节码清单
-
public demo.jvm0104.HelloByteCode(); // 如果不定义任何构造函数,就会有一个默认的无参构造函数.这是 Java 编译器生成的, 而不是运行时JVM自动生成的。
-
//每个构造函数中会先调用super类的构造函数,默认构造函数中有些字节码指令来干这个事情 //解析的java/lang/Object 默认继承了Object类 public demo.jvm0104.HelloByteCode();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: return
-
-
查看class中的常量池
- 常量池就是一个常量的大字典,使用编号的方式把程序里用到的各类常量统一管理起来,这样在字节码操作里,只需要引用编号即可。
- 大多数时候指的是
运行时常量池
。运行时常量池里面的常量主要是由 class 文件中的常量池结构体
组成的。 - 查看常量池信息的命令:javap -c -verbose demo.jvm0104.HelloByteCode
- 反编译class的时候,指定-verbose选项,会输出附加信息
#1 = Methodref #4.#13 // java/lang/Object."<init>":()V
,#1
:常量编号,该文件中其他地方可以引用=
:分隔符Methodref
:表明这个常量指向的是一个方法;具体是哪个类的哪个方法呢? 类指向的#4
, 方法签名指向的#13
;
-
查看方法信息
-
//main方法编译结果public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=2, args_size=1
-
方法描述: ([Ljava/lang/String;)V:
- 小括号内是入参信息/形参信息;
- 左方括号表述数组;
L
表示对象;- 后面的
java/lang/String
就是类名称; - 小括号后面的
V
则表示这个方法的返回值是void
; - 方法的访问标志也很容易理解
flags: ACC_PUBLIC, ACC_STATIC
,表示 public 和 static。
-
还可以看到执行该方法时需要的栈(stack)深度是多少,需要在局部变量表中保留多少个槽位, 还有方法的参数个数:
stack=2, locals=2, args_size=1
。
-
-
线程栈与字节码执行模型
- 每个线程都有一个独属于自己的线程栈(JVM stack),用于存储
栈帧
(Frame)。每一次方法调用,JVM都会自动创建一个栈帧。栈帧
由操作数栈
,局部变量数组
以及一个class 引用
组成。class 引用
指向当前方法在运行时常量池中对应的 class)。
局部变量数组
也称为局部变量表
(LocalVariableTable), 其中包含了方法的参数,以及局部变量。 局部变量数组的大小在编译时就已经确定: 和局部变量+形参的个数有关,还要看每个变量/参数占用多少个字节。操作数栈是一个 LIFO 结构的栈, 用于压入和弹出值。 它的大小也在编译时确定。- 有一些操作码/指令可以将值压入“操作数栈”; 还有一些操作码/指令则是从栈中获取操作数,并进行处理,再将结果压入栈。操作数栈还用于接收调用其他方法时返回的结果值。
- 每个线程都有一个独属于自己的线程栈(JVM stack),用于存储
-
方法体中的字节码解读
-
0: new #2 // class demo/jvm0104/HelloByteCode3: dup4: invokespecial #3 // Method "<init>":()V7: astore_18: return
-
前面的数字:间隔不相等的原因是, 有一部分操作码会附带有操作数, 也会占用字节码数组中的空间。
-
例如:
new
就会占用三个槽位: 一个用于存放操作码指令自身,两个用于存放操作数。因此,下一条指令dup
的索引从3
开始。
-
-
对象初始化指令:new 指令, init 以及 clinit 简介
-
创建类实例生成操作码
-
0: new #2 // class demo/jvm0104/HelloByteCode 创建对象,但没有调用构造函数 3: dup // 用来调用某些特殊方法的,即构造函数 4: invokespecial #3 // Method "<init>":()V 用于复制栈顶的值。构造函数调用不会返回值,所以如果没有 dup 指令, 在对象上调用方法并初始化之后,操作数栈就会是空的,在初始化之后就会出问题。所以在构造函数返回之后,可以将对象实例赋值给局部变量或某个字段
-
接下来指令
-
astore {N} or astore_{N} – 赋值给局部变量,其中 {N} 是局部变量表中的位置。 putfield – 将值赋给实例字段 putstatic – 将值赋给静态字段
-
在调用构造函数的时候,还会执行另一个类似的方法
<init>
,甚至在执行构造函数之前就执行了。 -
还有一个可能执行的方法是该类的静态初始化方法
<clinit>
, 但<clinit>
并不能被直接调用,而是由这些指令触发的:new
,getstatic
,putstatic
orinvokestatic
。
-
-
栈内存操作指令
- 最基础的是
dup
和pop
指令。dup
指令复制栈顶元素的值。pop
指令则从栈中删除最顶部的值。
- 复杂一点的指令:比如,
swap
,dup_x1
和dup2_x1
。swap
指令可交换栈顶两个元素的值,例如A和B交换位置(图中示例4);dup_x1
将复制栈顶元素的值,并在栈顶插入两次(图中示例5);dup2_x1
则复制栈顶两个元素的值,并插入第三个值(图中示例6)。
- dup 指令:复制栈顶的值,并将复制的值压入栈。
- dup_x1 指令:复制栈顶的值,并将复制的值插入到最上面 2 个值的下方。
- dup2_x1 指令:复制栈顶 1 个 64 位/或 2 个 32 位的值, 并将复制的值按照原始顺序,插入原始值下面一个 32 位值的下方。
- 最基础的是
-
局部变量表
-
stack
主要用于执行指令,而局部变量则用来保存中间结果,两者之间可以直接交互。 -
javac -g demo/jvm0104/*.java(生成调试信息的
-g
参数) -
javap -c -verbose demo/jvm0104/LocalVariableTest (反编译)
-
代码
-
//移动平均数 public class MovingAverage {private int count = 0;private double sum = 0.0D;public void submit(double value){this.count ++;this.sum += value;}public double getAvg(){if(0 == this.count){ return sum;}return this.sum/this.count;} }public class LocalVariableTest {public static void main(String[] args) {MovingAverage ma = new MovingAverage();int num1 = 1;int num2 = 2;ma.submit(num1);ma.submit(num2);double avg = ma.getAvg();} }
-
反编译 public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=3, locals=6, args_size=10: new #2 // class demo/jvm0104/MovingAverage new, 创建 MovingAverage 类的对象;3: dup // 复制栈顶引用值。4: invokespecial #3 // Method demo/jvm0104/MovingAverage."<init>":()V invokespecial 执行对象初始化。7: astore_1 //使用 astore_1 指令将引用地址值(addr.)存储(store)到编号为1的局部变量中: astore_1 中的 1 指代 LocalVariableTable 中ma对应的槽位编号,8: iconst_1 // iconst_1 和 iconst_2 用来将常量值1和2加载到栈里面, 并分别由指令 istore_2 和 istore_3 将它们存储到在 LocalVariableTable 的槽位 2 和槽位 3 中。store 之类的指令调用实际上从栈顶删除了一个值。 这就是为什么再次使用相同值时,必须再加载(load)一次的原因。9: istore_210: iconst_211: istore_312: aload_113: iload_214: i2d15: invokevirtual #4 // Method demo/jvm0104/MovingAverage.submit:(D)V18: aload_119: iload_320: i2d21: invokevirtual #4 // Method demo/jvm0104/MovingAverage.submit:(D)V24: aload_1 //调用 getAvg() 方法后,返回的结果位于栈顶,然后使用 dstore 将 double 值保存到本地变量4号槽位,这里的d表示目标变量的类型为double。25: invokevirtual #5 // Method demo/jvm0104/MovingAverage.getAvg:()D28: dstore 430: returnLineNumberTable:line 5: 0line 6: 8line 7: 10line 8: 12line 9: 18line 10: 24line 11: 30LocalVariableTable:Start Length Slot Name Signature0 31 0 args [Ljava/lang/String;8 23 1 ma Ldemo/jvm0104/MovingAverage;10 21 2 num1 I12 19 3 num2 I30 1 4 avg D
-
给局部变量赋值时,需要使用相应的指令来进行
store
,如astore_1
。store
类的指令都会删除栈顶值。 相应的load
指令则会将值从局部变量表压入操作数栈,但并不会删除局部变量中的值。
-
-
流程控制指令
-
主要是分支和循环在用, 根据检查条件来控制程序的执行流程。
-
代码
-
public class ForLoopTest {private static int[] numbers = {1, 6, 8};public static void main(String[] args) {MovingAverage ma = new MovingAverage();for (int number : numbers) {ma.submit(number);}double avg = ma.getAvg();} }
-
编译反编译
-
javac -g demo/jvm0104/*.java javap -c -verbose demo/jvm0104/ForLoopTest
-
字节码
-
0: new #2 // class demo/jvm0104/MovingAverage 3: dup 4: invokespecial #3 // Method demo/jvm0104/MovingAverage."<init>":()V 7: astore_1 8: getstatic #4 // Field numbers:[I 11: astore_2 12: aload_2 13: arraylength 14: istore_3 15: iconst_0 16: istore 418: iload 4 //循环体 用于执行循环计数器与数组长度的比较20: iload_321: if_icmpge 43 //if, integer, compare, great equal, 如果一个数的值大于或等于另一个值,则程序执行流程跳转到pc=43的地方继续执行。24: aload_225: iload 427: iaload28: istore 530: aload_131: iload 533: i2d34: invokevirtual #5 // Method demo/jvm0104/MovingAverage.submit:(D)V 37: iinc 4, 1 // 4号槽位的值加140: goto 18 //跳到循环开始的地方43: aload_144: invokevirtual #6 // Method demo/jvm0104/MovingAverage.getAvg:()D47: dstore_248: returnLocalVariableTable:Start Length Slot Name Signature30 7 5 number I //5 号槽位被 number 占用了。0 49 0 args [Ljava/lang/String; //0槽位被 main 方法的参数 args 占据了8 41 1 ma Ldemo/jvm0104/MovingAverage; //1 号槽位被 ma 占用了。48 1 2 avg D //2 号槽位是for循环之后才被 avg 占用的。2号槽位的变量保存了 numbers 的引用值,占据了 2号槽位。 3号槽位的变量, 由 arraylength 指令使用, 得出循环的长度。 4号槽位的变量, 是循环计数器, 每次迭代后使用 iinc 指令来递增。
-
-
算术运算指令与类型转换指令
-
将
int
值作为参数传递给实际上接收double
的submit()
方法时, 在实际调用该方法之前,使用了类型转换的操作码 -
31: iload 533: i2d34: invokevirtual #5 // Method demo/jvm0104/MovingAverage.submit:(D)V
-
将一个 int 类型局部变量的值, 作为整数加载到栈中,然后用
i2d
指令将其转换为double
值,以便将其作为参数传给submit
方法。 -
唯一不需要将数值load到操作数栈的指令是
iinc
,它可以直接对LocalVariableTable
中的值进行运算。 其他的所有操作均使用栈来执行。
-
-
方法调用指令和参数传递
用于方法调用的指令
-
invokestatic
,用于调用某个类的静态方法,这也是方法调用指令中最快的一个。 -
invokespecial
, 用来调用构造函数,也可以用于调用同一个类中的 private 方法, 以及可见的超类方法。 -
invokevirtual
,如果是具体类型的目标对象,用于调用公共,受保护和打包私有方法。 -
invokeinterface
,调用的方法属于某个接口。运行时受到更多限制区别:
-
使用
invokestatic
指令,JVM 就确切地知道要调用的是哪个方法:因为调用的是静态方法,只能属于一个类。 -
使用
invokespecial
时, 查找的数量也很少, 解析也更加容易, 那么运行时就能更快地找到所需的方法。
-
-
JDK7 新增的方法调用指令 invokedynamic
- 是实现“动态类型语言”
- 在不改变字节码的时候,Java 语言层面想调用一个类 A 的方法 m,只有两个办法:
- 使用
A a=new A(); a.m()
,拿到一个 A 类型的实例,然后直接调用方法; - 通过反射,通过 A.class.getMethod 拿到一个 Method,然后再调用这个
Method.invoke
反射调用;
- 使用
- invokedynamic配合新增的方法句柄(Method Handles,可以用来描述一个跟类型 A 无关的方法 m 的签名,甚至不包括方法名称,这样就可以做到我们使用方法 m 的签名,但是直接执行的时候调用的是相同签名的另一个方法 b),可以在运行时再决定由哪个类来接收被调用的方法。在此之前,只能使用反射来实现类似的功能。该指令使得可以出现基于 JVM 的动态语言,让 jvm 更加强大。而且在 JVM 上实现动态调用机制,不会破坏原有的调用机制。这样既很好的支持了 Scala、Clojure 这些 JVM 上的动态语言,又可以支持代码里的动态 lambda 表达式。
相关文章:
JVM-Java字节码技术笔记
Java字节码技术 Java字节码是java代码编译后的中间代码格式,JVM需要读取并解析字节码才能执行相应的任务 获取字节码简介:由单字节(byte)的指令组成 操作码( 指令), 主要由类型前缀和操作名称两部分组成。根据指令的性质…...

C++ 友元、重载、继承、多态
友元 关键字:friend 友元的三种实现 全局函数做友元类做友元成员函数做友元 全局函数做友元 //建筑物类 class Building {//goodGay全局函数是Building好朋友,可以访问Building中私有成员friend void goodGay(Building& building); public:Build…...

Spring Boot 日志文件
前言 本篇博客主要介绍自定义的日志打印、日志的级别高低、如何保存日志等等..... 一、日志是什么?日志有什么用? 日志就是我们控制台上输出的内容,控制台上的输出的信息就是日志信息,如下所示: 日志有什么用&#x…...

vulhub venom
文章目录 靶场环境信息收集ftp服务二、信息利用三、任意文件上传三 sudo提权靶场环境 `vmware 靶场信息:https://www.vulnhub.com/entry/venom-1,701/ 下载地址:https://download.vulnhub.com/venom/venom.zip 新建虚拟机打开下载后的ovf文件 遇见导入失败合规性检查时,重试…...
量化交易之One Piece篇 - linux - 定时任务(重启服务器、执行程序、验证)
linux 执行命令: crontab -e 0 5 * * 1-5 sudo /sbin/shutdown -r now 0 17 * * 1-5 sudo /sbin/shutdown -r now 45 8 * * 1-5 cd /home/ubuntu/onepiece/bin/datacore && ./datacore 45 20 * * 1-5 cd /home/ubuntu/onepiece/bin/datacore && ./datacore 以…...

Qt5开发及实例V2.0-第二十三章-Qt-多功能文档查看器实例
Qt5开发及实例V2.0-第二十三章-Qt-多功能文档查看器实例 第23章 多功能文档查看器实例23.1. 简介23.2. 界面与程序框架设计23.2.1. 图片资源23.2.2. 网页资源23.2.3. 测试用文件 23.3 主程序代码框架23.4 浏览网页功能实现23.4.1 实现HtmIHandler处理器 23.5. 部分代码实现23.5…...

爬虫笔记_
爬虫简介 爬虫初始深入 爬虫在使用场景中的分类 通用爬虫: 抓取系统重要组成部分。抓取的是一整张页面数据 聚焦爬虫: 是建立在通用爬虫的基础上。抓取的是页面中特定的局部内容。 增量式爬虫 监测网站中数据更新的情况。只会抓取网站中最新更新出来的…...
Spring设计模式,事务管理和代理模式的应用
扩充:贝叶斯定理答案见底。 设计模式对关于面向对象问题的具体解决方案. 1,单例多例 在设计单例模式时,要注意两个点 1.构造方法要私有 2.成员变量要私有 3.创建对象所用的方法要被synchronized修饰.(因为方法体中会涉及到判断当…...

基于海康Ehome/ISUP接入到LiveNVR实现海康摄像头、录像机视频统一汇聚,做到物联网无插件直播回放和控制
LiveNVR支持海康NVR摄像头通EHOME接入ISUP接入LiveNVR分发视频流或是转GB28181 1、海康 ISUP 接入配置2、海康设备接入2.1、海康EHOME接入配置示例2.2、海康ISUP接入配置示例 3、通道配置3.1、直播流接入类型 海康ISUP3.2、海康 ISUP 设备ID3.3、启用保存3.4、接入成功 4、相关…...

Linux下git安装及使用
Linux下Git使用 1. git的安装 sudo apt install git安装完,使用git --version查看git版本 2. 配置git git config --global user.name "Your Name“ ##配置用户 git config --global user.email emailexample.com ##配置邮箱git config --global --list …...
python读取图片
要在Python中读取图片,你可以使用第三方库Pillow(Python Imaging Library,PIL)或OpenCV。以下是使用这两个库的示例: 使用Pillow库读取图片: 首先,确保你已经安装了Pillow库。如果还没有安装&am…...

虚幻4学习笔记(15)读档 和存档 的实现
虚幻4学习笔记 读档存档 B站UP谌嘉诚课程:https://www.bilibili.com/video/BV164411Y732 读档 添加UI蓝图 SaveGame_UMG 添加Scroll Box 修改Scrollbar Thickness滚动条厚度 15 15 勾选 is variable 添加text 读档界面 添加背景模糊 添加UI蓝图 SaveGame_Slot …...

Spring面试题22:Spring支持哪些ORM框架?优缺点分别是什么?Spring可以通过哪些方式访问Hibernate?
该文章专注于面试,面试只要回答关键点即可,不需要对框架有非常深入的回答,如果你想应付面试,是足够了,抓住关键点 面试官:Spring支持哪些ORM框架?优缺点分别是什么? Spring 支持多种 ORM(对象关系映射)框架,其中包括: Hibernate:Hibernate 是一个强大的 ORM 框架…...
流行的Python库numpy及Pandas简要介绍
numpy.ndarray 是NumPy库中的主要数据结构,它是一个多维数组,用于存储和操作数值数据。NumPy是Python中用于数值计算的强大库,numpy.ndarray 是它的核心数据类型,提供了高效的数值运算和广泛的数学函数。 以下是 numpy.ndarray 的…...

【二、安装centOS】
下载 地址:https://mirrors.aliyun.com/centos/ 地址 1、https://mirrors.aliyun.com/centos/7.9.2009/ 2、https://mirrors.aliyun.com/centos/7.9.2009/isos/ 3、https://mirrors.aliyun.com/centos/7.9.2009/isos/x86_64/ 选哪一个 可以选择第一个࿰…...

【动手学深度学习-Pytorch版】序列到序列的学习(包含NLP常用的Mask技巧)
序言 这一节是对于“编码器-解码器”模型的实际应用,编码器和解码器架构可以使用长度可变的序列作为输入,并将其转换为固定形状的隐状态(编码器实现)。本小节将使用“fra-eng”数据集(这也是《动手学习深度学习-Pytor…...

AUTOSAR 面试知识回顾
如果答不上来,就讲当时做了什么 1. Ethernet基础: 硬件接口: ECU到PHY: data 是MII总线, 寄存器控制是SMI总线【MDCMDIO两根线, half duplex】PHY输出(100BASE-T1): MDI总线,2 wire 【T1: twisted 1 pair …...

华为NFC设置教程(门禁卡/公交卡/校园卡等)
今天把华为NFC设置教程分享给大家 出门带门禁卡、校园卡、银行卡、身份证……东西又多,携带又麻烦,还容易搞丢,有没有一种方法可以把它们都装下?有!只要一部手机,出门不带卡包,各种证件&#x…...

基于微信小程序的音乐播放器设计与实现(源码+lw+部署文档+讲解等)
前言 💗博主介绍:✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌💗 👇🏻…...

如何取消显示Notepad++每行显示的CRLF符号
新电脑中重新安装了Nodepad,打开记事本后发现出现了许多黑底的CR|LF标记,特别碍眼。 如何取消呢? 视图 -> 显示符号 -> 取消勾选 显示行尾符操作步骤 预期效果...

shell脚本--常见案例
1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件: 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...
基于Uniapp开发HarmonyOS 5.0旅游应用技术实践
一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架,支持"一次开发,多端部署",可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务,为旅游应用带来…...

智能在线客服平台:数字化时代企业连接用户的 AI 中枢
随着互联网技术的飞速发展,消费者期望能够随时随地与企业进行交流。在线客服平台作为连接企业与客户的重要桥梁,不仅优化了客户体验,还提升了企业的服务效率和市场竞争力。本文将探讨在线客服平台的重要性、技术进展、实际应用,并…...
大模型多显卡多服务器并行计算方法与实践指南
一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...

【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...
【C语言练习】080. 使用C语言实现简单的数据库操作
080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...

【开发技术】.Net使用FFmpeg视频特定帧上绘制内容
目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法,当前调用一个医疗行业的AI识别算法后返回…...

NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合
在汽车智能化的汹涌浪潮中,车辆不再仅仅是传统的交通工具,而是逐步演变为高度智能的移动终端。这一转变的核心支撑,来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒(T-Box)方案:NXP S32K146 与…...
在树莓派上添加音频输入设备的几种方法
在树莓派上添加音频输入设备可以通过以下步骤完成,具体方法取决于设备类型(如USB麦克风、3.5mm接口麦克风或HDMI音频输入)。以下是详细指南: 1. 连接音频输入设备 USB麦克风/声卡:直接插入树莓派的USB接口。3.5mm麦克…...
【FTP】ftp文件传输会丢包吗?批量几百个文件传输,有一些文件没有传输完整,如何解决?
FTP(File Transfer Protocol)本身是一个基于 TCP 的协议,理论上不会丢包。但 FTP 文件传输过程中仍可能出现文件不完整、丢失或损坏的情况,主要原因包括: ✅ 一、FTP传输可能“丢包”或文件不完整的原因 原因描述网络…...