1. GC概述
垃圾回收(Garbage Collection,简称GC)机制是JVM中最重要的部分之一。在Java程序运行的过程中,运行时数据区域(包括堆和栈等内存区域)一直都需要使用和回收内存空间。由于Java中的内存分配方式是动态的,所以在程序运行期间,其内存空间的占用量会不断变化。
如果Java程序没有进行垃圾回收,那么程序运行过程中使用的内存空间将不断累加,最后内存会被完全占用,导致程序崩溃。因此,为了保证程序正常运行,避免内存被耗尽和泄漏问题,JVM中设计了垃圾回收机制,用来定期清理无用的对象,并回收内存空间。
在JVM中,GC操作是一个自动化过程,由JVM自动执行。JVM把一些没有被引用的对象或不再使用的对象称为“垃圾”,在程序运行中,GC机制会对这些对象进行标记,并自动回收其占用的内存空间。这样,就可以让应用程序专注于业务逻辑上,而不需要去关心内存管理的问题。
2. 垃圾判断算法
垃圾判断算法通常有两种实现方式:引用计数法和可达性分析法。
引用计数法是一种简单和直观的垃圾判断算法,它通过统计每个对象被引用的次数来判断它是否为垃圾。当一个对象被新的引用指向时,它的引用计数就会加1;当一个引用失效时,对应的对象的引用计数就会减1。当引用计数为0时,JVM就认为该对象已经成为垃圾,并将其回收。
但是,引用计数法有一个很明显的缺陷,即无法解决循环依赖的情况。比如,如果存在两个对象相互引用,它们的引用计数都不为0,但是它们实际上已经成为了垃圾,而由于它们之间互相引用,导致引用计数法无法正确识别。
相比之下,可达性分析法是一种更为普遍和有效的垃圾判断算法。这种算法认为,一个对象是不可用的,当且仅当没有任何一个引用可以到达该对象。换句话说,只要存在一条从根对象(如静态变量、本地变量等)开始的引用链可以到达该对象,那么该对象就是有用的。
JVM采用可达性分析法来判断对象是否为垃圾。从GC Roots作为起点开始扫描,通过向下递归搜索并标记所有被引用的对象,最终将没有标记的对象标记为垃圾并进行回收。在实现时可以采用可达性分析算法的复杂度,即O(n),其中n为对堆中对象进行遍历所需要的时间。
3. GC算法
GC的实现依赖于GC算法,常见的GC算法包括:标记-清除算法、复制算法、标记-整理算法和分代算法。
- 标记-清除算法
标记-清除算法(Mark-Sweep Algorithm)是最基本的垃圾回收算法之一。它分为两个阶段:标记阶段和清除阶段。在标记阶段,GC会遍历所有对象,并标记所有需要回收的对象。在清除阶段,GC会将所有被标记的对象进行删除。
然而,标记-清除算法存在几个明显的缺点:
- 效率低下。由于回收过程中需要遍历所有对象,因此时间复杂度较高,不适合大型应用程序。
- 空间效率低下。标记-清除算法无法对碎片化空间进行整理,导致内存空间浪费。
- 复制算法
为了解决标记-清除算法的效率和空间问题,复制算法(Copying Algorithm)被提出。该算法将可用内存空间分为两块,每次只使用其中一块。当这一块内存用完后,GC将其中存活的对象复制到另一块未使用的内存中,然后重新启动程序。
复制算法以牺牲空间换取时间的方式,提高了垃圾回收机制的效率。但同样也存在一个缺点,即将内存分为两块损失了一半的空间。
- 标记-整理算法
标记-整理算法(Mark-Compact Algorithm)是一种改进的标记-清除算法。该算法与标记-清除算法类似,但在标记阶段完成后,将所有需要回收的对象移到内存的一端,再将其余未被标记的对象移到另一端,从而使得内存中的空间连续。
由于标记-整理算法可以避免内存碎片问题,因此它通常用于长时间运行的服务器和大型应用程序。
- 分代算法
在实际应用中,很多对象都具有不同生命周期。比如,一些对象经常会在程序的初始阶段创建和使用,而一些对象则会在程序的后期阶段创建和使用。基于这种情况,分代算法(Generational Algorithm)被提出。
分代算法将内存分为几个不同的区域(通常是新生代、老年代和持久代),并根据对象的生命周期分配到相应的区域。对于新生代,采用复制算法进行回收;对于老年代,采用标记-清除算法或标记-整理算法进行回收。同时分代算法也支持熬过虚拟机参数来进行相关设置。
4. GC的类型
JVM中主要有两种GC类型:串行GC和并行GC。
- 串行GC
串行GC(Serial Garbage Collector)是默认的垃圾回收器,也是最古老的垃圾回收器之一。它采用单线程方式进行垃圾回收,即同时只有一个线程在执行GC操作。这种GC模式适用于小型应用程序,或者是对响应时间要求不高的系统。
- 并行GC
并行GC(Parallel Garbage Collector)是一种基于多线程的垃圾回收器。它将堆空间分为多块,然后使用多个线程并行回收垃圾对象。并行GC具有快速、高效的特点,可以同时使用多个CPU核心来执行垃圾回收任务,因此适用于大型应用程序和对响应时间要求较高的系统。
5. GC的实现
JVM实现GC机制主要通过以下几点:
-
内存分配策略。JVM使用两种方式来分配内存空间:指针碰撞(Bump the Pointer)和空闲列表(Free List)。指针碰撞法是指对于堆内存来说,有一个固定的指针表示已分配区域的末尾位置。每次分配内存时,JVM只需要将指针向前移动到未分配地址的位置,并将这段区域标记为已分配,从而完成内存分配。空闲列表则是将堆内存划分为不同大小的块,然后维护一个空闲块的列表,在需要分配内存时从该列表中查找,直到找到合适大小的空闲块为止。
-
对象的创建和销毁。在JVM中,新创建的对象通常都会被放入新生代中。对于大部分应用程序来说,新生代中的对象往往有较短的生命周期,因此通过复制算法进行回收,以提高GC效率。而老年代中则包含着更多的持久对象,这些对象在程序运行期间不断被使用,因此采用标记-清除或标记-整理算法进行回收。
-
GC线程和GC触发方式。JVM会在特定条件下触发GC操作,包括以下几种GC线程和触发方式:
-
Serial GC线程。当JVM采用串行GC时,只会有一个GC线程运行。
-
Parallel GC线程。当JVM采用并行GC时,可以通过设置JVM参数,在不同的CPU核心上创建多个GC线程来提高GC效率。
-
CMS GC线程。CMS(Concurrent Mark and Sweep)是一种基于标记-清除算法的垃圾回收器,它允许应用程序在GC操作过程中继续执行。CMS GC线程用于执行并发标记阶段和并发清除阶段的任务。
-
G1 GC线程。G1(Garbage First)是一种基于分代算法的垃圾回收器,它将内存分为多个区域,并采用多线程进行垃圾回收。G1 GC线程用于执行各个分区的垃圾回收任务。
GC的触发方式包括:
-
Minor GC。Minor GC通常是指对新生代进行垃圾回收。当新生代满了,或者到达一定比例时,就会触发Minor GC操作。
-
Major GC。Major GC通常是指对老年代进行垃圾回收。当老年代空间不足时,就会触发Major GC操作。
-
Full GC。Full GC指对整个堆空间进行垃圾回收,包括新生代和老年代。Full GC通常是由Major GC或CMS GC触发的。
JVM提供了多种垃圾回收器和相关参数,可以根据应用程序的特点和需求进行选择和配置,以达到最佳的GC效果和性能。
以下是对JVM中GC的优化方式:
-
合理配置堆大小。堆大小的配置会直接影响到GC的效率和性能,一般来说,越大的堆空间可以降低GC的频率和时间,但同时也会增加GC的暂停时间。因此,需要根据应用程序的需求和特点,合理配置堆大小。
-
选择合适的GC算法。JVM提供了多种GC算法,每种算法都有其适用场景和优缺点。例如,对于需要快速响应的实时系统,可以选择CMS GC算法;而对于需要处理大量垃圾对象的系统,可以选择G1 GC算法。
-
减少全局共享数据的使用。全局共享数据的使用会导致GC线程竞争共享数据,从而影响GC的效率和性能。因此,应该尽量减少全局共享数据的使用,避免GC线程之间的竞争。
-
尽可能使用局部变量。局部变量存储在栈中,可以避免在堆中创建对象,从而减少垃圾回收的频率。
-
对象重用。重复使用对象可以避免频繁创建和销毁对象,从而降低GC的负担。
-
避免使用finalizer方法。finalizer是一种不稳定、不可控的方式,尽量避免使用,以免影响GC的效率和性能。
-
使用内存分配器。使用内存分配器可以减少GC的压力,例如,JVM提供了TLAB(Thread Local Allocation Buffer)机制,可以为每个线程分配独立的内存缓存。
综上所述,对于JVM中的GC优化,需要充分考虑应用程序的需求和特点,选择合适的GC算法和堆大小,并尽可能避免全局共享数据的使用,使用局部变量和对象重用等策略来降低GC的负担,最终提高系统的性能和稳定性。