java内存区域简介以及GC机制

2023-11-14

目录

前言

1.java内存区域简介

1.1 方法区 (Method Area)

1.2 堆区 (Heap)

1.3 程序计数器 (pc 寄存器):

1.4 Java 虚拟机栈(JVM Stacks)

1.5 本地方法栈(Native  Mthod Stacks)

1.6 运行时常量池

2. 元数据以及java创建对象过程

2.1.元数据

2.2 具体实例

2.3 java创建对象的过程

3.垃圾回收机制

3.1.GC简介

3.1.1 不同的垃圾回收器的作用位置

3.1.2.垃圾回收器执行时机:

3.2垃圾判断算法

3.2.1 引用计数法

3.2.2 可达性分析算法

3.2.3 引用判断

3.3 垃圾回收算法

3.3.1 标记-清除算法

3.3.2 标记-整理算法

3.3 .3 复制算法

3.3 .4 分代收集算法


前言

我们在使用计算机的过程中,难免会产生许多使用过废弃的一些文件,正常情况下,我们会使用各种清理软件对垃圾进行清理。而在java编程中,我们同样会产生许多创建使用后丢弃的对象,那么,在java虚拟机中的这些对象我们要如何删除,该如何回收了?这就需要了解java虚拟机内存的分布情况以及相应的GC机制。

1.java内存区域简介

java虚拟机在执行java程序时会将他管理的内存划分为若干个不同的数据区域。

1.1 方法区 (Method Area)

方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。**

HotSpot 虚拟机中方法区也常被称为 **“永久代”**,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。

相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。

1.2 堆区 (Heap)

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建

1.3 程序计数器 (pc 寄存器):

程序计数器也称为(pc寄存器)是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。

1,通过线程拿到cpu的执行权,执行程序,当执行完毕以后,释放cpu的执行权

2.保存线程的位置,当前线程执行完毕后,程序计数器根据线程的泣置获取该线程,然后执行

注意:程序计数器是唯一一个不会出现QutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

1.4 Java 虚拟机栈(JVM Stacks)

与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:在细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

1.5 本地方法栈(Native  Mthod Stacks)

和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。

1.6 运行时常量池

运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)

既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

2. 元数据以及java创建对象过程

2.1.元数据

概念——关于数据的数据或者叫做用来描述数据的数据或者叫做信息的信息。
        我们可以把元数据简单的理解成,最小的数据单位。元数据可以为数据说明其元素或属性(名称、大小、数据类型、等),或其结构(长度、字段、数据列),或其相关数据(位于何处、如何联系、拥有者)。

特点
1.元数据是关于数据的结构化的数据,它不一定是数字形式的,可来自不同的资源。

2.元数据是与对象相关的数据,此数据使其潜在的用户不必先具备对这些对象的存在和特征的完整认识。

3.元数据是对信息包裹(Information Package)的编码的描述。

4.元数据包含用于描述信息对象的内容和位置的数据元素集,促进了网络环境中信息对象的发现和检索。

5.元数据不仅对信息对象进行描述,还能够描述资源的使用环境、管理、加工、保存和使用等方面的情况。

6.在信息对象或系统的生命周期中自然增加元数据。
 

2.2 具体实例

java代码操作数据库时,哪些是元数据信息?

1.ResultSet 结果集(保护:二维表结构(字段,类型,所在列的编号等),数据)

2. ResultSetMetaData元数据;二维表结构(字段,类型,所在列编号等)

3.持久层框架的问题

简单操作:将一行数据分封装到javaBean 简单操作:将一行数据分封装到javaBean

User user=mapper.selectPrimaryKey(1);
user.uid,username,password
tab_user.uid,username,password

2.3 java创建对象的过程

检测类是否被加载没有加载的先加载→为新生对象分配内存→将分配到的内存空间都初始化为零值→对对象进行必要的设置→执行<init>方法把对象进行初始化

1. 检测类是否被加载

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

2.为新生对象分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定。

3.初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4.进行必要的设置

接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。

5、执行init方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚开始,<init>方法还没有执行,所有的字段都还为零。所以一般来说,执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

3.垃圾回收机制

3.1.GC简介

在真实工作中的项目中,时不时的会发生内存溢出、内存泄露的问题,这也是不可避免的Bug,这些潜在的Bug在某些时候会影响到项目的正常运行,如果你的项目没有合理的进行业务内存分配,将会直接影响到的项目的并发处理,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。而了解了GC实现机制则是我们一切监控和调节的前提。

GC机制,通俗来讲,就是JVM虚拟机在清理java中一些已经死亡的对象的一个回收机制。而要详细了解回收机制,首先要了解java虚拟机中堆内存的结构。在Java虚拟机中进行垃圾回收的场所有两个,一个是堆,一个是方法区。在堆中存储了Java程序运行时的所有对象信息,而垃圾回收其实就是对那些“死亡的”对象进行其所侵占的内存的释放,让后续对象再能分配到内存,从而完成程序运行的需要。

3.1.1 不同的垃圾回收器的作用位置

 Minor GC:新生代

 Major Gc:老年代

 Full GC:整个堆(通常用在老年代,永久代)

3.1.2.垃圾回收器执行时机:

Minor GC:当新生代空间不足时,会执行Minor Gc


Major GC: 当老生代空间(新生代空间不足)不足时,会执行Minor GcFull GC:当永久代(或老年代)空间不足时,会执行GC
1.当Full GC执行时,清理老年代空间时,执行完后,空间依然不足
outofmemoryError: java heap space
2.当Full GC执行时,清理永久代空间时,执行完后,空间依然不足

3.2 垃圾判断算法

在 JVM 的眼中,垃圾就是指那些在堆中存在的,已经“死亡”的对象。而对于“死亡”的定义,我们可以简单的将其理解为“不可能再被任何途径使用的对象”。那怎样才能确定一个对象是存活还是死亡呢?这就涉及到了垃圾判断算法,其主要包括引用计数法和可达性分析法。

3.2.1 引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。反之,当引用失效时,例如一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时,则之前被引用的对象的计数器的值就减 1。而那些引用计数为 0 的对象,就可以称之为垃圾,可以被收集。

  • 优点:引用计数法实现起来比较简单,对程序不被长时间打断的实时环境比较有利。
  • 缺点:需要额外的空间来存储计数器,难以检测出对象之间的循环引用。

3.2.2 可达性分析算法

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

reachable-objects

3.2.3 引用判断

无论是引用计数算法,还是可达性分析算法,对象是否存货都跟 “引用” (reference)有关,在JDK1.2 之后,引用可分为以下4个

强引用:直接 new ,如 new Object(); 只要这类强引用还在,对象就不会回收
软引用:(SoftReference类)用来描述一些还有用但非必需的对象;当将来发生内存溢出之前,系统会把这些有软引用的对象列入回收范围中进行二次回收,如果这次回收还没有足够的内存,则内存溢出报异常;
弱引用:(WakeReference类) 被弱引用的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器开始工作时,无论内存是否足够,都会对这些对象进行回收。
虚引用(幽灵引用/幻影引用):(PhantomReference类) 一个虚引用的对象,它的唯一目的就是能在这个对象被收集器回收时收到一个系统通知,即一个对象是否有虚引用,完全不会对其生存时间构成影响。

3.3 垃圾回收算法

垃圾收集算法

 

3.3.1 标记-清除算法

标记-清除(Tracing Collector)算法是最基础的收集算法,为了解决引用计数法的问题而提出。它使用了根集的概念,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的可达性分析法中判定垃圾对象的标记过程。

3.3.2 标记-整理算法

标记-整理(Compacting Collector)算法标记的过程与“标记-清除”算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于“标记-整理”算法的收集器的实现中,一般增加句柄和句柄表。

3.3 .3 复制算法

复制(Copying Collector)算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。


copy-2

3.3 .4 分代收集算法
 

分代收集(Generational Collector)算法的将堆内存划分为新生代、老年代和永久代。新生代又被进一步划分为 Eden 和 Survivor 区,其中 Survivor 由 FromSpace(Survivor0)和 ToSpace(Survivor1)组成。所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。分代收集,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行垃圾回收,以便提高回收效率。

generation

3.5 垃圾回收器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

虽然我们对各个收集器进行比较,但并非了挑选出一个最好的收集器。因为知道现在位置还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器

 

3.5.1 Serial收集器

Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束。

3.5.2 ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。

3.5.3 Parallel Scavenge收集器

Parallel Scavenge 收集器类似于ParNew 收集器。 那么它有什么特别之处呢?

-XX:+UseParallelGC 
​
    使用Parallel收集器+ 老年代串行
​
-XX:+UseParallelOldGC
​
    使用Parallel收集器+ 老年代并行

Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。

3.5.4.Serial Old收集器

Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。

3.5.5 Parallel Old收集器

Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。

3.5.6 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。

CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

  • 初始标记: 暂停所有的其他线程,并记录下直接与root相连的对象,速度很快 ;

  • 并发标记: 同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。

  • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短

  • 并发清除: 开启用户线程,同时GC线程开始对为标记的区域做清扫。

CMS垃圾收集器

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

  • 对CPU资源敏感;

  • 无法处理浮动垃圾;

  • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

3.5.7 G1收集器

G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.

被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。它具备一下特点:

  • 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

  • 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。

  • 空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

  • 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。

G1收集器的运作大致分为以下几个步骤:

  • 初始标记

  • 并发标记

  • 最终标记

  • 筛选回收

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

java内存区域简介以及GC机制 的相关文章

  • 在Java中使用命令行编译多个包

    您好 我一直在使用 IDE 但现在我需要从命令行运行和编译 问题是我有多个软件包 我试图找到答案 但没有任何效果 所以我有 src Support java files Me java files Wrapers java files 你知
  • 使用 Java 在 WebDriver 中按 Ctrl+F5 刷新浏览器

    我已经使用 java 刷新了 WebDriver 中的浏览器 代码如下 driver navigate refresh 如何使用 Java 在 WebDriver 中按 Ctrl F5 来做到这一点 我认为您可以使用 WebDriver 和
  • 如何在远程 WebSphere 上进行 JNDI 查找期间解决 sun/io/MalformedInputException

    我使用 WebSphere 8 5 来托管我的应用程序 并在应用程序服务器上配置了一些 JDBC 资源 我还使用瘦客户端运行时库开发了一个客户端应用程序 当按以下方式执行 JNDI 查找时 env put Context INITIAL C
  • RMI 中的引用传递问题? [复制]

    这个问题在这里已经有答案了 有人可以告诉我我错在哪里 为什么这个 RMI 聊天应用程序不起作用 目标是通过远程对象或序列化对象实现客户端 服务器和逻辑之间的解耦 import javax swing import java awt even
  • 正则表达式获取字符串中的第一个数字和其他字符

    我是正则表达式的新手 想知道如何才能只获取字符串中的第一个数字 例如100 2011 10 20 14 28 55 在这种情况下 我希望它返回100 但该数字也可以更短或更长 我在想类似的事情 0 9 但它单独获取每个数字 100 2001
  • 字符串池可以包含两个具有相同值的字符串吗? [复制]

    这个问题在这里已经有答案了 字符串池可以包含两个具有相同值的字符串吗 String str abc String str1 new String abc Will the second statement with new operator
  • Eclipse:切换选项卡最大化/最小化的热键

    通常 当我在 Eclipse 中工作时 我会处于一个视角 例如调试 并在那里做一些事情 然后我想最大化我正在查看的文件 我双击它并对其进行处理 然后再次双击以最小化回到调试视角 是否有一个热键可以从键盘上执行此操作 Ctrl M will
  • Java-如何将黑白图像加载到二进制中?

    我在 FSE 模式下使用 Java 和 swing 我想将完全黑白图像加载为二进制格式 最好是二维数组 并将其用于基于掩码的每像素碰撞检测 我什至不知道从哪里开始 过去一个小时我一直在研究 但没有找到任何相关的东西 只需将其读入Buffer
  • 从 @JsonProperty 值获取枚举常量

    我有一个标有 JsonProperty 的枚举 用于使用 Jackson 进行 JSON 序列化 反序列化 并且希望获取给定字符串 JsonProperty 的枚举值 public enum TimeBucket JsonProperty
  • 异步迭代器

    我有以下代码 while slowIterator hasNext performLengthTask slowIterator next 由于迭代器和任务都很慢 因此将它们放入单独的线程中是有意义的 这是对迭代器包装器的快速而肮脏的尝试
  • Jenkins 的代码覆盖率 [关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi
  • 我想在java中使用XQuery进行Xml处理

    我想用XQuery用于从 java 中的 Xml 获取数据 但我没有得到需要为此添加哪个 Jar 我在谷歌上搜索了很多 但没有得到任何有用的例子 例如我得到以下链接 https docs oracle com database 121 AD
  • 在 Java 中将弯音发送到 MIDI 音序器

    我了解启动和运行 MIDI 音序器的基础知识 并且希望能够在播放过程中增加 减小序列的音高 但弯音是发送到合成器而不是音序器的消息 我尝试将音序器的接收器设置为合成器的发射器 当我发送弯音短消息时 音序器保持相同的音调 但随后合成器以新的弯
  • 带有 OpenId 提供程序的 Java Spring 安全性

    我有一个 spring MVC 应用程序 另一个客户端应用程序想要使用 open id connect 访问我的 spring 应用程序 如何在服务器端实现开放ID提供商 请帮忙 MITREid 连接 OpenID Connect Java
  • JSch中如何设置文件类型和文件传输模式?

    我使用 Apache Common NetFTPClient并设置了我的ftpClient在上传文件之前使用如下所示的方法 ftpClient setFileType FTP BINARY FILE TYPE ftpClient setFi
  • JavaFX - 为什么多次将节点添加到窗格或不同的窗格会导致错误?

    我现在正在学习基本的 JavaFX 我不明白我正在阅读的书中的这一说法 不 诸如文本字段之类的节点只能添加到一个窗格中一次 将节点添加到多次窗格或不同的窗格将导致运行时错误 我可以从书中提供的UML图看出它是一个组合 但我不明白为什么 库类
  • 了解 Spark 中的 DAG

    问题是我有以下 DAG 我认为当需要洗牌时 火花将工作划分为不同的阶段 考虑阶段 0 和阶段 1 有些操作不需要洗牌 那么为什么 Spark 将它们分成不同的阶段呢 我认为跨分区的实际数据移动应该发生在第 2 阶段 因为这里我们需要cogr
  • Java中的回调接口是什么?

    SetObserver 接口的代码片段取自有效的Java 避免过度同步第67条 public interface SetObserver
  • 对于当前月份和日期但年份不同的日期,经过的月份计算未给出正确的结果

    我正在尝试计算自特定日期以来经过的月份 该函数工作正常 尽管如果我将今天的日期与过去的不同年份放在一起 它会给我一个月的差异 不到一个月 假设对于所有日期 该函数都运行良好 除了 如果今天是 2014 03 06 YYYY MM DD 并且
  • 为什么应该首选 Java 类的接口?

    PMD https pmd github io 将举报以下违规行为 ArrayList list new ArrayList 违规行为是 避免使用 ArrayList 等实现类型 而是使用接口 以下行将纠正违规行为 List list ne

随机推荐