JVM之垃圾回收

1. 如何判断对象可以回收

1.1 引用计数法

引用计数法是一种内存管理技术,其中每个对象都有一个与之关联的引用计数。引用计数表示当前有多少个指针引用了该对象。当引用计数变为零时,表示没有指针再指向该对象,该对象可以被释放,因为没有程序可以再访问它。

基本思想是在对象创建时初始化引用计数为1,每当有新的引用指向对象时,引用计数加1,当引用离开作用域或者被显式赋值为其他值时,引用计数减1。当引用计数为零时,释放对象的内存。

虽然引用计数法简单,但它有一些缺点,其中最主要的是不能解决循环引用的问题。如果两个或多个对象形成环状引用(彼此相互引用),它们的引用计数永远不会变为零,即使程序不再使用它们,这会导致内存泄漏。

1.2 可达性分析法

可达性分析是一种垃圾回收算法,用于确定在程序执行期间哪些对象是可访问的,即哪些对象可以被程序的根引用直接或间接访问到。这种算法通过从根引用出发,沿着对象之间的引用链,标记所有可达的对象,然后将未标记的对象认定为不可达,从而进行垃圾回收。

在可达性分析算法中,根对象是算法的起始点,从这些根对象开始追踪引用链以标记可达的对象。以下是一些典型的可达性分析算法的根对象:

  1. 虚拟机栈的本地变量引用的对象(Local Variables in Stack Frames):

    属于当前线程的栈帧中的本地变量可以作为根对象。这包括方法的参数、局部变量等。

  2. 本地方法栈中的变量引用的对象
    与虚拟机栈类似,但是用于支持本地方法(Native Method)的栈。

  3. 静态变量引用的对象(Static Variables):

    静态变量属于类而不是实例,因此在整个程序运行期间都存在。静态变量可以作为根对象,因为它们在程序的整个生命周期中都能被访问到。

  4. 常量引用的对象(Constant References):

    对于一些被认为是常量的引用,它们在整个程序运行期间都是可达的。这可能包括一些预定义的常量、静态 final 字段等。

  5. 活动的线程对象(Thread):

    线程对象本身可以被视为根对象。每个线程通常都有一个栈,栈中的内容(如本地变量)可以作为可达性分析的根。

  6. 被同步锁持有的对象(Locked Objects):

被Java线程持有的同步锁的对象,因为这些对象可能在其他线程中被访问。

  1. Java核心类库对象:

    如java.lang.Class对象。

这些根对象通常是垃圾回收器开始遍历引用链的起点。通过从这些根对象开始,垃圾回收器可以识别并标记所有通过引用链可达的对象,并最终确定哪些对象是不可达的,从而进行垃圾回收。

可达性分析算法的优点之一是它可以处理循环引用,因为只要一组对象是可达的,它们就不会被回收。这种算法通常与分代垃圾回收结合使用,以更有效地管理不同生命周期的对象。

Java虚拟机中的垃圾回收器(如G1收集器)就使用了可达性分析算法。这种算法的一个关键优势是,在进行垃圾回收的同时,程序的其他部分仍然可以继续执行,从而减小了垃圾回收对程序性能的影响。

1.3 四种引用

  1. 强引用

    强引用(Strong Reference)是Java中最普遍的引用类型。当一个对象具有强引用时,垃圾回收器不会回收这个对象,即使系统中存在内存不足的情况。只有当没有任何强引用指向一个对象时,该对象才会被垃圾回收器回收。

    在Java中,一般的对象引用,如赋值操作 Object obj = new Object(); 就是强引用。例如:

    Object obj = new Object();  // 强引用
    

    在这个例子中,obj 引用了一个新创建的 Object 对象。只要 obj 不被重新赋值为其他值,这个 Object 对象就会一直存在,不会被垃圾回收。

    强引用确保了对象的存在不会受到垃圾回收的影响,但也意味着程序员需要自己管理对象的生命周期,及时释放不再需要的引用,以便让垃圾回收器能够回收不再被引用的对象。

    Object obj = new Object();  // 强引用
    // 在某个时刻不再需要obj引用的对象
    obj = null;  // 将obj设置为null,释放对Object对象的强引用
    

    上述操作后,如果没有其他引用指向这个 Object 对象,它就变得不可达,最终可能被垃圾回收器回收。强引用的使用非常普遍,但需要谨慎管理,以避免内存泄漏或者持续占用过多内存的问题。

  2. 软引用

    软引用(Soft Reference)是Java中一种相对强引用更具弹性的引用类型。当一个对象只被软引用引用时,在内存不足时,这个对象可能被垃圾回收器回收,但它的回收是在系统判断内存不足的情况下进行的,因此相对于强引用来说,软引用更容易被回收。

    在Java中,可以使用java.lang.ref.SoftReference类来创建软引用。例如:

    import java.lang.ref.SoftReference;
    
    public class SoftReferenceExample {
        public static void main(String[] args) {
            // 创建一个字符串对象并使用软引用引用它
            String data = new String("Soft Reference Example");
            SoftReference<String> softReference = new SoftReference<>(data);
    
            // 现在,data可以被释放了,因为只有软引用引用它
            data = null;
    
            // 从软引用中获取对象
            String retrievedData = softReference.get();
            System.out.println(retrievedData);
    
            // 在这里模拟内存不足的情况
            simulateMemoryFull();
    
            // 再次尝试获取对象,如果内存不足,软引用可能被垃圾回收器回收
            retrievedData = softReference.get();
            System.out.println(retrievedData);
        }
    
        private static void simulateMemoryFull() {
            // 在这个方法中,模拟内存不足的情况,触发垃圾回收
            // 实际场景中,内存不足的情况可能由系统触发
            System.gc();
        }
    }
    
    

    在这个例子中,字符串对象被创建后,通过软引用 softReference 引用。然后,将 data 设置为 null,这意味着只有软引用引用这个字符串对象。在模拟内存不足的情况时,垃圾回收器可能会回收这个字符串对象。

    需要注意的是,软引用并不是一种确保对象被回收的机制,而是一种提供更灵活的回收策略的手段。软引用通常用于缓存等场景,允许在内存不足时释放一些缓存数据,而不会导致程序崩溃。

  3. 弱引用

    弱引用(Weak Reference)是Java中一种比软引用更弱的引用类型。与软引用类似,弱引用在垃圾回收时对对象的保护程度更低。当一个对象只被弱引用引用时,它在下一次垃圾回收时就有可能被回收,无论当前内存是否足够。

    在Java中,可以使用 java.lang.ref.WeakReference 类来创建弱引用。以下是一个简单的示例:

    import java.lang.ref.WeakReference;
    
    public class WeakReferenceExample {
        public static void main(String[] args) {
            // 创建一个字符串对象并使用弱引用引用它
            String data = new String("Weak Reference Example");
            WeakReference<String> weakReference = new WeakReference<>(data);
    
            // 现在,data可以被释放了,因为只有弱引用引用它
            data = null;
    
            // 从弱引用中获取对象
            String retrievedData = weakReference.get();
            System.out.println(retrievedData);
    
            // 在这里模拟内存不足的情况
            simulateMemoryFull();
    
            // 再次尝试获取对象,由于只有弱引用引用,对象可能被垃圾回收
            retrievedData = weakReference.get();
            System.out.println(retrievedData);
        }
    
        private static void simulateMemoryFull() {
            // 在这个方法中,模拟内存不足的情况,触发垃圾回收
            // 实际场景中,内存不足的情况可能由系统触发
            System.gc();
        }
    }
    
    

    在这个例子中,字符串对象被创建后,通过弱引用 weakReference 引用。然后,将 data 设置为 null,这意味着只有弱引用引用这个字符串对象。在模拟内存不足的情况时,垃圾回收器可能会回收这个字符串对象。

    弱引用通常用于一些临时性的缓存,当被引用对象不再被其他强引用引用时,垃圾回收器可以更自由地回收它们。弱引用的典型应用场景包括实现一些缓存策略,其中缓存项可以在内存紧张时被更容易地回收。

  4. 虚引用

    虚引用(Phantom Reference)是Java中最弱的一种引用类型,与弱引用和软引用不同,虚引用的存在几乎没有直接的影响。虚引用主要用于跟踪对象被垃圾回收的状态。

    在Java中,可以使用 java.lang.ref.PhantomReference 类来创建虚引用。虚引用并不能通过 get() 方法获取被引用的对象,而是通过配合引用队列(ReferenceQueue)来使用。当虚引用引用的对象被垃圾回收时,虚引用会被加入到引用队列中。

    以下是一个简单的虚引用示例:

    import java.lang.ref.PhantomReference;
    import java.lang.ref.ReferenceQueue;
    
    public class PhantomReferenceExample {
        public static void main(String[] args) {
            // 创建一个字符串对象并使用虚引用引用它
            String data = new String("Phantom Reference Example");
            ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
            PhantomReference<String> phantomReference = new PhantomReference<>(data, referenceQueue);
    
            // 现在,data可以被释放了,因为只有虚引用引用它
            data = null;
    
            // 在这里检查引用队列,看是否有虚引用进入
            // 虚引用入队后,表示对象即将被回收
            ReferenceQueue<String> queue = new ReferenceQueue<>();
            PhantomReference<String> phantomRef = new PhantomReference<>(data, queue);
    
            // 模拟垃圾回收的动作
            System.gc();
    
            // 检查引用队列,看是否有虚引用进入
            // 虚引用入队后,表示对象即将被回收
            java.lang.ref.Reference<? extends String> polledReference = referenceQueue.poll();
            if (polledReference != null) {
                System.out.println("PhantomReference is enqueued.");
            } else {
                System.out.println("PhantomReference is not enqueued.");
            }
        }
    }
    

    在这个例子中,字符串对象被创建后,通过虚引用 phantomReference 引用。然后,将 data 设置为 null,这意味着只有虚引用引用这个字符串对象。在模拟垃圾回收的动作时,我们检查引用队列,如果虚引用已经进入队列,表示相关对象即将被回收。

    虚引用通常用于一些特殊的清理操作,例如在对象被回收时执行一些资源释放或日志记录等。由于虚引用的特性,它并不能阻止被引用对象的回收。

  5. 终结器引用

    在Java中,终结器引用(Finalizer Reference)是一种与对象终结器(Finalizer)相关的引用。对象终结器是一个用于在对象被垃圾回收前执行清理操作的特殊方法,它由Object类中的finalize方法表示。

    终结器引用通常与对象的终结器方法相关联,通过 java.lang.ref.Finalizer 类进行管理。然而,需要注意的是,终结器机制在现代Java中被认为是不推荐使用的,因为它的执行时机不确定性,可能导致一系列问题,如内存泄漏和性能问题。

    以下是一个简单的示例,演示了终结器引用的使用:

    import java.lang.ref.Finalizer;
    
    class MyObject {
        @Override
        protected void finalize() throws Throwable {
            System.out.println("Finalizing MyObject");
        }
    }
    
    public class FinalizerReferenceExample {
        public static void main(String[] args) {
            MyObject obj = new MyObject();
    
            // 创建终结器引用
            Finalizer<MyObject> finalizerReference = new Finalizer<>(obj, null);
    
            // 将对象置为null,使对象变为可终结
            obj = null;
    
            // 请求垃圾回收
            System.gc();
    
            // 在垃圾回收后,finalize方法可能被调用
            // 但不推荐依赖finalize方法执行清理操作
        }
    }
    
    

    在这个例子中,MyObject类重写了finalize方法,然后通过 Finalizer 类创建了一个终结器引用。当垃圾回收器请求垃圾回收时,finalize方法可能被调用。

    然而,强烈建议不要过度依赖终结器机制。替代终结器的方法包括使用try-with-resources结构、AutoCloseable接口以及其他清理模式。这些方法能够提供更可靠、可预测和有效的资源管理。

2. 垃圾回收算法

2.1 标记清除

标记清除算法(Mark and Sweep Algorithm)是一种基本的垃圾回收算法,用于找出不再被程序引用的对象并释放它们所占用的内存。该算法主要分为两个阶段:标记阶段和清除阶段。

以下是标记清除算法的基本步骤:

  1. 标记阶段(Marking Phase):

从根对象(GC Roots)开始,通过遍历对象引用链,标记所有被引用的对象。这个过程确保所有可达的对象都被标记为活动对象。

  1. 清除阶段(Sweeping Phase):

在清除阶段,垃圾回收器遍历整个堆,清除未被标记的对象。即,垃圾回收器释放那些没有在标记阶段被标记为活动对象的内存。清除的对象会被加入到可用的内存池,等待下次分配。

标记清除算法的优点在于它能够处理循环引用,因为它通过标记活动对象的方式,确保只有活动对象能够被保留。然而,这种算法也有一些缺点,例如:

  • 碎片化问题: 由于清除阶段释放的内存是不连续的,可能导致堆中出现碎片化,从而降低了内存的使用效率。

  • 效率问题: 清除阶段需要遍历整个堆,这在堆较大时可能会导致较长的停顿时间。

由于这些缺点,现代垃圾回收算法往往采用其他更高效的算法,如复制算法、标记-整理算法等。标记清除算法在教育和理论研究中仍然具有重要的地位,但在实际应用中,往往会选择更先进的垃圾回收算法。

2.2 标记整理

标记整理算法(Mark and Compact Algorithm)是一种垃圾回收算法,它结合了标记阶段、整理阶段和清除阶段,旨在减少内存碎片化。这个算法主要用于堆的管理,确保存活的对象在内存中是紧凑排列的,而非出现碎片。

以下是标记整理算法的基本步骤:

  1. 标记阶段(Marking Phase):

从根对象开始,通过遍历对象引用链,标记所有被引用的对象,将它们标记为活动对象。

  1. 整理阶段(Compacting Phase):

在整理阶段,垃圾回收器将所有活动对象向一端(通常是堆的起始端)移动,以便在移动过程中将空闲空间集中到堆的另一端。这一步骤类似于复制算法,但标记整理算法并不创建一个新的空间,而是在原有的堆中进行整理。

  1. 清除阶段(Sweeping Phase):

在清除阶段,垃圾回收器遍历整个堆,清除未被标记的对象。即,垃圾回收器释放那些没有在标记阶段被标记为活动对象的内存。

标记整理算法的优势在于它避免了标记清除算法的碎片化问题。通过整理阶段,存活的对象被移动到一起,而空闲的内存空间则被集中在一起,使得内存分配更为高效。但与此同时,标记整理算法仍然可能存在一定的停顿时间,因为整理阶段可能需要移动大量对象。

虽然标记整理算法在解决内存碎片问题上表现良好,但在某些场景下,如实时性要求较高的应用中,仍然可能会选择其他垃圾回收算法,如分代垃圾回收算法。

2.3 复制算法

复制算法(Copying Algorithm)是一种垃圾回收算法,主要用于解决内存碎片化的问题。该算法将堆分为两个区域:一个是活动对象存放的From空间,另一个是空闲空间的To空间。在每次垃圾回收时,将所有存活的对象从From空间复制到To空间,同时将From空间清空,然后交换From和To的角色,使得下一次垃圾回收时复制存活对象到新的To空间。

以下是复制算法的基本步骤:

  1. 标记阶段(Marking Phase):

从根对象开始,通过遍历对象引用链,标记所有被引用的对象,将它们标记为活动对象。

  1. 复制阶段(Copying Phase):

将所有活动对象从From空间复制到To空间。由于复制过程中保持了对象的相对顺序,因此无需移动引用。

  1. 更新引用(Update References):

更新所有指向被复制的对象的引用,使其指向新的To空间中的地址。

  1. 角色交换(Swap Roles):

交换From和To的角色,使To空间成为下一次垃圾回收的From空间。
复制算法的优点在于它解决了内存碎片的问题,因为新的To空间是一块干净的连续内存。然而,这种算法的缺点是它浪费了一半的内存空间,因为每次垃圾回收都需要有一块足够大的To空间来容纳所有活动对象。

复制算法通常用于新生代的垃圾回收,而在分代垃圾回收中,新生代采用复制算法,老年代则采用其他算法,如标记清除或标记整理算法。这样可以更好地平衡内存利用和垃圾回收效率。

3. 分代垃圾回收

分代垃圾回收是一种垃圾回收策略,根据对象的存活周期将堆内存划分为不同的代(Generation),并采用不同的垃圾回收算法和频率来处理每个代。这种策略基于两个观察:

  1. 弱存活假说(Weak Generational Hypothesis):

大部分对象在内存中存在的时间很短,而只有一小部分对象存活得较长。因此,可以将对象划分为新生代和老年代,分别采用不同的垃圾回收算法。

  1. 新生代的对象更容易死亡(Most objects die young):

大多数对象在被分配后很快就变得不可达,因此新生代中的对象更容易死亡。
分代垃圾回收一般将堆划分为三代:

  1. 新生代(Young Generation):

这是对象刚被分配的地方。新生代中的对象大多数是短命的。典型的垃圾回收算法是复制算法,因为复制算法适用于高回收率的场景。

  1. 老年代(Old Generation):

这是新生代中存活时间较长的对象被晋升到的地方。老年代中的对象更有可能长时间存活,因此采用标记清除或标记整理算法。

  1. 永久代(Permanent Generation):

用于存放静态不变的类信息、方法信息等。在JDK 8及以后的版本中,永久代被元空间(Metaspace)取代。
分代垃圾回收的优势在于它针对不同代采用不同的垃圾回收策略,根据对象的生命周期进行优化。这可以降低整体垃圾回收的成本,提高垃圾回收的效率。在实践中,分代垃圾回收策略被广泛应用于Java虚拟机和其他语言的运行时系统中。

4. 垃圾回收器

垃圾回收器是一种用于自动管理程序运行时内存的机制,它负责检测和回收不再被程序使用的内存对象,以便释放资源并防止内存泄漏。在Java和其他高级编程语言中,垃圾回收器是运行时系统的一部分。

以下是一些常见的垃圾回收器:

  1. 串行垃圾回收器(Serial Garbage Collector):

串行垃圾回收器是最基本的垃圾回收器,它使用单线程进行垃圾回收操作。适用于小型或简单的应用程序,但在大型应用中可能导致停顿时间较长。

  1. 并行垃圾回收器(Parallel Garbage Collector):

并行垃圾回收器使用多个线程进行垃圾回收操作,提高了垃圾回收的效率。适用于多核处理器的系统,可减少垃圾回收造成的停顿时间。

  1. 并发标记清除垃圾回收器(Concurrent Mark-Sweep Garbage Collector,CMS):

CMS垃圾回收器使用多线程进行标记和清除操作,以减少停顿时间。它适用于对停顿时间敏感的应用程序,但可能会牺牲一些吞吐量。

  1. G1垃圾回收器(Garbage First Garbage Collector):

G1垃圾回收器是一种面向大堆内存的垃圾回收器,旨在提供低停顿时间和高吞吐量。它将堆划分为多个区域,通过优先处理垃圾量较小的区域来降低停顿时间。

  1. Z垃圾回收器(Z Garbage Collector):

Z垃圾回收器是JEP 333引入的一种低停顿时间的垃圾回收器。它使用了一种称为Colored Pointers的技术,通过并发标记和整理的方式,减少了垃圾回收引起的停顿时间。

  1. Shenandoah垃圾回收器:

Shenandoah是一种低停顿时间的垃圾回收器,旨在减小大堆内存的垃圾回收停顿时间。它使用了一种被称为"Concurrent Compacting"的技术,通过并发标记和压缩来实现低停顿时间。
这些垃圾回收器具有不同的特性,选择哪个垃圾回收器取决于应用程序的性能需求、硬件配置以及对停顿时间的敏感性。在某些情况下,可以通过Java虚拟机的参数来选择或配置垃圾回收器。

5. 垃圾回收调优

JVM(Java Virtual Machine)的垃圾回收调优是优化Java应用程序性能和减少垃圾回收停顿时间的关键部分。以下是一些常见的JVM垃圾回收调优技巧:

  1. 选择合适的垃圾回收器:

    根据应用程序的性能需求和硬件配置,选择适合的垃圾回收器。例如,对于对停顿时间敏感的应用程序,可以考虑使用CMS(Concurrent Mark-Sweep)或G1(Garbage First)垃圾回收器。如果系统具有多核处理器,可以考虑使用并行垃圾回收器。

  2. 调整堆大小:

    根据应用程序的内存使用情况,调整堆的大小。合理的堆大小可以降低垃圾回收的频率,减小停顿时间。可以使用-Xms和-Xmx参数分别设置堆的初始大小和最大大小。

java -Xms256m -Xmx512m -jar YourApplication.jar
  1. 选择合适的垃圾回收策略:

    根据应用程序的内存使用模式,选择合适的垃圾回收策略。例如,对于短时间存活的对象较多的应用,考虑使用串行垃圾回收器或Parallel垃圾回收器。对于大堆内存,可以尝试使用G1垃圾回收器。

  2. 设置垃圾回收器相关的参数:

    针对选择的垃圾回收器,可以调整相关的参数。例如,对于CMS垃圾回收器,可以使用-XX:MaxGCPauseMillis参数设置最大停顿时间。

java -XX:+UseConcMarkSweepGC -XX:MaxGCPauseMillis=500 -jar YourApplication.jar
  1. 监控和分析垃圾回收日志:

    启用垃圾回收日志并进行监控分析是调优的重要手段。可以使用-XX:+PrintGCDetails和-Xloggc:gc.log参数来生成详细的垃圾回收日志。

    java -XX:+PrintGCDetails -Xloggc:gc.log -jar YourApplication.jar
    

    使用工具如jvisualvm、jConsole或专业的监控工具,对垃圾回收的情况进行实时监控和分析,以便及时发现潜在问题。

  2. 避免过度内存分配:

    减少不必要的对象创建和过度的内存分配有助于降低垃圾回收的压力。避免创建大量临时对象,尽可能使用对象池或重用对象的方式。

  3. 分析应用程序的内存使用情况:

    使用内存分析工具,如Eclipse Memory Analyzer(MAT)或VisualVM,深入了解应用程序的内存使用情况,找出可能的内存泄漏或不必要的内存占用。

这些调优技巧需要结合具体应用程序的特点进行调整,因为不同的应用有不同的内存使用模式和性能需求。定期进行性能测试和监控是调优的关键。

你可能感兴趣的:(jvm,java,算法)