文章已收录至精进Java编程系列 此系列其它文章 https://www.wormholestack.com/tag/java/0. 序本文开始前，提出几个问题，各位读者可以试着答答看。计算机是如何执行程序代码的？Java是编译型语言还是解释型语言呢？Java代码的执行过程是怎么样的？如果你对此还有些模糊，不妨看完本文。作为许多知识底层原理的基础。本文将从计算机发展史说起，探究计算机执行程序的过程，并浅析Java程序执行原理，以开篇的几个常见问题为引，来达到巩固Java基础的目的。只有足够了解程序本质，才会在编写代码的时得心应手。1.计算机发展史想了解计算机是如何程序的，我们首先应该对计算机有足够的了解。让我们了解计算机的发展，走进历史。1.1 图灵机在 1936 年，艾伦·图灵在论文《论可计算数及其在可判定性问题上的应用》中开创性地提出了计算机的通用逻辑模型 —— 图灵机模型。这一理论深刻地影响了第一代计算机科学家，更多能够实现计算功能的计算机被制造出来，图灵也因此被誉为 “计算机科学之父”。图灵的基本思想是用机器来模拟人们用纸笔进行数学运算的过程。下图为图灵机的简单示意图图灵机的基本组成如下：假设有一条无穷的【纸带】，纸带由一个个连续的格子组成，每个格子可以写入字符，纸带就好比内存，而纸带上的格子的字符就好比内存中的数据或程序；有一个【读写头】，读写头可以读取纸带上任意格子的字符，也可以把字符写入到纸带的格子，同时可以编辑数据（写入或擦除），移动纸带向左或者向右；读写头上有一些部件，比如存储单元、控制单元以及运算单元：存储单元用于存放数据；控制单元用于识别字符是数据还是指令，以及控制程序的流程等；运算单元用于执行运算指令；知道了图灵机的组成后，我们以简单数学运算的 1 + 2 作为例子，来看看它是怎么执行这行代码的。首先，用读写头把 【1、2、+】这 3 个字符分别写入到纸带上的 3 个格子，然后读写头先停在 1 字符对应的格子上；接着，读写头读入 1 到存储设备中，这个存储设备称为图灵机的状态；然后读写头向右移动一个格，用同样的方式把 2 读入到图灵机的状态，于是现在图灵机的状态中存储着两个连续的数字， 1 和 2；读写头再往右移动一个格，就会碰到 + 号，读写头读到 + 号后，将 + 号传输给【控制单元】，控制单元发现是一个 + 号而不是数字，所以没有存入到状态中，因为 + 号是运算符指令，作用是加和目前的状态，于是通知【运算单元】工作。运算单元收到要加和状态中的值的通知后，就会把状态中的 1 和 2 读入并计算，再将计算的结果 3 存放到状态中；最后，运算单元将结果返回给控制单元，控制单元将结果传输给读写头，读写头向右移动，把结果 3 写入到纸带的格子中；通过上面的图灵机计算 1 + 2 的过程，可以发现图灵机主要功能就是读取纸带格子中的内容，然后交给控制单元识别字符是数字还是运算符指令，如果是数字则存入到图灵机状态中，如果是运算符，则通知运算符单元读取状态中的数值进行计算，计算结果最终返回给读写头，读写头把结果写入到纸带的格子中。事实上，图灵机这个看起来很简单的工作方式，但它可以模拟计算机的任何算法，无论这个算法有多复杂。1.2 计算机的发展史随着计算机的元器件从继电器升级到电子管（也叫真空管），计算机也从 “机电” 时代进入到 “电子” 时代。第一台电子计算机诞生了，它由各种门电路组成的，这些门电路通过组装出一个固定的电路板，来执行一个特定的程序，也就与其他早期的计算机一样，都是不可重新编程的也就是说一台计算机只能执行一个特定的程序，如果需要修改程序功能，就需要将整个计算器拆开，然后重新组装电路。1943 年，Colossus Mark I 计算机（巨人 1 号）在英国 Bletchley 公园（二战时的密码破译机构）被建造出来，以破解纳粹通信，好家伙一口气造了 10 台。 Colossus Mark I 被认为是第一台可编程的电子计算机，编程方法就是使用大量的开关和插线板（PlugBoards）。 但 Colossus Mark I 并不是通用计算机，它只被设计用于执行密码分析相关的计算。直到 1946 年，John Mauchly 和 J. Presper Eckert 在美国宾夕法尼亚大学建造了 ENIAC（Electronic Numerical Integrator and Computer，中译：埃尼亚克）， ENIAC 被认为是第一台可编程的通用电子计算机，也被认为是第一台现代意义上的计算机。 但是，ENIAC 和 Colossus Mark I 一样都使用插线板编程，虽然不需要拆掉整台计算机来重新编程，但是编程效率依然非常低，据说一个简单程序在 ENIAC 上编程最多要花费三个星期。这对于早期异常昂贵的计算机来说，需要停机这么长时间来重新编程是无法接受的。人们迫切需要一种更高效更灵活的编程方式，有人开始研究 使用存储器来保存程序和程序处理的数据。 在 1944 年，ENIAC 的发明者之一 J. Presper Eckert 发明了一种基于水银管的存储器，这为后来的存储程序概念提供了实现基础。在建造 ENIAC 的同时，Mauchly 和 Eckert 也在同步研究一种新设计 EDVAC，并向 John von Neumann（冯·诺依曼）提出咨询。冯·诺依曼也参与到 EDVAC 项目中，并且写了一份著名的内部文档 《First Draft of a Report on the EDVAC》 ，在 1945 年冯诺依曼和其他计算机科学家们提出了计算机具体实现的报告，其遵循了图灵机的设计，详细阐述了 “存储程序计算机（Stored-program Computer）” 的概念，也就是人们所说的冯·诺依曼架构。他们在 1947 年对 ENIAC 进行诸多改进，ENIAC 也成为了世界上第一台存储程序计算机。而 1948 年建造完成的 Manchester Baby 则被认为是世界上第一台基于冯·诺依曼架构的通用计算机。从 Baby 到现在 70 多年的时间，所有的单片机、PC 电脑、智能手机、服务器依然在遵循这一计算机架构。现代所有的计算机科学上的发展都是在软件和硬件能力上做优化，根本上的计算机架构依然没有改变。冯·诺依曼也因而被誉为 “电子计算机之父”。下面让我们具体了解下冯诺依曼模型。1.3 冯诺依曼模型冯.诺依曼结构中，将程序和数据一样看待，将程序编码为数据，然后与数据一同存放在存储器中，这样计算机就可以调用存储器中的程序来处理数据了。意味着，无论什么程序，最终都是会转换为数据的形式存储在存储器中，要执行相应的程序只需要从存储器中依次取出指令执行。冯.诺依曼结构的灵魂所在正是这里：减少了硬件的连接，这种设计思想导致了硬件和软件的分离，即硬件设计和程序设计可以分开执行。冯·诺依曼架构将通用计算机定义为以下 3 个基本原则：采用二进制：指令和数据均采用二进制格式；存储程序：一个计算机程序，不可能只有一条指令，而是由成千上万条指令组成的。指令和数据均存储在存储器中，而不是早期的插线板中，计算机按需从存储器中取指令和取数据；计算机由 5 个硬件组成：运算器、控制器、存储器、输入设备和输出设备。1.3.1 运算器首先计算机要有运算处理数据的能力，所以需要一个处理单元来完成各种算数运算和逻辑运算，这就是算术逻辑单元（Arithmetic Logic Unit，ALU）。ALU 的主要功能就是在控制信号的作用下，完成加、减、乘、除等算术运算以及与、或、非、异或等逻辑运算以及移位、补位等运算。运算器的主要部件就是 ALU ，运算器的处理对象是数据，所以数据的长度以及数据的表示方法，对运算器的影响很大。大多数通用计算机是以 16、32 、64 位数据作为运算器一次处理数据的长度。能够对一个数据的所有位同时处理运算器称为并行运算器，一次只能对数据的一个位处理的运算器称为串行运算器。运算器与其他部分的关系：计算机运算时，运算器的操作对象和操作种类由控制器决定。运算器操作的数据从内存中读取，处理的结果再写入内存（或者暂时存放在内部寄存器中），而且运算器对内存数据的读写是由控制器来进行的。1.3.2 控制器控制器又称为控制单元（Control Unit），是计算机的神经中枢和指挥中心，只有在控制器的控制下，整个计算机才能够有条不紊地工作、自动执行程序。控制器的工作流程为：从内存中取指令、翻译指令、分析指令，然后根据指令的内存向有关部件发送控制命令，控制相关部件执行指令所包含的操作。控制器和运算器共同组成中央处理器（Central Processing Unit）简称CPU，CPU 是一块超大规模集成电路，是计算机运算核心和控制核心，CPU 的主要功能是解释计算机指令以及处理数据。1.3.3 存储器存储器的主要功能是存储程序和各种数据，并且能够在计算机运行过程高速、自动地完成程序或者数据的存储，存储器是有记忆的设备，而且采用俩种稳定状态的物理器件来记录存储信息，所以计算机中的程序和数据都要转换为二进制代码才可以存储和操作。存储器可以分为内部存储器（内存）和外部存储器，俩者在计算机系统中各有用处，下面大概介绍一下俩种存储器的特点：1.内部存储器内部存储器称为内存或者主存，是用来存放欲执行的程序和数据。在计算机内部，程序和数据都是以二进制代码的形式存储的，它们均以字节为单位（8位）存储在存储器中，一个字节占用一个存储单元，并且每个存储单元都有唯一的地址号。这里以字节（8位）为存储单元，就与上面运算器的操作数据的大小联系起来了，16、32、64都是8的倍数CPU 可以直接使用指令对内部存储器按照地址进行读写俩种操作，读：将内存中某个存储单元的内容读出，送入 CPU 的某个寄存器中；写：在控制器的控制下，将 CPU 中某寄存器内容传到某个存储单元中。要注意，内存中的数据和地址码都是二进制数，但是俩者是不同的，一个地址可以指向一个存储单元，地址是存储单元的位置，数据是存储单元的内容，数据可以是操作码、可以是 CPU 要处理阿数据、也可以是数据的地址，地址码的长度由内存单元的个数确定。内存的存取速度会直接影响计算机的运算速度，由于 CPU 是高速器件，但是 CPU 的速度是受制于内存的存取速度的，所以为了解决 CPU 和内存速度不匹配的问题，在 CPU 和内存直接设置了一种高速缓冲存储器Cache。 Cache 是计算机中的一个高速小容量存储器，其中存放的是 CPU 近期要执行的指令和数据，其存取速度可以和CPU的速度匹配，一般采用静态 RAM 充当 Cache。内存按工作方式的不同又可以分为俩部分：RAM：随机存储器，可以被 CPU 随机读取，一般存放 CPU 将要执行的程序、数据，断电丢失数据，作为线性排列存储区域，存储的数据单位为一个二进制位（bit），最小的存储单位叫做字节（byte）每个字节对应一个内存地址，1byte=8bit。ROM：只读存储器，只能被 CPU 读，不能轻易被 CPU 写，用来存放永久性的程序和数据，比如：系统引导程序、监控程序等。具有掉电非易失性。2.外部存储器外部存储器主要来存放”暂时“用不着的程序和数据，可以和内存交换数据。一般是磁盘、光盘、U盘、硬盘等。1.3.4 输入输出设备实际上我们操作计算机都是与输入输出设备在打交道。鼠标键盘是输入设备、显示器是输出设备；手机触摸屏即时输入设备又是输出设备；服务器中网卡既是输入设备又是输出设备；所有的计算机程序都可以抽象为输入设备读取信息，通过 CPU 来执行存储在存储器中的程序，结果通过输出设备反馈给用户。经过发展现代计算机结构如下：中央处理器（CPU）、内存、输入设备、输出设备、总线。总线（控制总线，数据总线，内存总线）总线（Bus）是计算机各种功能部件之间传送信息的公共通信干线，它是由导线组成的传输线束， 按照计算机所传输的信息种类，计算机的总线可以划分为数据总线、地址总线和控制总线，分别用来传输数据、数据地址和控制信号。总线是一种内部结构，它是CPU、内存、输入、输出设备传递信息的公用通道，主机的各个部件通过总线相连接，外部设备通过相应的接口电路再与总线相连接，从而形成了计算机硬件系统。总结一下，冯.诺依曼结构消除了原始计算机体系中，只能依靠硬件控制程序的状况（程序作为控制器的一部分，作为硬件存在），将程序编码存储在存储器中，实现了可编程的计算机功能，实现了硬件设计和程序设计的分离，大大促进了计算机的发展。2 计算机是如何执行程序代码的在前面，我们知道了程序在图灵机的执行过程，接下来我们来看看程序在冯诺依曼模型上是怎么执行的。CPU 只能识别机器码，所以不管是哪位类型编程语言，不管经历了什么样的编译，解释过程，最终都是要交给CPU 来执行。那么 CPU 是如何执行程序的呢？程序实际上是一条一条指令，程序的运行过程就是 CPU 一步一步执行每一条指令。我们都知道，CPU的读写速度是纳秒级，CPU 读取内存数据耗时是 CPU 相关操作耗时的千倍，基本上到达了微秒级别。如果是去执行 IO 操作，即使是较快的 SSD，耗时也是内存操作耗时的千倍，基本上到达了毫秒级别；如果是在更慢的磁盘上执行 IO 操作，耗时也是 SSD 操作耗时的百倍，达到了毫秒级别；若是需要执行网络请求去获取数据，则耗时是 SSD 操作耗时的千倍，稍有不慎就会上到秒的级别综上所述获取数据的存储来源每上一个等级，时间量级也要跟着上一个等级CPU. -> 内存 -> SSD -> 磁盘 -> 网络 纳秒 -> 微秒 -> 毫秒 -> 毫秒 -> 秒CPU 和内存之间的速度瓶颈被称为冯诺依曼瓶颈，为解决这个问题，寄存器和（L1，L2，L3）三级缓存诞生了。寄存器的硬件设计高成本高功耗，且离CPU的距离非常近（通常寄存器集成在CPU上），寄存器读写速度与CPU有可匹敌，读写速度非常之快，所以数据的计算之前将数据从内存读入寄存器，计算之后再从寄存器写入内存，计算过程中的临时结果的存取由寄存器代替内存管理，会极大减少CPU与慢速内存的交互。不同CPU包含的寄存器种类不同，常见的寄存器如下：1）通用寄存器：AX，BX，CX，DX通用寄存器可用于传送和暂存数据，也可参与算术逻辑运算，并保存运算结果。AX（Accumulator）-累加器：常用于存放算术(乘除)、逻辑运算等操作。另外，所有的I/O指令都要使用累加器与外设接口传递数据。BX（Base Register）-基址寄存器：DS段中的数据指针，常用来存放访问内存时的地址。CX（Count Register）-计数寄存器：在循环、字符串操作指令中用作计数器，用于控制循环次数。DX（Data Register）-数据寄存器：在寄存器间接寻址中的I/O指令中存放I/O端口的地址。2）指针寄存器：BP，SP，SI，DI，IP主要用来保存当前正在执行的一条指令。BP（Base Pointer Register）和 SP（Stack Pointer Register）是用于存储栈空间地址的寄存器，SP存储栈顶地址，BP比较特殊，一般存储栈中一个栈帧的栈底地址。SI（Source Index Register）源地址寄存器和 DI（Destination Index Register）目的地址寄存器，分别用来存储读取和写入数据的内存地址。IP（Instruction Pointer Register）指令指针寄存器存储下一条将要执行的指令的内存地址，CS 和IP 两个寄存器中存储的内容通过计算，能得到一个真正的物理内存地址。3）段寄存器：CS，DS，SS段寄存器是因为对内存的分段管理而设置的。计算机需要对内存分段，以分配给不同的程序使用（类似于硬盘分页）程序由一组指令和一堆数据组成。指令存储在某块内存中（这块内存被称为代码段），由 CPU 逐一读取执行。数据也存储在某块内存中（这块内存被称为数据段）。指令执行的过程中，会操作（读取或写入）这块内存中的数据。CS（Code Segment Register）代码段地址寄存器存储了代码段的起始地址。CS 和 IP 两个寄存器中存储的内容通过计算，能得到一个真正的物理内存地址。PC寄存器（Program Counter Register）实际上是一个抽象概念，CS 寄存器和 IP 寄存器是具体寄存器的名称。通过这两个寄存器，可以得知对于存储下一条将要执行的指令的地址。DS（Data Segment Register）数据段地址寄存器存储了数据段的起始地址，同理，它跟 DI 或 SI 结合才能确定一个数据段中的内存地址。SS（Stack Segment Register）栈寄存器存储的是栈的起始地址，同理，它跟 SP 结合才能确定栈顶的内存地址，跟 BP 结合才能确定栈中某个中间位置的内存地址。4）指令寄存器：IRIR（Instruction Register）指令寄存器用来存放当前正在执行的指令。指令为一串二进制码，指令译码器需要从指令中解析出操作码和操作地址或操作数。所以，指令需要暂存在指令寄存器中等待译码处理。5）标志寄存器FR（Flag Register）标志寄存器，也叫做程序状态字寄存器（Program Status Word，PSW），在这个寄存器中的每一个二进制位记录一类状态，主要用于反映处理器的状态和ALU运算结果的某些特征及控制指令的执行。以上寄存器都是用来辅助完成CPU的各种指令。注意，以上是 16 位的寄存器，32 位的寄存器名称在对应的16 位寄存器名称前加E（例如EAX，EBP，ESP，EIP），64 位的寄存器名称在对应的 16 位寄存器名称前加R（例如RAX，RBP，RSP，RIP）CPU 执行程序的过程如下：首先，操作系统会把机器码，加载到内存中的代码段，将代码中变量等数据放入内存中的数据段，并且设置好各个寄存器的初始值，如 DS、CS 等。IP 寄存器中存储代码段中第一条指令的内存地址相对于CS的偏移地址。数据准备好后通过【数据总线】将指令数据传给 CPU。CPU 收到内存传来的数据后，根据 PC 寄存器（CS寄存器和IP寄存器的总称）存储的内存地址，从对应的内存单元中取出一条 CPU 指令，放到 IR 指令寄存器中，CPU 分析 IR 指令寄存器中的指令，确定指令的类型和参数，如果是计算类型的指令，就把指令交给【逻辑运算单元（运算器）】运算；如果是存储类型的指令，则交由【控制单元】执行；举例说明：假设指令是一条加法指令，控制器通过总线发送指令给运算器告知“你要准备运算了”，运算器接收指令，需要获取数据，于是通过总线发送命令给存储器告知“运算器要获取数据了”，存储器接收命令后，根据地址计算将运算所需的数据取出来，交给运算器做加法，然后将结果给存储器，最后控制器发送控制命令给存储器和输出设备告知指令执行结束，存储器将结果传递给输出设备打印。CPU 执行完指令后，将IP寄存器中的地址 + 4（也就是下一条指令在代码段中的偏移地址。内存中的每一个字节都对应一个地址。对于 32 位 CPU ，一条指令长度为4字节，下一条指令地址 = 当前指令地址 + 4。对于 64 位 CPU ，一条指令长度是 8 字节，下一条指令地址=当前指令地址 + 8），一条指令执行完成之后，再通过 PC 寄存器中的地址，取下一条指令继续执行。循环往复，直到所有的指令都执行完成。简单总结一下就是，一个程序执行的时候，CPU 会根据程序计数器里的内存地址，从内存里面把需要执行的指令读取到指令寄存器里面执行，然后根据指令长度自增，开始顺序读取下一条指令。CPU 从程序计数器读取指令、到执行、再到下一条指令，这个过程会不断循环，直到程序执行结束，这个不断循环的过程被称为 CPU 的指令周期。3. 编译型语言 or 解释型语言？上文中我们了解了计算机是如何执行程序的，我们知道 CPU 只能识别机器码。在早期计算机发展史，程序员直接使用机器码来编写程序，机器码为二进制，可读性不好且非常复杂，于是汇编语言诞生了，汇编语言由一组汇编指令组成，汇编指令与 CPU 指令一一对应，但汇编指令采用字符串而非二进制表示指令，可读性比机器码好些，但汇编语言编写的程序，需要经过“翻译”成机器码才会被 CPU 执行。而计算机语言发展至如今，像 Java，C++，Python 这种高级语言所编写的代码，仍需要“翻译”成机器码，才能被 CPU 所识别执行，具体的编程语言翻译的类型不同。总的来说分为以下 3 类编译型语言解释型语言混合型语言下面让我们来详细了解它们。3.1 编译型语言以 C++ 为代表，代码被编译成机器码（可执行文件）后，由 CPU 逐条执行，执行效率较高，但由于 CPU 类型不同导致所支持的 CPU 指令集不同，操作系统不同导致程序所引用系统 API 不同，这样造成编译之后的可执行文件只能在特定的操作系统或机器上执行，否则可能无法执行。3.2 解释型语言以 Python 为代表，代码是在执行的过程中，由 Python 解释器逐条取出程序中的代码，逐条解释成机器码后交给 CPU 执行，CPU 执行完当前机器码后，再取出下一条代码，重复上述流程。以上流程称为解释执行，边解释边执行。相较于编译型语言，解释型语言边解释边执行的机制，执行速度略慢，但可移植性好，由 Python 解释器根据不同的操作系统或机器，解释成相对应的 CPU 指令码，从而达到一次编写，到处运行的效果。3.3 混合型语言以 Java 为代表，Java 编译器先将程序代码(.java)编译成字节码（.class)，再有 Java 虚拟机(JVM)解释执行，逐行取出字节码，翻译成机器码，在交由 CPU 执行。字节码作为中间状态和平台无关，Java 虚拟机会将反复多次执行的字节码编译成机器码后缓存下来作为热点数据，以避免热点字节码反复编译，这个过程被称为 JIT 编译（JustInTime Compile 即时编译），这样可节省解释执行时间。加上从字节码翻译成机器码的过程，会比从高级语言翻译成机器码的耗时少，所以这样的机制造就了 Java 程序的可移植性高，且执行效率不慢。总的来说混合型语言既包含了编译执行的过程也包含了解释执行的过程，集两家所长。4. Java代码的执行过程上面我们了解了 Java 作为混合型语言的大概执行流程，也明白了 Java 语言一次编写，到处运行，跨平台的特性，功劳离不开 Java 虚拟机（JVM）的存在，接下来我们来细说 Java 编译执行过程。其中流程包括前端编译，类加载，解释执行，JIT 编译执行，AOT 编译。4.1 前端编译前端编译是只通过 javac 编译器将 .java 文件编译成 .class 文件的过程，前端编译除了经过词法分析，语法分析，语义分析外还做了注解处理和解语法糖处理。注解处理从 JDK6 开始，javac 编译器开始支持 JSR269（Pluggable Annotation Processing API）规范。我们只需要按照这个规范来开发注解插件（插件包含定义注解、使用注解、以及对应的注解处理器三部分内容），那么，javac 编译器在执行前端编译时，就会调用注解插件执行相应的注解处理器代码。我们在开发中经常用到的 Lombook 插件，便是按照 JSR269 规范开发的注解插件。在编译代码时，javac 编译器会调用 Lombook 插件的注解处理器，注解处理器根据 @getter、@setter 等注解，为类、成员变量生成 getter、setter 等方法。Lombook 插件中定义的注解大部分都是 SOURCE 级别的，也就是JVM也不会感知到语法糖。语法糖仅存在于源码中。当代码编译成字节码之后，这些注解便没有存在的意义了。毕竟 JVM 并不关心 getter、setter 方法来自于程序员手敲，还是 Lombook 注解。语法糖处理Java 为了提高开发效率，提供了很多语法糖（对已经存在的基本语法的二次封装）目的是提高易用性，在执行前端编译时，编译器会将语法糖解封装为基本语法。字节码并不包含语法糖，JVM 也不会感知到语法糖。语法糖仅存在于源码中。常见的语法糖有泛型包装类自动装箱与拆箱增强for循环方法变长参数枚举内部类条件编译断言Lambda表达式字符串+号语法switch-case对String和枚举类的支持等等以泛型为例说明泛型只存在于源码中，编译时会进行类型擦除，也就是说不管是List 还是 List在字节码中都是 List，所以说泛型中仅仅是编译器做类型检查所用，所以Java的泛型被称为伪泛型。以包装类自动装箱拆箱为例说明Integer count = 1; // 装箱 int temp = count; // 拆箱通过jad反编译后发现底层实现如下：Integer count = Integer.valueof(1); int i = count.intValue();由此发现自动装箱底层相当于执行了 Integer 的 valueof 方法。自动拆箱底层相当于执行了 Integer 的 intValue 方法。以增强for循环为例说明 for(int index : list){ System.out.println("" + index); }通过jad反编译后发现底层实现如下：Iterator<Integer> iterator = list.iterator(); while(iterator.hasNext()){ System.out.println("" + iterator.next()); }底层实现实际依赖迭代器。变长方法参数实际底层实现是编译器在编译源码的时候将变长参数部分转换成了 Java 数组枚举类型也只是编译器编译成普通的 class 类，继承java.lang.Enum，并被 final 关键字修饰，在Java 的字节码结构中，并没有枚举类型。内部类编译完成，编译器就会为内部类生成一个单独的 class 文件，名为outer$innter.class。条件编译是通过编译器的优化原则实现的：如果 if 的条件是 false，则在编译时忽略这个 if 语句。 忽略未使用的变量。断言的底层实现就是if语言，如果断言结果为 true，则什么都不做，程序继续执行，如果断言结果 false，则程序抛出 AssertError 来打断程序的执行。字符串+号语法如果在编译期能确定字符相加的结果，则会进行编译期优化。例如：String s = "a" + "b"; 优化为 String s = "ab";运行时，两个字符串 str1， str2 的拼接首先会 new 一个 StringBuilder 对象，然后分别对字符串进行append 操作，最后调用toString()方法。4.2 类加载Java 程序执行过程中，类的字节码按需被加载到内存中，也就是说当第一次创建某个类的对象或调用某个类的方法时，这个类才会被加载到内存中，加载后一直保存在内存中。 类加载遵循双亲委派机制，不同的类由不同的类加载器加载，类加载过程被分为验证，准备，解析，初始化。4.2.1 字节码Java 字节码是什么样子，我们借助一段简单的代码来看一看。/** * @description： HelloClass * @Author MRyan * @Version 1.0 */ public class HelloClass { private static String str = "Hello Class"; public static String get() { return str; } }经过 javac 编译成 class 后，通过 javap 工具对其反编译结果如下：Classfile /Users/ryan/ProgramData/码农翻身/Mine/mlearn/src/main/java/cn/wormholestack/mlearn/jvm/HelloClass.class Last modified 2022-11-27; size 410 bytes MD5 checksum c2510b3273939199f415f75260d0630a Compiled from "Helloclass.java" public class cn.wormholestack.mlearn.jvm.HelloClass minor version: 0 major version: 52 flags: ACC_PUBLIC， ACC_SUPER Constant pool: #1 = Methodref #5.#17 // java/lang/Object."<init>":()V #2 = Fieldref #4.#18 // cn/wormholestack/mlearn/jvm/HelloClass.str:Ljava/lang/String; #3 = String #19 // Hello Class #4 = Class #20 // cn/wormholestack/mlearn/jvm/HelloClass #5 = Class #21 // java/lang/Object #6 = Utf8 str #7 = Utf8 Ljava/lang/String; #8 = Utf8 <init> #9 = Utf8 ()V #10 = Utf8 Code #11 = Utf8 LineNumberTable #12 = Utf8 get #13 = Utf8 ()Ljava/lang/String; #14 = Utf8 <clinit> #15 = Utf8 SourceFile #16 = Utf8 Helloclass.java #17 = NameAndType #8:#9 // "<init>":()V #18 = NameAndType #6:#7 // str:Ljava/lang/String; #19 = Utf8 Hello Class #20 = Utf8 cn/wormholestack/mlearn/jvm/HelloClass #21 = Utf8 java/lang/Object { private static java.lang.String str; descriptor: Ljava/lang/String; flags: ACC_PRIVATE， ACC_STATIC public cn.wormholestack.mlearn.jvm.HelloClass(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1， locals=1， args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 9: 0 public static java.lang.String get(); descriptor: ()Ljava/lang/String; flags: ACC_PUBLIC， ACC_STATIC Code: stack=1， locals=0， args_size=0 0: getstatic #2 // Field str:Ljava/lang/String; 3: areturn LineNumberTable: line 13: 0 static {}; descriptor: ()V flags: ACC_STATIC通过 jclasslib 可以直观的看到当前字节码文件的类信息，常量池，方法区等信息通过 xxd HelloClass.class 命令查看一下这个字节码文件。JVM 规范要求每一个字节码文件都要由十部分按照固定的顺序组成。结合上面3张图，字节码文件包含的结构属性如下：魔数（Magic）：第一行中有一串特殊的字符 cafebabe，它就是一个魔数，是 JVM 识别 class 文件的标志，JVM 会在验证阶段检查 class 文件是否以该魔数开头，如果不是则会抛出 ClassFormatError你可能很好奇，为什么用cafebabe当做魔数，事实上它并没有什么特别的意义。James Gosling（Java编程语言之父）的解释如下：“我和小伙伴们经常去一个叫 St Michael’s Alley 的地方吃午餐。根据当地传说, 在深暗的过去，Grateful Dead在出名前曾在此地表演. 这绝对是一个因 Grateful Dead Kinda Place 而闻名的地方。Jerry去世时, 他们进行了祭奠.我们经常去那里, 称这个地方为 Cafe Dead。可以看到,这是一个十六进制数. 那时候我正好需要维护一些文件的编码格式,需要用到两个魔数，一个用于对象持久化文件, 另一个用于类文件. 于是我就用 cafe dead 作为对象持久化文件的魔数, 并选中了cafe babe作为 class 文件标识。”版本号（Version）：紧跟着魔数后面的四个字节 0000 0034 分别表示副版本号和主版本号。也就是说，主版本号为 52（0x37 的十进制），也就是 Java 8 对应的版本号，副版本号为 0。常量池（constant_pool）：紧跟在版本号之后的是常量池入口，字符串常量和较大的整数都会存储在常量池中，当使用这些数值时，会根据常量池中的索引来查找。常量池中存储两类常量：字面量与符号引用。字面量：比较接近于Java语言层面的常量概念，如文本字符串、声明为final的常量值等符号引用：如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。常量池整体上分为两部分：常量池计数器以及常量池数据区，常量池计数器（constant_pool_count）：由于常量的数量不固定，所以需要先放置两个字节来表示常量池容量计数值。图 2 中示例代码的字节码前 10 个字节，将十六进制的 16 转化为十进制值为 22，排除掉下标 “0”，也就是说，这个类文件中共有 21 个常量。常量池数据区：数据区是由（constant_pool_count-1）个 cp_info 结构组成，一个 cp_info 结构对应一个常量。在字节码中共有 14 种类型的 cp_info，每种类型的结构都是固定的。具体以 CONSTANT_utf8_info 为例，它的结构如下左侧所示。首先一个字节 “tag”，它的值取自上图6中对应项的 Tag，由于它的类型是 utf8_info ，所以值为 “01”。接下来两个字节标识该字符串的长度 Length，然后 Length 个字节为这个字符串具体的值。从反编译结果的字节码中摘取一个 cp_info 结构，如下图右侧所示。将它翻译过来后，其含义为：该常量类型为 utf8 字符串，长度为一字节，数据为 “a”。其他类型的 cp_info 结构在本文不再赘述，整体结构大同小异，都是先通过 Tag 来标识类型，然后后续 n个字节来描述长度和（或）数据。注意此常量池被称为class文件常量池，不要混淆，它可以理解为一个元信息资源仓库（字面量，符号引用），由于java的特殊机制，class文件没法保存各个方法或字段最终的内存布局信息，只能通过JVM加载字节码时动态链接，在类加载解析阶段从常量池获取对应的符号引用翻译成可直接访问内存储地址的直接引用，同时会将class文件常量池中的内容导入方法区中的运行时常量池，在这个过程中通过cp_info来判定数据结构类型。访问标识（access_flag):常量池结束之后的两个字节，描述该 Class 是类还是接口，以及是否被 Public、Abstract、Final等修饰符修饰。JVM 规范规定了如下图的访问标志（Access_Flag）。需要注意的是，JVM并没有穷举所有的访问标志，而是使用按位或操作来进行描述的，比如某个类的修饰符为 Public Final，则对应的访问修饰符的值为 ACC_PUBLIC | ACC_FINAL，即 0x0001 | 0x0010=0x0011。当前类索引（this_class）：访问标志后的两个字节，描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值，根据索引值就能在常量池中找到这个类的全限定名。父类索引（super_class）：当前类索引后的两个字节，描述父类的全限定名，同上，保存的也是常量池中的索引值。接口索引（interfaces）：父类索引后为两字节的接口计数器，描述了该类或父类实现的接口数量。紧接着的n个字节是所有接口名称的字符串常量的索引值。字段表（fields）：字段表用于描述类和接口中声明的变量，包含类级别的变量以及实例变量，但是不包含方法内部声明的局部变量。字段表也分为两部分，第一部分为两个字节，描述字段个数；第二部分是每个字段的详细信息fields_info。字段表结构如下图方法表（methods)：直接看反编译后的的内容，有兴趣的读者可以去研究下字节码 16 进制文件对应的方法表。我们可以看到构造函数及程序中定义的 get 方法其中 Code 内主要属性为：stack: 最大操作数栈深度，JVM 运行时会根据这个值来分配栈帧(Frame)中的操作栈深度，此处为 1locals: 局部变量所需的存储空间，单位为 Slot， Slot 是虚拟机为局部变量分配内存时所使用的最小单位，为 4 个字节大小。方法参数(包括实例方法中的隐藏参数 this)，显示异常处理器的参数( try catch 中的 catch 块所定义的异常)，方法体中定义的局部变量都需要使用局部变量表来存放。值得一提的是，locals 的大小并不一定等于所有局部变量所占的 Slot 之和，因为局部变量中的 Slot 是可以重用的。args_size: 方法参数的个数，这里是 0，每个实例方法都会有一个隐藏参数 this,但仅针对非Static方法而言。attribute_info: 方法体内容，0，1，4 为字节码"行号"，该段代码的意思是将第一个引用类型本地变量推送至栈顶，然后执行该类型的实例方法，也就是常量池存放的第一个变量，也就是注释里的java/lang/Object."":()V， 然后执行返回语句，结束方法。LineNumberTable:行号表，将 Code 区的操作码和源代码中的行号对应，Debug 时会起到作用（源代码走一行，需要走多少个 JVM 指令操作码）。LocalVariableTable: 本地变量表，包含This和局部变量，之所以可以在每一个方法内部都可以调用 This，是因为 JVM 将 This 作为每一个方法的第一个参数隐式进行传入。当然，这是针对非 Static方法而言。附加属性表（attributes）字节码的最后一部分，该项存放了在该文件中类或接口所定义属性的基本信息。4.2.2 验证这一阶段主要是验证所生成的类字节码格式是否符合 JVM 规范，避免由于类字节码来源不可控导致安全问题或恶意篡改所做的合法性校验。4.2.3 准备在准备阶段，虚拟机为类的静态变量分配内存，并将其初始化为默认值。对于 static final 修饰的静态常量，虚拟机直接将其初始化为指定值。例如：private static final int count = 1，JVM在准备阶段将变量count初始化为1。对于只有static修饰的静态变量，JVM将其初始化为所属类型的默认值而非指定值，例如：private static int count = 1，JVM会在准备阶段将变量 count 初始化为int 类型的默认值 0 ，指定值 1 会在初始化阶段赋值给变量 count 。静态变量归属于类，非静态变量归属于对象，因此，在类的加载过程中，虚拟机只处理类的静态变量，类的非静态变量在对象的创建过程中处理，并且存储在对象所占用的内存空间中。4.2.4 解析类字节码常量池中的符号引用转换为直接引用，常量池中存储了所涉及的类，方法，变量等描述符，通过此步将描述符转换为可以直接访问内存存储地址的直接引用。4.2.5 初始化虚拟机会执行静态变量的初始化，初始化语句，静态代码块4.3 解释执行对于 Java 语言来说，经过前段编译后的 .class 字节码文件，加载到内存无法被 CPU 直接执行，依赖于 JVM 虚拟机将字节码逐条取出，一遍将其解释为机器码，一遍交给 CPU 执行。举例说明：如下代码 JVM 执行 Main 方法，执行 new HelloClass()创建HelloClass类对象时，JVM 发现内存中没有此类的字节码信息，于是通过类加载器在 classpath 对应的路径下寻找 HelloClass 文件，将其加载至内存中，虚拟机会根据类的字节码在堆中创建对象，在执行 helloClass.get()方法时，JVM 会根据 HelloClass 对象类指针找到内存中的 HelloClass 类，在类的方法表中查找 get 方法对应的字节码，然后逐句解释执行。/** * @description： HelloClass * @Author MRyan * @Version 1.0 */ public class HelloClass { private String str = "Hello Class"; public String get() { return str; } } class Main { public static void main(String[] args) { HelloClass helloClass = new HelloClass(); System.out.println(helloClass.get()); } }这里简单提一嘴类加载器的双亲委派机制类加载由类加载器完成，JVM定义了几种不同类型的类加载器启动类加载器（BootStrap ClassLoader） 负责加载jre/lib/rt.jar包中类扩展类加载器（Extension ClassLoader） 负责加载jre/lib/ext目录下jar包中类应用程序类加载器（Application ClassLoader） 负责加载其他classpath指定路径下（除启动，扩展类加载器负责的路径）的类JVM无法通过类全限定名来明确由哪个类加载器加载哪个类，这样就会导致重复加载类的情况，所以为了去重，JVM设计了双亲委派机制，虚拟机定义了类加载器的父子关系，并提出如下机制：当某个类加载器接受到加载类的请求时，如果当前类加载器没有加载过这个类就委托给父类加载器加载此类，如果父类加载器也没有加载过此类，则继续委托给父类的父类加载器加载，知道某个类加载器已经加载过此类结束。如果都没有找到已经加载过此类的类加载器，虚拟机会从上往下请求各个类加载器，如果此类事自己负责的路径下的类则就由它来加载。4.4 JIT编译上文有提过，边解释边执行的机制造成程序执行效率并没有编译型语言高，为了解决这个问题，Java 引入了JIT（Just in Time）。也就是说为 JVM 将热点代码的机器码缓存了起来，让下去执行热点代码的时候可复用，无需解释。HotSpot 虚拟机支持两种JIT编译器：Client（C1）编译器 Server（C2）编译器，Client 编译器只进行局部的编译优化所以它编译时间短，但是优化程度低，Server编译器进行局部和全局编译优化，所以编译时间长，但优化程度高。常说的 JVM 的两种运行模式其实就是 Client 模式和 Server 模式。Java7之前，虚拟机对于两者编译器只能二选一，无法同时使用，但Java7引入了分层编译，可以让虚拟机根据不同代码在不同时刻选择不同的编译类型，Java8种默认开启分层编译技术，JIT的实现是在运行期收集代码运行情况，做热点探测。那么什么样的代码才算热点代码呢？被多次执行的方法被多次执行的循环HotSpot虚拟机针对每个方法维护两个计数器，方法调用计数器和回边计数器，分别用来统计方法执行次数和循环执行次数，计数器通过比对动态阈值（动态计算）判定是否为热点代码。同时为解决随运行时间增长，一直执行的方法总会出现调用次数总高于阈值的时刻，JVM引入了热度衰减机制，超过一定时间限制之后，某方法没有达到阈值，那么使方法的计数器值减半，注意只有方法计数器存在此机制，且可以通过-XX:CounterHalfLifeTime设置超过的时间，通过-XX:UseCounterDecay设置关闭热度衰减机制。4.5 AOT编译AOT（Ahead Of Time）即提前编译，C/C++编译型语言便是AOT编译，可以在运行前生成被直接执行的二进制机器码，运行速度快、执行性能表现好，但每次执行前都需要提前编译，开发测试效率低。Java也支持AOT编译，区别于JIT编译，AOT编译优化属于静态编译优化，而JIT编译优化属于动态编译优化，通常在Java中使用比较少。5. 小结到此本文就要收尾了，本文逐步递进，由计算机历史为引，到计算机组成原理，再到Java程序原理，涉及内容较多，但很好理解。现在回到文章开篇的3个问题，我知道你已经可以完美解答。参考https://gist.github.com/hellerbarde/2843375https://cizixs.com/2017/01/03/how-slow-is-disk-and-network/https://cloud.tencent.com/developer/article/1729507https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.htmlhttps://www.zhihu.com/question/23874627https://en.wikipedia.org/wiki/Colossus_computerhttps://en.wikipedia.org/wiki/Delay-line_memoryhttps://en.wikipedia.org/wiki/EDVAChttps://en.wikipedia.org/wiki/Stored-program_computerhttps://en.wikipedia.org/wiki/Java_class_file?spm=a2c6h.12873639.article-detail.6.133d4200KDZW5M《Java编程之美》《深入理解Java虚拟机》
什么是JDK SPI 机制？SPI (Service Provider Interface) 是一种将服务接口与服务实现分离以达到解耦、大大提升了程序可扩展性的机制。引入服务提供者就是引入了spi接口的实现者，通过本地的注册发现获取到具体的实现类，轻松可插拔。当服务的提供者提供了一种接口的实现之后，需要在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件，此文件记录了该 jar 包提供的服务接口的具体实现类。当某个应用引入了该 jar 包且需要使用该服务时，JDK SPI 机制就可以通过查找这个 jar 包的 META-INF/services/ 中的配置文件来获得具体的实现类名，进行实现类的加载和实例化，最终使用该实现类完成业务功能。为什么要引入JDK SPI？上述已经描述清楚，在Java中SPI是被用来设计给服务提供商做插件用的，基于策略模式，来实现动态加载机制。例如我们在程序中只定义一个接口（规范），而具体的实现交给不同的服务提供者，在程序的启动中，读取设置好的配置文件，由配置文件决定加载哪一种服务的实现。这种将服务接口与实现分离达到解耦的作用，同时也提升了程序的可扩展性。举例说明：使用Java语言访问数据库的时候我们会使用到Java.sql.Driver接口不同数据库产品底层的协议不同，提供的Driver实现也不同，而开发者也不确定用户会选择哪种数据库，所以这种情况下可以使用JDK SPI机制，根据配置文件 用户选择哪种数据库，就调用哪种数据库的实现。 简单实现写一个简单的例子有一个接口Log，定义一个方法log，作用：作为日志输出传入参数。而实现接口的方式有两种，分别是Log4j和Logback。具体要使用哪种方式看配置文件。就这样，一个小例子下面是代码Log接口public interface Log { void log(String info); }两个实现类public class Logback implements Log { @Override public void log(String info) { System.out.println("Logback:" + info); } } public class Log4j implements Log { @Override public void log(String info) { System.out.println("Log4jLog4j:" + info); } } service配置文件service.impl.Log4j service.impl.Logback测试类public class Main { public static void main(String[] args) throws IOException { ServiceLoader<Log> spiLoader = ServiceLoader.load(Log.class); Iterator<Log> iteratorSpi = spiLoader.iterator(); while (iteratorSpi.hasNext()) { Log log = iteratorSpi.next(); log.log("MRyan"); } } }输出：源码分析由 ServiceLoader的load方法作为入口进行分析 传入的参数是接口类 public static ServiceLoader load(Class service) { //获取当前线程绑定的ClassLoader ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }接着调用重载load（）传入接口类和，当前线程绑定的ClassLoader public static ServiceLoader load(Class service, ClassLoader loader){ //为给定的服务类型和类加载器创建一个新的服务加载器。 return new ServiceLoader<>(service, loader); } // 表示正在加载的服务的类或接口 private final Class service; // 用于查找，加载和实例化提供程序的类加载器 private final ClassLoader loader; // 创建ServiceLoader时采取的访问控制上下文 private final AccessControlContext acc; private ServiceLoader(Class svc, ClassLoader cl) { //判断当前接口类引用返回是否为空 service = Objects.requireNonNull(svc, "Service interface cannot be null"); //判断当前线程绑定的ClassLoader是否为空 为空则调用SystemClassLoader loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; //判断AccessControlContext是否为空 acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; //清除此加载程序的提供程序缓存，重新加载所有提供程序 reload(); }核心方法reload,来看看它都做了什么 // provider的缓存 //该缓存用来记录 ServiceLoader 创建的实现对象，其中 Key 为实现类的完整类名，Value 为实现类的对象。 private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); //懒加载迭代器 private LazyIterator lookupIterator; //清除此加载程序的提供程序缓存，重新加载所有提供程序 public void reload() { //清空provider的缓存 providers.clear(); //创建 LazyIterator 迭代器，用于读取 SPI 配置文件并实例化实现类对象。 lookupIterator = new LazyIterator(service, loader); } 有点好奇LazyIterator是什么，看名字像是迭代器，有点眼熟，我们发现测试类中迭代器就是调用 ServiceLoader.LazyIterator 实现的。Iterator 接口有两个关键方法：hasNext() 方法和 next() 方法。这里的 LazyIterator 中的next() 方法最终调用的是其 nextService() 方法，hasNext() 方法最终调用的是 hasNextService() 方法 private static final String PREFIX = "META-INF/services/"; Enumeration<URL> configs = null; Iterator<String> pending = null; String nextName = null; private class LazyIterator implements Iterator { Class service; ClassLoader loader; Enumeration<URL> configs = null; Iterator<String> pending = null; String nextName = null; private LazyIterator(Class service, ClassLoader loader) { this.service = service; this.loader = loader; } private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { String fullName = PREFIX + service.getName(); if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; } private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen } public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction action = new PrivilegedAction() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } public void remove() { throw new UnsupportedOperationException(); } }其中着重看下hasNextService方法 private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { //很熟悉有没有 PREFIX 赋值是 "META-INF/services/" String fullName = PREFIX + service.getName(); // 加载配置文件 if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } // 按行SPI遍历配置文件的内容 while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } // 解析配置文件 pending = parse(service, configs.nextElement()); } //更新字段 nextName = pending.next(); return true; }在 hasNextService() 方法中完成 SPI 配置文件的解析之后，再来看nextService() 方法，该方法负责实例化 hasNextService() 方法读取到的实现类，其中会将实例化的对象放到 providers 集合中缓存起来，核心实现如下所示：private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { // // 加载 nextName字段指定的类 c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } // // 检测类型 if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { //// 创建实现类的对象 S p = service.cast(c.newInstance()); //关键代码将实例化的对象放入缓存 将实现类名称以及相应实例对象添加到缓存 providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen }说了这么多迭代器的实现，那么测试类中的迭代是怎么实现的？public Iterator iterator() { return new Iterator() { // knownProviders用来迭代providers缓存 Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator(); // 先走查询缓存，缓存查询失败，再通过LazyIterator加载 public boolean hasNext() { if (knownProviders.hasNext()) return true; return lookupIterator.hasNext(); } //// 先走查询缓存，缓存查询失败，再通过 LazyIterator加载 public S next() { if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next(); } public void remove() { throw new UnsupportedOperationException(); } }; }到这里，文章就接近尾声了，了解了SPI是什么，SPI能做什么，SPI的实现，那么问题来了，他的缺点是什么呢？其实很明显真正实现是需要读取配置类的信息，那我们如果需要新增一个驱动类的话，是不是需要在配置类中手动的添加一行，那删除一个驱动类的时候也需要手动删除，那这样的话扩展性是不是就不好了呢。
由一个小例子引出今天的课题：校验用户信息，后端对接收的对象的信息例如用户名和密码做一个指定的校验。代码如下：User类指定用户名和密码设定要求@Data @NoArgsConstructor @AllArgsConstructor public class User { /** * 用户名 不能为空 长度不能大于6 */ private String username; /** * 密码 不能为空 长度不能低于5 大于13 */ private String password; } 然后我们编写校验逻辑 /** * 模拟接收（username）用户名 （passwoord）密码 验证用户信息时候符合要求 */ public static boolean saveUser(User user) { //User为空返回false if (user == null) { System.out.println("失败：不可为空"); return false; } String username = user.getUsername(); String password = user.getPassword(); if (username == null || "".equals(username)) { System.out.println("｛用户名可空校验｝失败：不可为空"); return false; } else { System.out.println("｛用户名可空校验｝通过！！！"); if (username.length() > 6) { System.out.println("｛用户名长度校验｝失败：长度不能超过6"); return false; } else { System.out.println("｛用户名长度校验｝通过！！！"); } } if (password == null || "".equals(password)) { System.out.println("｛密码可空校验｝失败：不可为空"); return false; } else { System.out.println("｛密码可空校验｝通过"); if (password.length() < 5 || password.length() > 13) { System.out.println("｛密码长度校验｝失败：长度不能低于5，不能超过13"); return false; } else { System.out.println("｛用户名长度校验｝通过！！！"); } } return true; }测试类调用 public static void main(String[] args) { System.out.println(saveUser(new User("MRyan", "123456"))); System.out.println(saveUser(new User("", "123456"))); System.out.println(saveUser(new User("MRyan", "1234566666666666"))); System.out.println(saveUser(new User("MRyan", "12"))); }输出log查看结果｛用户名可空校验｝通过！！！ ｛用户名长度校验｝通过！！！ ｛密码可空校验｝通过 ｛用户名长度校验｝通过！！！ true ｛用户名可空校验｝失败：不可为空 false ｛用户名可空校验｝通过！！！ ｛用户名长度校验｝通过！！！ ｛密码可空校验｝通过 ｛密码长度校验｝失败：长度不能低于5，不能超过13 false ｛用户名可空校验｝通过！！！ ｛用户名长度校验｝通过！！！ ｛密码可空校验｝通过 ｛密码长度校验｝失败：长度不能低于5，不能超过13 false Process finished with exit code 0 实现了这个需求，但是我们会发现，校验融入了业务逻辑中，如果想要修改校验需求那就需要改动业务代码，这样做显然不是我们想要的。我们可以用Java注解的方式来完成校验。什么是Java注解1.注解的定义：Java文件叫做Annotation，用@interface表示。2.元注解：@interface上面按需要注解上一些东西，包括@Retention、@Target、@Document、@Inherited四种。3.注解的保留策略：@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Retention { /** * Returns the retention policy. * @return the retention policy */ RetentionPolicy value(); } @Retention(RetentionPolicy.SOURCE) // 注解仅存在于源码中，在class字节码文件中不包含@Retention(RetentionPolicy.CLASS) // 默认的保留策略，注解会在class字节码文件中存在，但运行时无法获得@Retention(RetentionPolicy.RUNTIME) // 注解会在class字节码文件中存在，在运行时可以通过反射获取到4.注解的作用目标：@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { /** * Returns an array of the kinds of elements an annotation type * can be applied to. * @return an array of the kinds of elements an annotation type * can be applied to */ ElementType[] value(); }public enum ElementType { /** Class, interface (including annotation type), or enum declaration */ TYPE, /** Field declaration (includes enum constants) */ FIELD, /** Method declaration */ METHOD, /** Formal parameter declaration */ PARAMETER, /** Constructor declaration */ CONSTRUCTOR, /** Local variable declaration */ LOCAL_VARIABLE, /** Annotation type declaration */ ANNOTATION_TYPE, /** Package declaration */ PACKAGE, /** * Type parameter declaration * * @since 1.8 */ TYPE_PARAMETER, /** * Use of a type * * @since 1.8 */ TYPE_USE } @Target(ElementType.TYPE) // 接口、类、枚举、注解 @Target(ElementType.FIELD) // 字段、枚举的常量@Target(ElementType.METHOD) // 方法@Target(ElementType.PARAMETER) // 方法参数@Target(ElementType.CONSTRUCTOR) // 构造函数@Target(ElementType.LOCAL_VARIABLE) // 局部变量@Target(ElementType.ANNOTATION_TYPE) // 注解@Target(ElementType.PACKAGE) // 包5.注解包含在javadoc中：@Documented6.注解可以被继承：@Inherited了解完基础知识我们就可以来实践了实践将上面的代码进行修改添加一个UserValidate注解类@Inherited @Retention(RetentionPolicy.RUNTIME) @Documented @Target({ElementType.FIELD, ElementType.METHOD}) public @interface UserValidate { /** * 用户名长度最大值 */ public int username_maxLength() default 10; /** * 密码长度最小值 */ public int password_minLength() default 2; /** * 密码长度最大值 */ public int password_maxLength() default 20; /** * 是否支持为空 */ public boolean isNotNull() default false; } User对象类,在变量上方添加注解@Data @NoArgsConstructor @AllArgsConstructor public class User { /** * 用户名 不能为空 长度不能大于6 */ @UserValidate(isNotNull = true, username_maxLength = 6) private String username; /** * 密码 不能为空 长度不能低于5 大于13 */ @UserValidate(isNotNull = true, password_minLength = 5, password_maxLength = 13) private String password; } 利用反射获取到注解修饰的属性，并做相应的校验逻辑public class UserCheck { public static boolean check(User user) { if (user == null) { System.out.println("失败：不可为空"); return false; } // 获取User类的所有属性（如果使用getFields，就无法获取到private的属性） Field[] declaredFields = User.class.getDeclaredFields(); for (Field declaredField : declaredFields) { UserValidate userValidate = declaredField.getAnnotation(UserValidate.class); if (declaredField.getName().equals("username")) { if (user.getUsername() == null || "".equals(user.getUsername())) { if (userValidate.isNotNull()) { System.out.println("｛用户名可空校验｝失败：不可为空"); return false; } else { System.out.println("｛用户名可空校验｝通过！！！"); } } if (user.getUsername().length() > userValidate.username_maxLength()) { System.out.println("｛用户名长度校验｝失败"); return false; } else { System.out.println("｛用户名长度校验｝通过！！！"); } } if (declaredField.getName().equals("password")) { if (user.getPassword() == null || "".equals(user.getPassword())) { if (userValidate.isNotNull()) { System.out.println("｛密码可空校验｝失败：不可为空"); return false; } else { System.out.println("｛密码可空校验｝通过！！！"); } } if (user.getPassword().length() < userValidate.password_minLength() || user.getPassword().length() > userValidate.username_maxLength()) { System.out.println("｛密码长度校验｝失败"); return false; } else { System.out.println("｛密码长度校验｝通过！！！"); } } } return true; } } 最后测试类public class Main { public static void main(String[] args) { System.out.println(UserCheck.check(new User("MRyan", "123456"))); System.out.println(UserCheck.check(new User("", "123456"))); System.out.println(UserCheck.check(new User("MRyan", "1234566666666666"))); System.out.println(UserCheck.check(new User("MRyan", "12"))); } }输出结果｛用户名长度校验｝通过！！！ ｛密码长度校验｝通过！！！ true ｛用户名可空校验｝失败：不可为空 false ｛用户名长度校验｝通过！！！ ｛密码长度校验｝失败 false ｛用户名长度校验｝通过！！！ ｛密码长度校验｝失败 false Process finished with exit code 0我们会发现使用注解确实会更方便，利用注解也可以实现一些其它的功能，Spring中就包含了大量的注解。
需求假设现在有一个需求：在项目现有所有类的方法前后打印日志。你如何在不修改已有代码的前提下，如何完成这个需求？直切主题那这个时候我们可以使用动态代理要理解动态代理首先要理解代理模式什么是代理模式？ 代理模式给某一个对象提供一个代理对象，并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。举个例子来说明：假如说我现在想买一辆二手车，虽然我可以自己去找车源，做质量检测等一系列的车辆过户流程，但是这确实太浪费我得时间和精力了。我只是想买一辆车而已为什么我还要额外做这么多事呢？于是我就通过中介公司来买车，他们来给我找车源，帮我办理车辆过户流程，我只是负责选择自己喜欢的车，然后付钱就可以了。用图表示如下：下面用代码来举一个例子说明假设有一个打印机的类public class Printer { public void print(){ System.out.println("打印!"); } }我想在打印之前先记录一下日志怎么做？最简单的方法：在打印的功能前面直接加上记录日志的功能。public class Printer { public void print(){ System.out.println("记录日志！"); System.out.println("打印！"); } }看上去好像没有问题，但是我们修改了打印机的源代码，破坏了面向对象的开闭原则，有可能影响到其它功能。怎么解决呢？很容易可以想到，既然不能修改原来的代码，那我新建一个类吧。public class LogPrinter extends Printer { public void print(){ System.out.println("记录日志！"); System.out.println("打印！"); } }这个类继承了打印机的类，重写了打印机的print方法，提供了记录日志的功能，以后需要打印机的时候使用这个类就好。问题似乎得到了解决，我们可以在这个解决方案的基础上进一步的优化：先抽象出一个接口:public interface IPrinter { void print(); }打印机类实现这个接口:public class Printer implements IPrinter { public void print(){ System.out.println("打印！"); } }创建打印机代理类也实现该接口，在构造函数中将打印机对象传进去，实现接口的打印方法时调用打印机对象的打印方法并在前面加上记录日志的功能:public class PrinterProxy implements IPrinter { private IPrinter printer; public PrinterProxy(){ this.printer = new printer(); } @Override public void print() { System.out.println("记录日志"); printer.print(); } }测试一下：public class Test { public static void main(String[] args) { PrinterProxy proxy = new PrinterProxy(); proxy.print(); } }结果出来了：记录日志打印以后我们就可以直接实例化PrinterProxy对象调用它的打印方法了，这就是静态代理模式，通过抽象出接口让程序的扩展性和灵活性更高了。静态代理是完美无缺的吗？考虑一下，如果我的打印机类中还有别的方法，也需要加上记录日志的功能，就不得不将记录日志的功能写n遍。进一步如果我还有电视机，电冰箱的类里面的所有方法也需要加上记录日志的功能，那要重复的地方就更多了。怎么办？动态代理闪亮登场：要想不重复写记录日志的功能，针对每一个接口实现一个代理类的做法肯定不可行了，可不可以让这些代理类的对象自动生成呢？JDK提供了invocationHandler接口和Proxy类，借助这两个工具可以达到我们想要的效果。invocationHandler接口上场：//Object proxy:被代理的对象 //Method method:要调用的方法 //Object[] args:方法调用时所需要参数 public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }接口里只有一个方法invoke，这个方法非常重要，先混个脸熟，稍后解释。Proxy类上场，它里面有一个很重要的方法 newProxyInstance：//CLassLoader loader:被代理对象的类加载器 //Class<?> interfaces:被代理类全部的接口 //InvocationHandler h:实现InvocationHandler接口的对象 public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException 调用Proxy的newProxyInstance方法可以生成代理对象一切准备就绪动态代理模式千呼万唤始出来：接口IPrinter 和 该接口的实现类 Printer的代码同前。实现一个类，该类用来创建代理对象，它实现了InvocationHandler接口：public class ProxyHandler implements InvocationHandler { private Object targetObject;//被代理的对象 //将被代理的对象传入获得它的类加载器和实现接口作为Proxy.newProxyInstance方法的参数。 public Object newProxyInstance(Object targetObject){ this.targetObject = targetObject; //targetObject.getClass().getClassLoader()：被代理对象的类加载器 //targetObject.getClass().getInterfaces()：被代理对象的实现接口 //this 当前对象，该对象实现了InvocationHandler接口所以有invoke方法，通过invoke方法可以调用被代理对象的方法 return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),targetObject.getClass().getInterfaces(),this); } //该方法在代理对象调用方法时调用 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("记录日志"); return method.invoke(targetObject,args); } }被代理的对象targetObject可以通过方法参数传进来：public Object newProxyInstance(Object targetObject){ this.targetObject=targetObject; }我们重点来分析一下这段代码：return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),targetObject.getClass().getInterfaces(),this);动态代理对象就是通过调用这段代码被创建并返回的。方法有三个参数：第一个参数： targetObject.getClass().getClassLoader()：targetObject对象的类加载器。 第二个参数: targetObject.getClass().getInterfaces()：targetObject对象的所有接口 第三个参数: this：也就是当前对象即实现了InvocationHandler接口的类的对象，在调用方法时会调用它的invoke方法。再来看一下这段代码：public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //在这里可以通过判断方法名来决定执行什么功能 System.out.println("记录日志"); //调用被代理对象的方法 return method.invoke(targetObject, args); }这个方法就是生成的代理类中的方法被调用时会去自动调用的方法，可以看到在这个方法中调用了被代理对象的方法: method.invoke(targetObject, args);我们可以在这里加上需要的业务逻辑，比如调用方法前记录日志功能.见证奇迹的时刻到了：public class Test { public static void main(String[] args){ ProxyHandler proxyHandler=new ProxyHandler(); IPrinter printer=(IPrinter) proxyHandler.newProxyInstance(new Printer()); printer.print(); } }打印结果：记录日志 打印将被代理的对象作为参数传入就可以执行里面的任意方法，所有的方法调用都通过invoke来完成。不用对每个方法进行处理，动态代理是不是很简洁。代理模式的定义：代理模式给某一个对象提供一个代理对象，并由代理对象控制对原对象的引用，通俗的来讲代理模式就是我们生活中常见的中介，动态代理和静态代理的区别在于静态代理我们需要手动的去实现目标对象的代理类，而动态代理可以在运行期间动态的生成代理类。
起因在文章的开始之前，有一个问题需要思考。String s = "MRyan";String s = new String("MRyan");以上是String的两种赋值方式，它们有什么区别吗？它们在内存中有几个实例？存储在哪个区域里？实例存储在哪里？字面量存储在哪里？想要回答这些问题，需要对JVM有一定的了解狂补JVM基础知识都知道JVM的内存结构包括堆，虚拟机栈，方法区，程序计数器，本地方法栈。其中和本文章有关系的1. 堆：作为整个JVM内存结构中占用最大的一块空间，存放对象实例和数组，它是线程共享的。2. 虚拟机栈：在线程的生命周期中，参与计算的数据会频繁的入栈和出栈，栈的生命周期和线程一样，栈内的每条数据都是栈帧。在每个java方法被调用的时候，就会创建一个栈帧并入栈，执行完调用在出栈。栈帧包含：局部变量表，操作数栈，动态链接，返回地址。它是线程独占的 。3. 方法区：又称非堆区，用于存储JVM加载的类型信息，常量，静态变量，代码缓存等数据。在JDK1.7后用元空间替代永久代，随之字符串常量池和静态变量移动到了堆中。它是线程共享的。其中在JDK1.7以后包括常量池，运行时常量池，字符串常量池都由方法区移动到了堆内存中。常量池：即class文件常量池，是class文件的一部分，用于保存编译时确定的数据。包括字面量和符号引用。运行时常量池：它是每个类私有的。每个class文件里的“常量池”在类被加载器加载之后，常量池中的数据就通过映射存放在运行时常量池，由于Java语言并不要求常量一定只能在编译期产生，运行期间也可能产生新的常量，这些常量被放在运行时常量池中，这里所说的常量包括：基本类型包装类（包装类不管理浮点型，整形只会管理-128到127）和String（也可以通过String.intern()方法可以强制将String放入常量池）。字符串常量池：HotSpot VM里，记录interned string的一个全局表叫做StringTable，它本质上就是个HashSet。注意它只存储对java.lang.String实例的引用，而不存储String对象的内容。有了以上知识点，我们可以继续说回String了回归正题编写一段代码如下：package com.mryan.csdn.string; class Test{ public static void main(String[] args){ String s1= "MRyan"; } }声明了一个字面量为“MRyan”的字符串，因为由于String的不可变性（被final修饰）所以字符串"MRyan"作为字面量存储在class文件常量池中。类加载之后class文件里常量池里大部分数据会被加载到运行时常量池，但是String类型的数据的引用会被同时存在字符串常量池中，实例创建在堆中。而其实这只是Test类被类加载的时候，并未有启动程序线程，此时“MRyan”的引用已经存放在字符串常量池中，实例存放在堆中。等主线程开始创建s1变量时，JVM就会到字符串常量池里找，看有没有能equals("MRyan")的String。如果找到了，就在虚拟机栈中当前栈帧的局部变量表里创建s1变量，然后把字符串常量池里对MRyan对象的引用复制给s1变量。找不到的话，才会在heap堆重新创建一个对象，然后把引用驻留到字符串常量区。然后再把引用复制栈帧的局部变量表。画个简图：当定义了多个值为"MRyan"的String，情况是什么样的呢？package com.mryan.csdn.string; class Test{ public static void main(String[] args){ String s1= "MRyan"; String s2= "MRyan"; String s3= "MRyan"; System.out.println(s1==s2); System.out.println(s2==s3); } }输出：和刚才一样，其实是局部变量表里三个变量统一指向同一个堆内存地址。画个简图：但如果是用new关键字来创建字符串，情况就不一样了package com.mryan.csdn.string; class Test{ public static void main(String[] args){ String s1= "MRyan"; String s2= "MRyan"; String s3= new String("MRyan"); System.out.println(s1==s2); System.out.println(s2==s3); } }输出：这时候，s1和s2还是和之前一样。但s3不同因为new关键字会在Heap堆申请一块全新的内存，来创建新对象。虽然字面还是"MRyan"，但是完全不同的对象，有不同的内存地址。画个简图：到这里文章开篇的问题你应该都能答出来吧。
问题介绍List如何在增加元素的同时删除元素。这还不简单？直接上代码： List<String> lists = new ArrayList<>(); lists.add("MRyan"); lists.add("MRyan2"); lists.add("MRyan3"); for (String list : lists) { if (list.equals("MRyan")) { lists.remove(list); } } System.out.println(lists); 然后兴奋的运行程序，结果发现Exception 。很惨，报错了，报了如下错误：Exception in thread "main" java.util.ConcurrentModificationException at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1012) at java.base/java.util.ArrayList$Itr.next(ArrayList.java:966) at com.text.text.main(text.java:18) 报错原因及分析源码发生了什么错误？java.util.ConcurrentModificationException表示ArrayList在迭代的时候如果同时对其进行修改就会抛出的异常。也就是说这是一个并发的问题。那为什么会发生这个错误？接下来我们来从源码下手：定位到ArrayList的源码查看ArrayList的add方法： /** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return {@code true} (as specified by {@link Collection#add}) */ public boolean add(E e) { modCount++; add(e, elementData, size); return true; } 首先modCount进行了自增然后调用了自己的private void add（E e, Object[] elementData, int s）方法。定位到private void add（E e, Object[] elementData, int s）方法 /** * This helper method split out from add(E) to keep method * bytecode size under 35 (the -XX:MaxInlineSize default value), * which helps when add(E) is called in a C1-compiled loop. */ private void add(E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1; }其实就是首先判断是否需要扩容，然后将值添加到数组中，数组大小加1。那你会有疑问了，刚才出现的modCount是什么？别着急往下看定位到remove（Object o）方法： /** * Removes the first occurrence of the specified element from this list, * if it is present. If the list does not contain the element, it is * unchanged. More formally, removes the element with the lowest index * {@code i} such that * {@code Objects.equals(o, get(i))} * (if such an element exists). Returns {@code true} if this list * contained the specified element (or equivalently, if this list * changed as a result of the call). * * @param o element to be removed from this list, if present * @return {@code true} if this list contained the specified element */ public boolean remove(Object o) { final Object[] es = elementData; final int size = this.size; int i = 0; found: { if (o == null) { for (; i < size; i++) if (es[i] == null) break found; } else { for (; i < size; i++) if (o.equals(es[i])) break found; } return false; } fastRemove(es, i); return true; }最后调用了fastRemove（Object[] es, int i）方法。定位到fastRemove（Object[] es, int i）/** * Private remove method that skips bounds checking and does not * return the value removed. */ private void fastRemove(Object[] es, int i) { modCount++; final int newSize; if ((newSize = size - 1) > i) System.arraycopy(es, i + 1, es, i, newSize - i); es[size = newSize] = null; }我们又发现了modCount进行了自增，于是我们得知modCount其实就是修改的次数，add和remove包括clear，addAll等对list元素的操作都会调用它。现在回到我们代码报错的地方Exception in thread "main" java.util.ConcurrentModificationException at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1012) at java.base/java.util.ArrayList$Itr.next(ArrayList.java:966) at com.text.text.main(text.java:18)发现checkForComodification处抛出了异常。 final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } 我们发现是因为判断modCount和expectedModCount不相等于是抛出了异常。那么问题又来了。expectedModCount是什么？？？ /** * An optimized version of AbstractList.Itr */ private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; // prevent creating a synthetic constructor Itr() {} public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } @Override public void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); final int size = ArrayList.this.size; int i = cursor; if (i < size) { final Object[] es = elementData; if (i >= es.length) throw new ConcurrentModificationException(); for (; i < size && modCount == expectedModCount; i++) action.accept(elementAt(es, i)); // update once at end to reduce heap write traffic cursor = i; lastRet = i - 1; checkForComodification(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } } 源码中发现初始化时expectedModCount=modCountexpectedModCount表示的是ArrayList修改次数的期望值迭代时调用next方法@SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; }也是先调用checkForComodification（）方法modCount != expectedModCount则抛出异常。刚才我们发现了remove方法会调用fastRemove方法 之后modCount会进行自增，而expectedModCount没有进行改变，两者不相等则抛出ConcurrentModificationException（）异常。如何解决此问题利用Iterator迭代器删除即可 public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { SubList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = SubList.this.modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } 因为Iterator的remove方法会使expectedModCoun和modCount两值相等，自然没问题了。正确代码： List<String> lists = new ArrayList<>(); lists.add("MRyan"); lists.add("MRyan2"); lists.add("MRyan3"); Iterator<String> iterator=lists.iterator(); while(iterator.hasNext()){ String list=iterator.next(); if(list.equals("MRyan")){ iterator.remove(); } } System.out.println(lists);运行结果：[MRyan2, MRyan3]总结那就是ArrayList 本身不是线程安全的