Bootstrap

JVM-垃圾回收与内存分配

目录

垃圾收集器与内存分配策略

引用

对象的访问方式有哪些?(句柄和直接指针)

Java的引用有哪些类型?

如何判断对象是否是垃圾?

请列举一些可作为GC Roots的对象?

对象头了解吗? mark word(hashcode、分代、锁标志位)、指向类信息的指针和数组长度(数组才有)

Object a = new object()的大小

对象引用占多少大小?(类型指针)

执行main方法的过程

创建对象的过程

对象的生命周期

什么是指针碰撞?什么是空闲列表?(用于给对象分配内存)

JVM 里 new 对象时,堆会发生抢占吗?JVM 是怎么设计来保证线程安全的?

垃圾收集器 (主要作用于方法区(常量)和堆(实例对象))

Full GC 的触发条件?

聊聊Java中的GC,分别作用在什么范围

有哪些GC算法? (可达性分析)标记清除算法(内存碎片)->复制算法(内存利用低)->标记整理算法->分代回收

说一下新生代的区域划分?

你知道哪些垃圾收集器?

Serial收集器 (复制 新生代 单线程)

Serial Old 收集器 (单线程 标记整理)

ParNew收集器 (复制 新生代 多线程 )

Parallel Scavenge 收集器 (新生代、多线程 、复制、 关注垃圾回收吞吐量)

Parallel Old 收集器 (多线程 标记整理)

CMS收集器 (并发标记清除,追求最短STW) 老年代

G1收集器(高吞吐低停顿的平衡) 分代回收, 标记整理+复制

ZGC 收集器

工作中项目使用的什么垃圾回收算法?

G1回收器的特色是什么?

GC只会对「堆」进行GC吗? (还会针对方法区)

有了 CMS,为什么还要引入 G1?

垃圾收集器应该如何选择?


垃圾收集器与内存分配策略

引用

对象的访问方式有哪些?(句柄和直接指针

Java程序会通过栈上的引用操作堆上的具体对象,对象的访问方式由虚拟机决定,主流访问方式主要有句柄和直接指针。

句柄 : 堆会划分出一块内存作为句柄池引用中存储对象的句柄地址,句柄包含对象实例数据与类型数据的地址信息。

优点是引用中存储的是稳定句柄地址,在GC过程中对象被移动时只会改变句柄的实例数据指针,而引用本身不需要修改。

直接指针 :引用中存储的直接就是对象的地址。对象包含到对象类型数据的指针,通过这个指针可以访问对象类型数据。

使用直接指针访问方式最大的好处就是访问对象速度快,它节省了一次指针定位的时间开销

虚拟机hotspot主要是使用直接指针来访问对象

Java的引用有哪些类型?

JDK1.2后对引用进行了扩充,按强度分为四种:

强引用:最常见的引用,例如Object obj - new Object()就属于强引用。只要对象有强引用指向且GC Roots可达,在内存回收时即使濒临内存耗尽也不会被回收。

软引用:弱于强引用,描述非必需对象。在系统将发生内存溢出前,会把软引用关联的对象加入回收范围以获得更多内存空间。用来缓存服务器中间计算结果及不需要实时保存的用户行为等。

弱引用:弱于软引用,描述非必需对象。弱引用关联的对象只能生存到下次YGC(Minor GC)前垃圾收集器开始工作时无论当前内存是否足够都会回收只被弱引用关联的对象。由于YGC具有不确定性,因此弱引用何时被回收也不确定。

虚引用:最弱的引用,定义完成后无法通过该引用获取对象。唯一目的就是为了能在对象被回收时收到一个系统通知虚引用必须与引用队列联合使用,垃圾回收时如果出现虚引用,就会在回收对象前把这个虚引用加入引用队列。

虚引用看起来和弱引用没啥区别,只是必须搭配ReferenceQueue。用虚引用的目的一般是跟踪对象被回收的活动。

如何判断对象是否是垃圾?

引用计数:在对象中添加一个引用计数器,如果被引用,计数器加1,引用失效计数器减1,如果计数器为0则被标记为垃圾。原理简单,效率高,但是在Java 中很少使用,因为存在对象间循环引用的问题,导致计数器无法清零。

可达性分析: 主流语言的内存管理都使用可达性分析判断对象是否存活。基本思路是通过称为 GC Roots 的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为引用链如果某个对象到 GC Roots 没有任何引用链相连,则会被标记为垃圾。

可作为 GC Roots的对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。 (引用的对象)

请列举一些可作为GC Roots的对象?

所谓的 GC Roots,就是一组必须活跃的引用,不是对象,它们是程序运行时的起点,是一切引用链的源头。

在 Java 中,GC Roots 包括以下几种:

  • 虚拟机栈中引用的对象,例如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 方法区中类静态属性引用的对象,例如Java类的引用类型静态变量。
  • 方法区中常量引用的对象,例如字符串常量池(String Table)里的引用。
  • 本地方法栈中Native方法引用的对象
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反映Java虚拟机内部情况的JM XBean、JVM TI中注册的回调、本地代码缓存等。
对象头了解吗? mark word(hashcode、分代、锁标志位)、指向类信息的指针和数组长度(数组才有)

在 HotSpot 中,对象在堆内存中的存储布局可以划分为三个部分:对象头(Object Header)、实例数据(Instance Data)和对齐填充(Padding)

  1. 对象的实例数据就是存储了对象的具体信息,属性和类型。
  2. 对齐填充字节:因为JVM要求对象占的内存大小是8bit 的倍数,因此后面有几个字节用于把对象的大小补齐至 8bit的倍数。(空间换时间)

CPU 进行内存访问时,一次寻址的指针大小是 8 字节,正好是 L1 缓存行的大小。如果不进行内存对齐,则可能出现跨缓存行访问,导致额外的缓存行加载,降低了 CPU 的访问效率。

  1. 对象头由以下三部分组成:mark word、指向类信息的指针数组长度(数组才有)。
  • mark word 包含:包含了对象自身的运行时数据,如哈希码HashCode)、垃圾回收分代年龄、锁状态标志、线程持有的锁、偏向线程 ID 等信息。在 64 位操作系统下占 8 个字节,32 位操作系统下占 4 个字节。
  • 类型指针(Class Pointer):指向对象所属类的元数据的指针,JVM 通过这个指针来确定对象的类。在开启了压缩指针的情况下,这个指针可以被压缩。在开启指针压缩的情况下占 4 个字节,否则占 8 个字节。在 JDK 8 中,压缩指针默认是开启的,以减少 64 位应用中对象引用的内存占用。

内存对齐的主要作用是:

  1. 平台原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:经过内存对齐后,CPU的内存访问速度大大提升。

Object a = new object()的大小

一般来说,对象的大小是由对象头、实例数据和对齐填充三个部分组成的。

  • 对象头的大小在 32 位 JVM 上是 8 字节,在 64 位 JVM 上是 16 字节(如果开启了压缩指针,就是 12 字节)。
  • 实例数据的大小取决于对象的属性和它们的类型。对于new Object()来说,Object 类本身没有实例字段,因此这部分可能非常小或者为零。
  • 对齐填充的大小取决于对象头和实例数据的大小,以确保对象的总大小是 8 字节的倍数。

一般来说,目前的操作系统都是 64 位的,并且 JDK 8 中的压缩指针是默认开启的,因此在 64 位 JVM 上,new Object()的大小是 16 字节(12 字节的对象头 (8+4)+ 4 字节的对齐填充)。

对象引用占多少大小?(类型指针)

在 64 位 JVM 上,未开启压缩指针时,对象引用占用 8 字节;开启压缩指针时,对象引用可被压缩到 4 字节。

而 HotSpot JVM 默认开启了压缩指针,因此在 64 位 JVM 上,对象引用占用 4 字节。

main方法的过程
  1. 编译.java后得到.class 后,执行这个class文件,系统会启动一个JVM进程,进行类的加载,根据全限定名找到二进制字节文件,转化成方法区的运行时数据结构。
  2. JVM 找到的主程序入口,执行main方法
  3. main方法的第一条语句为Person p = new Person("大彬"),就是让JVM创建一个Person 对象,但是这个时候方法区中是没有Person类的信息的,所以JVM马上加载 Person类,把Person类的信息放到方法区中。
  4. 加载完Person类后,JVM 在堆中分配内存给Person 对象,然后调用构造函数初始化 Person 对象,这个Person 对象持有指向方法区中的Person类的类型信息的引用。
  5. 执行p.getName()时,JVM根据 p 的引用找到 p 所指向的对象,然后根据此对象持有的引用定位到方法区中Person类的类型信息的方法表,获得getName()的字节码地址。
  6. 执行getName()方法。
创建对象的过程

在Java中创建对象的过程包括以下几个步骤:

  1. 类加载检查:虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程
  2. 分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
  3. 初始化零值内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(成员变量,数值类型是 0,布尔类型是 false,对象类型是 null
    1. (不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  1. 进行必要设置,比如对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
  2. 执行 init 方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始——构造函数,即class文件中的方法还没有执行,所有的字段都还为零,对象需要的其他资源和状态信息还没有按照预定的意图构造好。所以一般来说,执行 new 指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全被构造出来。成员变量赋值为预期的值
对象的生命周期

对象的生命周期包括创建、使用和销毁三个阶段:

  • 创建:对象通过关键字new在堆内存中被实例化,构造函数被调用,对象的内存空间被分配。
  • 使用:对象被引用并执行相应的操作,可以通过引用访问对象的属性和方法,在程序运行过程中被不断使用。
  • 销毁:当对象不再被引用时,通过垃圾回收机制自动回收对象所占用的内存空间。垃圾回收器会在适当的时候检测并回收不再被引用的对象,释放对象占用的内存空间,完成对象的销毁过程。
什么是指针碰撞?什么是空闲列表?(用于给对象分配内存)

在堆内存分配对象时,主要使用两种策略:指针碰撞和空闲列表。

①、指针碰撞(Bump the Pointer)

假设堆内存是一个连续的空间,分为两个部分,一部分是已经被使用的内存,另一部分是未被使用的内存。

在分配内存时,Java 虚拟机维护一个指针,指向下一个可用的内存地址,每次分配内存时,只需要将指针向后移动(碰撞)一段距离,然后将这段内存分配给对象实例即可。

②、空闲列表(Free List)

JVM 维护一个列表,记录堆中所有未占用的内存块,每个空间块都记录了大小和地址信息。

当有新的对象请求内存时,JVM 会遍历空闲列表,寻找足够大的空间来存放新对象。

分配后,如果选中的空闲块未被完全利用,剩余的部分会作为一个新的空闲块加入到空闲列表中。

指针碰撞适用于管理简单、碎片化较少的内存区域(如年轻代),而空闲列表适用于内存碎片化较严重或对象大小差异较大的场景(如老年代)。

JVM 里 new 对象时,堆会发生抢占吗?JVM 是怎么设计来保证线程安全的?

会,假设 JVM 虚拟机上,每一次 new 对象时,指针就会向右移动一个对象 size 的距离,一个线程正在给 A 对象分配内存,指针还没有来的及修改,另一个为 B 对象分配内存的线程,又引用了这个指针来分配内存,这就发生了抢占。

有两种可选方案来解决这个问题:

  • 采用CAS 分配重试的方式来保证更新操作的原子性
  • 每个线程在 Java 堆中预先分配一小块内存,也就是本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),要分配内存的线程,先在本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。

垃圾收集器 (主要作用于方法区(常量)和堆(实例对象))

Full GC 的触发条件?

对于Minor GC,其触发条件比较简单,当Eden 空间满时,就将触发一次Minor GC

而Full GC触发条件相对复杂,有以下情况会发生 full GC:

  1. 调用 System.gc()

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

  1. 老年代空间不足

老年代空间不足的常见场景为大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。

除此之外,可以通过 -Xmn参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

  1. 空间分配担保失败

新生代的 To 区放不下从 Eden 和 From 拷贝过来对象,或者新生代对象 GC 年龄到达阈值需要晋升这两种情况,老年代如果放不下的话都会触发 Full GC。

  1. JDK 1.7及以前的永久代空间不足

在JDK 1.7及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些Class的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用CMS GC 的情况下也会执行 Full GC。如果经过 Full GC仍然回收不了,那么虚拟机抛出 java.lang.OutOfMemoryError。

聊聊Java中的GC,分别作用在什么范围
  • 新生代收集(Minor GC / Young GC) 只是新生代的垃圾收集
  • 老年代收集 (Major GC / Old GC) 只是老年代的垃圾收集(目前只有CMS GC会有单独收集老年代的行为)
  • 整堆收集(Full GC)收集整个堆和方法区(元空间)的垃圾
  • 混合收集(Mixed GC)收集整个新生代以及部分老年代的垃圾收集 (目前只有G1会有这种行为)
有哪些GC算法? (可达性分析)标记清除算法(内存碎片)->复制算法(内存利用低)->标记整理算法->分代回收

Java的内存回收机制基于自动内存管理,开发人员无需手动释放内存。垃圾回收器会自动识别不再使用的对象,并回收它们所占用的内存空间。

垃圾回收算法主要有 :

  • 标记-清除算法:标记-清除算法分为“标记”和“清除”两个阶段,首先通过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。
    • 标记-清除算法有两个缺陷,一个是效率问题,标记和清除的过程效率都不高,另外一个就是,清除结束后会造成大量的碎片空间。有可能会造成在申请大块内存的时候因为没有足够的连续空间导致再次 GC。
  • 复制算法:为了解决碎片空间的问题,出现了“复制算法”。复制算法的原理是,将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。
    • 复制算法解决了空间碎片的问题。但是也带来了新的问题。因为每次在申请内存时,都只能使用一半的内存空间。内存利用率严重不足
  • 标记-整理算法复制算法在 GC 之后存活对象较少的情况下效率比较高,但如果存活对象比较多时,会执行较多的复制操作,效率就会下降。而老年代的对象在 GC 之后的存活率就比较高,所以就有人提出了“标记-整理算法”。标记-整理算法的“标记”过程与“标记-清除算法”的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都移动到内存的一端。移动结束后直接清理掉剩余部分。
  • 分代回收算法:分代收集是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15,可以通过参数 -XX:MaxTenuringThreshold 来设定)后,如果对象还存活,那么该对象会进入老年代。
说一下新生代的区域划分?

新生代的垃圾收集主要采用标记-复制算法,因为新生代的存活对象比较少,每次复制少量的存活对象效率比较高。

基于这种算法,虚拟机将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次分配内存只使用 Eden 和其中一块 Survivor。发生垃圾收集时,将 Eden 和 Survivor 中仍然存活的对象一次性复制到另外一块 Survivor 空间上,然后直接清理掉 Eden 和已用过的那块 Survivor 空间。默认 Eden 和 Survivor 的大小比例是 8∶1。

你知道哪些垃圾收集器?

垃圾回收器主要分为以下几种:Serial、ParNew、Parallel Scavenge、Serial old、 Parallel old、CMS、G1.

Serial收集器 (复制 新生代 单线程)

单线程收集器,使用一个垃圾收集线程去进行垃圾回收,在进行垃圾回收的时候必须暂停其他所有的工作线程(Stop The World),直到它收集结束。

特点:简单高效;内存消耗小;没有线程交互的开销,单线程收集效率高;需暂停所有的工作线程,用户体验不好。

Serial 是虚拟机在客户端模式的默认新生代收集器

Serial Old 收集器 (单线程 标记整理)

Serial收集器的老年代版本,单线程收集器,使用标记整理算法。

ParNew收集器 (复制 新生代 多线程 )

Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其他行为、参数与 Serial 收集器基本一致。

Parallel Scavenge 收集器 (新生代、多线程 、复制、 关注垃圾回收吞吐量)

新生代收集器,基于复制算法实现的收集器。特点是吞吐量优先,能够并行收集的多线程收集器,允许多个垃圾回收线程同时运行,降低垃圾收集时间,提高吞吐量。

Parallel Scavenge收集器关注点是吞吐量,高效率的利用 CPU资源。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的

-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

  • -XX:MaxGCPauseMillis参数的值是一个大于0的毫秒数,收集器将尽量保证内存回收花费的时间不超过用户设定值。
  • -XX:GCTimeRatio参数的值大于0小于100,即垃圾收集时间占总时间的比率,相当于吞吐量的倒数。
Parallel Old 收集器 (多线程 标记整理)

Parallel Scavenge收集器的老年代版本。多线程垃圾收集,使用标记整理算法。

CMS收集器 (并发标记清除,追求最短STW) 老年代

初始标记和重新标记会 STW,JDK 1.5 时引入,JDK9 被标记弃用,JDK14 被移除。

Concurrent Mark Sweep,并发标记清除,追求获取最短停顿时间,实现了让垃圾收集线程与用户线程基本上同时工作。

CMS垃圾收集器关注点更多的是用户线程的停顿时间

CMS垃圾回收基于标记清除算法实现,整个过程分为四个步骤:

  • 初始标记:暂停所有用户线程(Stop The World),记录直接与GCRoots直接相连的对象。
  • 并发标记:从GC Roots 开始对堆中对象进行可达性分析,找出存活对象,耗时较长,但是不需要停顿用户线程
  • 重新标记:在并发标记期间对象的引用关系可能会变化,需要重新进行标记。此阶段也会暂停所有用户线程。
  • 并发清除:清除标记对象,这个阶段也是可以与用户线程同时并发的。

在整个过程中,耗时最长的是并发标记和并发清除阶段,这两个阶段垃圾收集线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

优点:并发收集,停顿时间短。

缺点:

  • 标记清除算法导致收集结束有大量空间碎片。
  • 产生浮动垃圾,在并发清理阶段用户线程还在运行,会不断有新的垃圾产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中回收它们,只好等到下一次垃圾回收再处理;

G1收集器(高吞吐低停顿的平衡) 分代回收, 标记整理+复制

G1(Garbage-First Garbage Collector)在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为了默认的垃圾收集器。G1 有五个属性:分代、增量、并行、标记整理、STW。

G1垃圾收集器的目标是在不同应用场景中追求高吞吐量和低停顿之间的最佳平衡。

G1将整个堆分成相同大小的分区 ( Region),有四种不同类型的分区:Eden、 Survivor、Old和Humongous。分区的大小取值范围为1M到 32M,都是2的幂次方。

分区大小可以通过 -XX:G1HeapRegionSize 参数指定。

Humongous区域用于存储大对象。G1规定只要大小超过了一个分区容量一半的对象就认为是大对象。

G1收集器对各个分区回收所获得的空间大小和回收所需时间的经验值进行排序,得到一个优先级列表,每次根据用户设置的最大回收停顿时间,优先回收价值最大的分区。也是名字的由来(Garbage-First)

特点:可以由用户指定期望的垃圾收集停顿时间。

G1收集器的回收过程分为以下几个步骤:

  • 初始标记。暂停所有其他线程,记录直接与GC Roots直接相连的对象,耗时较短。
  • 并发标记。从GC Roots 开始对堆中对象进行可达性分析,找出要回收的对象,耗时较长,不过可以和用户程序并发执行。
  • 最终标记。需对其他线程做短暂的暂停,用于处理并发标记阶段对象引用出现变动的区域。
  • 筛选回收对各个分区的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,然后把决定回收的分区的存活对象复制到空的分区中,再清理掉整个旧的分区的全部空间

G1 在垃圾回收期间仍然需要「Stop the World」。不过,G1 在停顿时间上添加了预测机制,用户可以 JVM 启动时指定期望停顿时间,G1 会尽可能地在这个时间内完成垃圾回收。

这里的操作涉及存活对象的移动,会暂停用户线程,由多条收集器线程并行完成

ZGC 收集器

ZGC 是 JDK 11 时引入的一款低延迟的垃圾收集器,它的目标是在不超过 10ms 的停顿时间内,为堆大小达到 16TB 的应用提供一种高吞吐量的垃圾收集器。

ZGC 的两个关键技术:指针染色和读屏障,不仅应用在并发转移阶段,还应用在并发标记阶段:将对象设置为已标记,传统的垃圾回收器需要进行一次内存访问,并将对象存活信息放在对象头中;而在 ZGC 中,只需要设置指针地址的第 42-45 位即可,并且因为是寄存器访问,所以速度比访问内存更快。

工作中项目使用的什么垃圾回收算法?

我们生产环境中采用了设计比较优秀的 G1 垃圾收集器,G1 采用的是分区式标记-整理算法,将堆划分为多个区域,按需回收,适用于大内存和多核环境,能够同时考虑吞吐量和暂停时间。

或者:

我们系统采用的是 CMS 收集器,CMS 采用的是标记-清除算法,能够并发标记和清除垃圾,减少暂停时间,适用于对延迟敏感的应用。

再或者:

我们系统采用的是 Parallel 收集器,Parallel 采用的是年轻代使用复制算法,老年代使用标记-整理算法,适用于高吞吐量要求的应用。

G1回收器的特色是什么?

G1回收器的特色在于它堆内存划分成多个大小相等的独立区域,并且通过维护一个优先列表来进行局部区域的垃圾收集,从而减少全堆垃圾收集的频率和停顿时间G1也特别注重停顿时间的可预测性,并允许用户指定期望的停顿时间目标。

GC只会对「堆」进行GC吗? (还会针对方法区)

主要的垃圾收集活动确实发生在堆内存中,因为这是大多数Java对象存活和死亡的地方。不过,方法区也是垃圾收集的目标之一,例如回收废弃常量和无用的类。

程序计数器虚拟机栈和本地方法栈通常随线程而生,随线程而灭,所以它们不是垃圾收集的目标。

有了 CMS,为什么还要引入 G1?

CMS 适用于对延迟敏感的应用场景,主要目标是减少停顿时间,但容易产生内存碎片。G1 则提供了更好的停顿时间预测和内存压缩能力,适用于大内存和多核处理器环境。

垃圾收集器应该如何选择?

这里简单地列一下上面提到的一些收集器的适用场景:

  • Serial :如果应用程序有一个很小的内存空间(大约 100 MB)亦或它在没有停顿时间要求的单线程处理器上运行。
  • Parallel:如果优先考虑应用程序的峰值性能,并且没有时间要求要求,或者可以接受 1 秒或更长的停顿时间。
  • CMS/G1:如果响应时间吞吐量优先级高,或者垃圾收集暂停必须保持在大约 1 秒以内。
  • ZGC:如果响应时间是高优先级的,或者堆空间比较大。

自己整理,借鉴很多博主,感谢他们

;