Java垃圾回收机制

发布时间:2023-05-09 09:45:30

  1. 垃圾回收的意义  在C++中,对象占用的内存在程序结束前被占用,在明确释放之前不能分配给其他对象;在Java中,当没有对象引用指向原始分配给某个对象的内存时,内存就会变成垃圾。JVM的系统级线程会自动释放内存块。垃圾回收意味着程序不再需要的对象是“无用信息”,这些信息将被丢弃。当一个对象不再被引用时,内存回收它所占用的空间,使空间被后来的新对象使用。事实上,垃圾回收除了释放无用的物体外,还可以清除内存记录碎片。由于创建对象和垃圾回收器释放丢弃对象所占用的内存空间,内存会出现碎片。碎片是分配给对象的内存块之间的空闲内存洞。碎片整理将占用的堆内存移动到堆的一端,JVM将整理出的内存分配给新的对象。   垃圾回收可以自动释放内存空间,减轻编程负担。这使得Java 虚拟机有一些优点。首先,它可以提高编程效率。在没有垃圾回收机制的情况下,解决一个难以理解的存储问题可能需要很多时间。用Java语言编程时,垃圾回收机制可以大大缩短时间。二是其保护程序的完整性, 垃圾回收是Java语言安全策略的重要组成部分。   垃圾回收的一个潜在缺点是其成本影响程序性能。Java虚拟机必须跟踪操作程序中有用的对象,并最终释放无用的对象。这个过程需要处理器的时间。其次,垃圾回收算法的不完整性,一些垃圾回收算法不能保证100%收集所有垃圾内存。当然,随着垃圾回收算法的不断提高和软硬件运行效率的不断提高,这些问题都可以得到解决。2. 垃圾收集算法分析   Java语言规范没有明确说明JVM使用哪种垃圾回收算法,但任何垃圾回收算法通常需要做两件基本的事情:(1)找到无用的信息对象;(2)回收无用对象占用的内存空间,使程序可以再次使用该空间。   大多数垃圾回收算法使用根集(root set)这个概念;所谓根集,是指Java程序可访问的引用变量(包括局部变量、参数和类变量)的集合。该程序可以使用引用变量访问对象的属性和调用对象的方法。垃圾回收首先需要确定哪些是可达的,哪些是不可达的。从根集可达的对象是活动对象,不能作为垃圾回收,包括从根集间接可达的对象。根集不能通过任何路径到达的对象符合垃圾收集条件的,应当回收。以下是一些常用的算法。   2.1. 引用计数法(Reference Counting Collector)  引用计数法是唯一一种不使用根集的垃圾回收方法。该算法使用引用计数器来区分生存对象和不再使用的对象。一般来说,堆中的每个对象对应一个引用计数器。引用计数器将每次创建一个对象并给出一个变量时放置为1。当对象被赋予任何变量时,引用计数器每次添加1。当对象出现作用域(对象丢弃且不再使用)时,引用计数器将减少1。一旦引用计数器为0,对象将满足垃圾收集的条件。   基于引用计数器的垃圾收集器运行迅速,不会长时间中断程序执行,必须实时运行。然而,引用计数器增加了程序执行的成本,因为每个对象都给出了新的变量,计数器增加了1,而现有对象每次都有一个作用域,计数器减少了1。   2.2. tracing算法(Tracing Collector)  为了解决引用计数法的问题,提出了tracing算法,它采用了根集的概念。基于tracing算法的垃圾收集器从根集开始扫描,识别哪些对象可以达到,哪些对象不能达到,并以某种方式标记可达对象,例如为每个可达对象设置一个或多个位置。在扫描和识别过程中,基于tracing算法的垃圾收集也被称为标记和清除(mark-and-sweep)垃圾收集器.   2.3. compacting算法(Compacting Collector)  为解决堆碎片问题,基于tracing的垃圾回收吸收了compacting算法的理念,在清除过程中,算法将所有对象移动到堆的一端,堆的另一端变成相邻的空闲内存区,收集器更新其移动对象的所有参考,使这些参考能够在新的位置识别原始对象。在实现基于Compacting算法的收集器时,通常会增加句柄和句柄表。在实现基于Compacting算法的收集器时,通常会增加句柄和句柄表。   2.4. copying算法(Coping Collector)  该算法旨在克服句柄的成本,解决堆放碎片的垃圾回收问题。一开始,它将堆分为一个对象区和多个空闲区。程序将空间从对象区分配给对象。当对象满时,基于复制算法的垃圾回收将从根部集中扫描活动对象,并将每个活动对象复制到空闲区(使活动对象占用的内存之间没有空闲间隔),使空闲区成为对象区,原对象区成为空闲区,程序将在新对象区分配内存。   基于coping算法的典型垃圾回收是stop-and-copy算法将堆叠成对象区和空闲区,在对象区和空闲区之间的切换过程中暂停程序执行。   2.5.generation算法(Generational Collector)  stop-and-复制垃圾收集器的一个缺点是收集器必须复制所有活动对象,这增加了程序等待时间,这就是为什么复制算法效率低下的原因。程序设计中有这样的规律:大多数对象存在时间较短,少数对象存在时间较长。因此,generation算法将堆分为两个或两个以上,每个子堆作为对象的一代 (generation)。垃圾收集器将从最年轻的子堆中收集这些物体,因为大多数物体存在的时间相对较短。分代垃圾收集器运行后,上次运行中幸存下来的对象移动到下一代最高子堆,节省了时间,因为老一代子堆不会经常回收。   2.6. adaptive算法(Adaptive Collector)   在特定情况下,一些垃圾收集算法将优于其他算法。基于Adaptive算法的垃圾收集器是监控当前堆的使用情况,并选择合适算法的垃圾收集器。

  3. System.gc()方法

  命令行参数透视垃圾收集器的运行   使用System.gc()Java垃圾回收可以要求Java垃圾回收,无论JVM使用哪种垃圾回收算法。在命令行中有一个参数——verbosegc可以查看Java使用的堆内存,其格式如下:   java -verbosegc classfile   可见一个例子:

  [java] view plain copy1. class TestGC 2. { 3. public static void main(String[] args) 4. { 5. new TestGC(); 6.    System.gc(); 7.    System.runFinalization(); 8.    } 9. }

  在这个例子中,创建了一个新的对象,因为它没有被使用,所以对象很快就变得无法实现。程序编译后,执行命令: java -verbosegc TestGC 后结果为:   [Full GC 168K->97K(1984K), 0.0253873 secs]   机器的环境是,Windows 2000 + JDK1.3.1.箭头前后的数据168K和97K分别表示垃圾收集GC前后所有生存对象使用的内存容量,表示168K-97K=71K的对象容量被回收,括号中的数据1984K是堆内存的总容量,收集时间为0.0253873秒(每次执行时都会有所不同)。

  需要注意的是,调用System.gc()也只是一个请求(建议)。JVM接到这个消息后,并没有立即做垃圾回收,而是加权了几种垃圾回收算法,使垃圾回收操作容易发生,或者提前发生,或者回收更多。

  4. finalize()方法

  在JVM垃圾回收器收集一个对象之前,程序通常需要调用适当的方法来释放资源,但Java提供了一种缺乏机制来终止对象的心释放资源,而没有明确的资源释放。这种方法是finalize()。其原型如下:   protected void finalize() throws Throwable   finalize()方法返回后,对象消失,垃圾收集开始实施。throws原型中的throws Throwable表示,它可以抛出任何类型的异常。   使用finalize()的原因是垃圾回收器无法处理的特殊情况。假设你的对象(不使用new方法)获得了一个“特殊”的内存区域,因为垃圾回收器只知道new显示的内存空间,所以它不知道如何释放这个“特殊”的内存区域,所以java允许在类中定义finalize()方法。

  特殊区域,如:1)在分配内存时可能会使用类似的区域 C语言的做法,而不是JAVA通常的new做法。这种情况主要发生在nativee 在method中,如nativee method调用C///C++方法malloc()函数系列分配存储空间,但除非调用free()函数,否则这些内存空间不会释放,此时可能会导致内存泄漏。但是因为free()的方法是C//C++本地方法可以在finalize()中调用函数。释放这些“特殊”的内存空间。2)或打开的文件资源不属于垃圾回收器的回收范围。 换言之,finalize()主要用途是释放其他做法开辟的一些内存空间,做一些清洁工作。因为JAVA中没有提到足够的函数,比如“分析”函数或者类似概念,所以在做一些类似的清理工作的时候,一定要自己创造一个普通的清理方法,那就是超级。 finalize()在Object类中的方法。例如,假设一个对象在创建过程中会在屏幕上画出自己,如果不清楚地从屏幕上擦出,它可能永远不会被清理干净。当GC工作时,如果在finalize()中添加某种擦除功能,finalize()调用后,图像将被擦除。如果GC没有发生,这个图像就会发生

  一直保存下来。

  只有在下次进行垃圾回收时,才能真正释放对象占用的内存空间。  在普通的清除工作中,为了清除一个对象,该对象的用户必须在想要清除的地方调用一种清除方法。这与C++“析构函数”的概念略有冲突。所有对象在C++中都会被破坏(清除)。换句话说,所有的对象都应该被“破坏”。如果将C++对象创建为本地对象,如在堆栈中创建(Java中不可能,Java在堆栈中),则以“结束花括号”为代表的清除或破坏工作将在创建对象的角色域的末尾进行。如果对象是由new创建的(类似Java),当程序员调用C++时 当delete命令(Java没有此命令)时,会调用相应的分析函数。如果程序员忘记了,分析函数永远不会被调用,我们最终会得到一个内存“漏洞”,包括对象的其他部分永远不会被清除。   相反,Java不允许我们创建本地(局部)对象——无论如何都要使用new。但在Java中,没有“”delete“命令释放对象,因为垃圾回收器可以帮助我们自动释放存储空间。因此,如果我们站在一个相对简单的位置,我们可以说Java没有分析函数,因为有垃圾回收机制。然而,随着未来学习的深入,我们将知道垃圾收集器的存在并不能完全消除对分析函数的需求,也不能消除分析函数所代表的机制的需求(原因见下一段。此外,finalize()函数是在垃圾回收器中准备释放的对象当占用的存储空间被调用时,绝对不能直接调用finalize(),因此应尽量避免使用)。如果要实施除释放存储空间以外的其他形式的清除工作,Java中的一种方法仍然需要调用。它等同于C++的析构函数,但没有后者方便。 使用delete()在C++中的所有对象肯定会被销毁,而JAVA中的对象并不总是被垃圾回收器回收。In another word, 1 对象可能不会被垃圾回收,2 垃圾回收不等于“分析” 垃圾回收只与内存有关。也就是说,如果一个对象不再被使用,是否需要在finalize()中释放其他对象?不是的。垃圾回收器负责释放物体占有的内存,因为无论物体是如何创建的。

  5. 触发主GC(Garbage Collector)的条件

  JVM频率很高,但由于GC占用时间很短,对系统影响不大。更值得注意的是主GC的触发条件,因为它对系统有明显的影响。一般来说,有两个条件会触发主GC:

  由于GC是在优先级最低的线程中进行的,除下列条件外,GC线程在应用繁忙时不会被调用。

  当Java堆内存不足时,GC将被调用。当应用程序线程运行并在运行过程中创建新对象时,如果此时内存空间不足,JVM将强制调用GC线程回收内存进行新的分配。如果GC一次后仍然不能满足内存分配的要求,JVM将进一步尝试两次GC。如果仍然不能满足要求, JVM将报“JVM将报”out of memoryJava应用程序将停止错误。

  由于主GC是否由JVM根据系统环境决定,系统环境不断变化,主GC的运行不确定,不可能预测何时不可避免,但可以肯定的是,对于长期运行的应用,主GC是重复的。

  6. 减少GC费用的措施

  根据上述GC机制,程序的运行将直接影响系统环境的变化,从而影响GC的触发。如果不设计和编码GC的特点,就会有一系列的负面影响,如内存停留。为了避免这些影响,基本原则是尽可能减少垃圾和GC过程中的成本。具体措施包括以下几个方面:

  (1)不要显式调用System.gc()

  这个函数建议JVM进行主GC。虽然只是建议而不是一定,但在很多情况下,它会触发主GC,从而增加主GC的频率,即间歇性停顿的次数。

  (2)尽量减少临时对象的使用

  跳出函数调用后,临时对象将成为垃圾。少使用临时变量相当于减少垃圾的产生,从而延长上述第二个触发条件的时间,减少主GC的机会。

  (3)当对象不使用时,最好将其显示为Nulll

  一般来说,Null的对象将被用作垃圾处理,因此将未使用的对象设置为Null,有利于GC收集器判断垃圾,从而提高GC的效率。

  (4)尽量使用StringBuffer而不是String来累加字符串

  由于String是一个固定的长字符串对象,当累积String对象时,它不会在String对象中扩展,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,因为新的String对象必须在二次“+”操作中创建,但这些过渡对象对系统没有实际意义,只会增加更多的垃圾。为了避免这种情况,可以用stringbuffer来累加字符串,因为stringbufffer可以变长,它在原有的基础上扩展,不会产生中间对象。

  (5)Int等基本类型可以使用,Long,不需要Integer,Long对象

  基本类型变量占用的内存资源远少于相应对象。如果没有必要,最好使用基本变量。

  (6)尽量少用静态对象变量

  静态变量属于全球变量,不会被GC回收,它们总是占用内存。

  (7)创建或删除分散对象的时间

  专注于在短时间内创建大量的新对象,特别是大对象,会导致突然需要大量的内存。面对这种情况,JVM只能回收或整合主GC的内存碎片,从而增加主GC的频率。同样的原因是集中删除对象。它突然出现了大量的垃圾对象,不可避免地减少了空闲空间,这大大增加了下次创建新对象时强迫主GC的机会。

  下面的例子展示了垃圾收集的过程,并总结了之前的陈述。

  [java] view plain copy 1. class Chair { 2. static boolean gcrun = false; 3. static boolean f = false; 4. static int created = 0; 5. static int finalized = 0; 6. int i; 7.   Chair() { 8.    i = ++created; 9. if(created == 47) 10. "Created 47"); 11.   } 12. protected void finalize() { 13. if(!gcrun) { 14. true; 15. "Beginning to finalize after " + created + " Chairs have been created"); 16.    } 17. if(i == 47) { 18. "Finalizing Chair #47, " +"Setting flag to stop Chair creation"); 19. true; 20.    } 21.    finalized++; 22. if(finalized >= created) 23. "All " + finalized + " finalized"); 24.   } 25. } 26. 27. public class Garbage { 28. public static void main(String[] args) { 29. if(args.length == 0) { 30. "Usage: /n" + "java Garbage before/n or:/n" + "java Garbage after"); 31. return; 32.   } 33. while(!Chair.f) { 34. new Chair(); 35. new String("To take up space"); 36.   } 37. "After all Chairs have been created:/n" + "total created = " + Chair.created + 38. ", total finalized = " + Chair.finalized); 39. if(args[0].equals("before")) { 40. "gc():"); 41.     System.gc(); 42. "runFinalization():"); 43.     System.runFinalization(); 44.   } 45. "bye!"); 46. if(args[0].equals("after")) 47. true); 48.   } 49. }

  上述程序创建了许多Chair对象,在垃圾收集器开始运行后的某些时候,程序将停止创建Chair。由于垃圾收集器可能在任何时候运行,我们无法准确地知道它何时启动。因此,该程序用一个名为gcrun的标记来指出垃圾收集器是否已经开始运行。使用第二个标记f,chair可以告诉main()它应该停止对象的生成。这两个标记都设置在finalize()内,在垃圾收集过程中调用。另外两个static变量-created, finalized--用于跟踪已创建的物体数量和垃圾收集器已完成的物体数量。最后,每个Chair都有自己的(非) static)int i,因此,我们可以跟踪了解它的具体编号。编号为47的Chair完成工作后,标记将设置为true,最终结束Chair对象的创建过程。7. 垃圾回收的几点补充   通过以上说明,可以发现垃圾回收具有以下特点:   (1)垃圾收集的不可预测性:由于实现了不同的垃圾回收算法,采用了不同的收集机制,可能定期发生,当系统空闲CPU资源出现时,也可能与原始垃圾收集相同,直到内存消耗极限,这与垃圾收集器的选择和具体设置有关。   (2)垃圾收集的准确性:主要包括2 个方面:(a)垃圾收集器能准确标记活物;(b)垃圾收集器可以准确定位对象之间的引用关系。   (2)垃圾收集的准确性:主要包括2 个方面:(a)垃圾收集器能准确标记活物;(b)垃圾收集器可以准确定位对象之间的引用关系。前者是完全回收所有废物的前提,否则可能会导致内存泄漏。后者是实现归并和复制算法的必要条件。所有未达到的对象都可以可靠地回收,所有的对象都可以重新分配,允许对象的复制和对象内存的缩并,从而有效地防止内存的分离和破碎。   (3)现在有许多不同的垃圾收集器,每个都有自己的算法和不同的性能,包括当垃圾收集开始时停止应用程序的运行,当垃圾收集开始时允许应用程序的线程运行,以及垃圾收集的多线程同时运行。   (4)实现垃圾收集和具体JVM 而且JVM的内存模型关系非常密切。不同的JVM 不同的垃圾可以收集,而JVM可以收集 内存模型决定了JVM可以收集什么类型的垃圾。现在,HotSpot JVM系列中的内存系统采用先进的面向对象框架设计,使JVM系列能够采用最先进的垃圾收集。   (5)随着技术的发展,现代垃圾收集技术提供了许多可选的垃圾收集器,在配置每个收集器时可以设置不同的参数,这使得根据不同的应用环境获得最佳的应用性能成为可能。   针对上述特点,在使用时应注意:   (1)不要试图假设垃圾收集发生的时间,这一切都是未知的。例如,该方法中的一个临时对象在该方法调用后变为无用对象,此时其内存可以释放。   (2)Java提供了一些处理垃圾收集的类别,并提供了一种强制执行垃圾收集的方法——调用System.gc(),但这也是一种不确定的方法。Java 不能保证每次调用这种方法都能启动垃圾收集。它只会向JVM发出这样的申请。垃圾收集是否真的实施还不得而知。   (3)选择适合自己的垃圾收集器。一般来说,如果系统没有特殊和苛刻的性能要求,JVM的缺失选项可以使用。否则,可以考虑使用有针对性的垃圾收集器,如增量收集器,更适合实时要求较高的系统。该系统配置高,闲置资源多,可考虑并行标记/清除收集器。   (4)关键也难以把握的问题是内存泄漏。良好的编程习惯和严谨的编程态度总是最重要的。不要让你的小错误导致内存漏洞。   (5)尽快释放无用对象的引用。当大多数程序员使用临时变量时,他们会让引用变量退出活动域(scope)之后自动设置为null,暗示垃圾收集器要收集对象,还要注意引用的对象是否被监控。如果是这样的话,去掉监听器,然后给空值。

上一篇 android上的JAVA8:使用retrolambda
下一篇 Java 内存分配全面浅析

文章素材均来源于网络,如有侵权,请联系管理员删除。

标签: Java教程Java基础Java编程技巧面试题Java面试题