java的内存回收--读《疯狂java》 笔记
发布日期:2021-06-28 23:30:05 浏览次数:2 分类:技术文章

本文共 18388 字,大约阅读时间需要 61 分钟。

内存回收

程序员需要通过关键字new创建Java对象,即可视作为Java对象申请内存空间,JVM会在堆内存中为每个对象分配空间;当一个Java对象失去引用时,JVM的垃圾回收机制会自动清除它们,并回收它们所占用的内存空间。
对于JVM的垃圾回收机制来说,是否回收一个对象的标准在于:是否还有引用变量引用该对象?只要有引用变量引用该对象,垃圾回收机制就不会回收它。

也就是说,当Java对象被创建出来之后,垃圾回收机制会实时地监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等。当垃圾回收机制实时地监控到某个对象不再被引用变量所引用时,立即回收机制就会回收它所占用的空间。

基本上,可以把JVM内存中对象引用理解成一种有向图,把引用变量、对象都当成为有向图的顶点,将引用关系当成图的有向边,有向边总是从引用端指向被引用的Java对象。因为Java所有对象都是由一条一条线程创建出来的,因此可把线程对象当成有向图的起始顶点。

对于单线程程序而言,整个程序只有一条main线程,那么该图就是以main进程为顶点的有向图。在这个有向图中,main顶点可达的对象都处于可达状态,垃圾回收机制不会回收它们;如果某个对象在这个有向图中处于不可达状态,那么就认为这个对象不再被引用,接下来垃圾回收机制就会主动回收它了。

当一个对象在堆内存中运行时,根据它在对应有向图中的状态,可以把它所处的状态分成如下3种。

  • 可达状态:当一个对象被创建后,有一个以上的引用变量引用它。在有向图中可从起始顶点导航到该对象,那它就处于可达状态,程序可通过引用变量来调用该对象的属性和方法。
  • 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它将先进入可恢复状态,此时从有向图的起始顶点不能导航到该对象。在这个状态下,系统的垃圾回收机制准备回收该对象所占用的内存。在回收该对象之前,系统会调用可恢复状态的对象的finalize方法进行资源清理,如果系统在调用finalize方法重新让一个以上引用变量引用该对象,则这个对象会再次变为可达状态;否则,该对象将进入不可达状态。
  • 不可达状态:当对象的所有关联都被切断,且系统调用所有对象的finalize方法依然没有使该对象变成可达状态,那这个对象将永久性地失去引用,最后变成不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源
  • 在这里插入图片描述

引用

对垃圾回收机制来说,判断一个对象是否可回收的标准就在于该对象是否被引用,因此引用也是JVM进行内存管理的一个重要概念。为了更好地管理对象的引用,从JDK 1.2开始,Java在java.lang.ref包下提供了3个类:SoftReference 、PhantomReference和WeakReference。它们分别代表了系统对对象的3种引用方式:软引用、虚引用和弱引用。归纳起来,Java语言对对象的引用有如下4种。

  • 强引用
  • 软引用
  • 弱引用
  • 虚引用

强引用

这是Java程序中最常见的引用方式,程序创建一个对象,并把这个对象赋给一个引用变量,这个引用变量就是强引用。

Java程序可通过强引用来访问实际的对象,前面介绍的程序中的所有引用变量都是强引用的方式。当一个对象被一个或一个以上的强引用变量所引用时,它处于可达状态,它不可能被系统垃圾回收机制回收。

强引用是Java编程中广泛使用的引用类型,被强引用所引用的Java对象绝不会被垃圾回收机制回收,即使系统内存非常紧张;即使有些Java对象以后永远都不会被用到,JVM也不会回收被强引用所引用的Java对象。

由于JVM肯定不会回收强引用所引用的Java对象,因此强引用是造成Java内存泄漏的主要原因之一。

软引用

软引用需要通过SoftReference类来实现,当一个对象只具有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象;当系统内存空间不足时,系统将会回收它。

软引用通常用于对内存敏感的程序中,软引用是强引用很好的替代。 对于软引用则不同,当系统内存空间充足时,软引用与强引用没有太大的区别;当系统内存空间不足时,被软引用所引用的Java对象可以被垃圾回收机制回收,从而避免系统内存不足的异常。

当程序需要大量创建某个类的新对象,而且有可能重新访问已创建老对象时可以充分使用软引用来解决内存紧张的难题。

class Person     {
String name; int age; public Person(String name , int age) {
this.name = name; this.age = age; } public String toString() {
return "Person[name = " + name + ", age = " + age + "]"; } } public class SoftReferenceTest {
public static void main(String[] args) {
SoftReference
[] people = new SoftReference[100]; for (int i = 0 ; i < people.length ; i + +) {
people[i] = new SoftReference
(new Person( "名字" + i , (i + 1) * 4 % 100)); } System.out.println(people[2].get()); System.out.println(people[4].get()); //通知系统进行垃圾回收 System.gc(); System.runFinalization(); //垃圾回收机制运行之后,SoftReference数组里的元素保持不变 System.out.println(people[2].get()); System.out.println(people[4].get()); } }

当使用java -Xmx2m -Xms2m SoftReferenceTest命令强制堆内存只有2m,而且程序创建一个长度为100000的数组,这样将使得系统内存紧张。在这种情况下,软引用所引用的Java对象将会被垃圾回收,

弱引用

弱引用与软引用有点相似,区别在于弱引用所引用对象的生存期更短。弱引用通过WeakReference类实现,弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。当然,并不是说当一个对象只有弱引用时,它就会立即被回收——正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收。

public class WeakReferenceTest     {
public static void main(String[] args) throws Exception {
//创建一个字符串对象 String str = new String("疯狂Java讲义"); //不要使用String str = "疯狂Java讲义";系统会缓存这个字符串直接量 //创建一个弱引用,让此弱引用引用到“疯狂Java讲义”字符串 WeakReference
wr = new WeakReference
(str); //① //切断str引用和“疯狂Java讲义”字符串之间的引用 str = null; //② //取出弱引用所引用的对象 System.out.println(wr.get()); //③ //强制垃圾回收 System.gc(); System.runFinalization(); //再次取出弱引用所引用的对象 System.out.println(wr.get()); //④ } }

上面程序创建了一个“疯狂Java讲义”字符串对象,并让str引用变量引用它。执行①行粗体字代码行时,系统创建了一个弱引用对象,并让该对象和str引用同一个对象。当程序执行到②行代码时,切断了str和“疯狂Java讲义”字符串对象之间的引用关系,此时系统内有如图4.6所示。

弱引用
由于垃圾回收的不确定性,当程序希望从弱引用中取出被引用对象时,可能这个被引用对象已经被释放了。如果程序需要使用那个被引用的对象,则必须重新创建该对象。这个过程可以采用两种风格的代码完成,下面代码显示了一种风格。

//取出弱引用所引用的对象     obj = wr.get();     //如果取出的对象为null     if (obj == null)     {
//重新创建一个新的对象,再次使用弱引用引用该对象 wr = new WeakReference(recreateIt()); //① //取出弱引用所引用的对象,将其赋给obj变量 obj = wr.get(); //② } ...//操作obj对象 //再次切断obj和对象之间的关联 obj = null;

下面代码显示了另一种取出被引用对象的代码风格。

//取出弱引用所引用的对象     obj = wr.get();     //如果取出的对象为null     if (obj == null)     {
//重新创建一个新的对象,将其使用强引用来引用它 obj = recreateIt(); //取出弱引用所引用的对象,将其赋给obj变量 wr = new WeakReference(obj); } ...//操作obj对象 //再次切断obj和对象之间的关联 obj = null;

当if块执行完成后,obj还是有可能为null。因为垃圾回收的不确定性,假设系统在①和②行代码之间进行垃圾回收,则会再次将wr所引用的对象回收掉,从而导致obj依然为null。第二段代码则不会存在这个问题,当if块执行结束后,obj一定不是null。

与WeakReference功能类似的还有WeakHashMap。其实程序很少会考虑直接使用单个的WeakReference来引用某个Java对象,因此这种时候系统内存往往不会特别紧张。当程序有大量的Java对象需要使用弱引用来引用时,可以考虑使用WeakHashMap来保存它们

虚引用

软引用和弱引用可以单独使用,但虚引用不能单独使用,单独使用虚引用没有太大的意义。虚引用的主要作用就是跟踪对象被垃圾回收的状态,程序可以通过检查与虚引用关联的引用队列中是否已经包含指定的虚引用,从而了解虚引用所引用对象是否即将被回收。

引用队列由java.lang.ref. ReferenceQueue类表示,它用于保存被回收后对象的引用。当把软引用、弱引用和引用队列联合使用时,系统回收被引用的对象之后,将会把被回收对象对应的引用添加到关联的引用队列中。与软引用和弱引用不同的是,虚引用在对象被释放之前,将把它对应的虚引用添加到它的关联的引用队列中,这使得可以在对象被回收之前采取行动。

虚引用通过PhantomReference类实现,它完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用,那它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列(ReferenceQueue)联合使用

下面程序与上面程序基本相似,只是使用了虚引用来引用字符串对象,虚引用无法获取它引用的对象。下面程序还将虚引用和引用队列结合使用,可以看到,被虚引用所引用对象被垃圾回收后,虚引用将被添加到引用队列中。

public class PhantomReferenceTest     {           public static void main(String[] args)   throws Exception           {                 //创建一个字符串对象                 String str = new String("疯狂Java讲义");                 //创建一个引用队列                 ReferenceQueue
rq = new ReferenceQueue
(); //创建一个虚引用,让此虚引用引用到“疯狂Java讲义”字符串 PhantomReference
pr = new PhantomReference
(str , rq); //切断str引用和“疯狂Java讲义”字符串之间的引用 str = null; //试图取出虚引用所引用的对象 //程序并不能通过虚引用访问被引用的对象,所以此处输出null System.out.println(pr.get()); // ① //强制垃圾回收 System.gc(); System.runFinalization(); //取出引用队列中最先进入队列中引用与pr进行比较 System.out.println(rq.poll() == pr); //② } }

因为系统无法通过虚引用来获得被引用的对象,所以执行①处的输出语句时,程序将输出null(即使此时并未强制进行垃圾回收)。当程序强制垃圾回收后,只有虚引用引用的字符串对象将会被垃圾回收,当被引用的对象被回收后,对应引用将被添加到关联的引用队列中,因而将在②代码处看到输出true。

使用这些引用类可以避免在程序执行期间将对象留在内存中。如果以软引用、弱引用或虚引用的方式引用对象,垃圾回收器就能够随意地释放对象。如果希望尽可能减小程序在其生命周期中所占用的内存大小,这些引用类就很有好处。

最后需要指出的是:要使用这些特殊的引用类,就不能保留对对象的强引用。如果保留了对对象的强引用,就会浪费这些类所提供的任何好处。

Java的内存泄漏

程序运行过程中会不断地分配内存空间,那些不再使用的内存空间应该即时回收它们,从而保证系统可以再次使用这些内存,如果存在无用的内存没有被回收回来,那就是内存泄漏。

对于C++程序而言,对象占用的内存空间都必须由程序员来显式回收,如果程序员忘记了回收它们,那它们所占用的内存空间就会产生内存泄漏;

对于Java程序来说,所有不可达的对象都由垃圾回收机制负责回收,因此程序员不需要考虑这部分的内存泄漏。但如果程序中有一些Java对象,它们处于可达状态,但程序以后永远都不会再访问它们,那它们所占用的内存空间也不会被回收,它们所占用的空间也会产生内存泄漏。
内存泄漏
ArrayList中remove(int index)方法的源代码

public E remove(int index)      {
//检查index索引是否越界 RangeCheck(index); //使修改次数加1 modCount + +; //获取被删除的元素 E oldValue = (E)elementData[index]; int numMoved = size - index - 1; //整体搬家 if (numMoved > 0) System.arraycopy(elementData, inde x + 1 , elementData, index, numMoved); //将ArrayList的size减1 //并将最后一个数组元素赋为null,让垃圾回收机制回收最后一个元素 elementData[--size] = null; return oldValue; }

代码elementData[–size] = null;就是为了避免垃圾回收机制而书写的代码。如果没有这行代码,这个方法就会产生内存泄漏——删除一个对象,但该对象所占用的内存空间却不会释放。

在这里插入图片描述
在这里插入图片描述
如果没有elementData[–size] = null;这行代码,图4.10所示数组的第4个元素将一直引用内存中原来的对象,那么这个对象将一直处于可达状态。但对于ArrayList而言,它的size等于3,也就是说该ArrayList认为自己只有3个元素,因此它永远也不会去访问底层数组的第4个元素。对于程序本身来说,这个对象已经变成了垃圾;但对于垃圾回收机制来说,这个对象依然处于可达状态,因此不会回收它,这就产生了内存泄漏。

垃圾回收机制

垃圾回收机制主要完成下面两件事情:

■ 跟踪并监控每个Java对象,当某个对象处于不可达状态时,回收该对象所占用的内存;
■ 清理内存分配、回收过程中产生的内存碎片。

高效的垃圾回收机制既能保证垃圾回收的快速运行,避免内存的分配和回收成为应用程序的性能瓶颈,又不能导致应用程序产生停顿。.

垃圾回收机制不可能实时检测到每个Java对象的状态,因此当一个对象失去引用后,它也不会被立即回收,只有等垃圾回收运行时才会被回收。

对于一个垃圾回收器的设计算法来说,大致有如下可供选择的设计:

  • 串行回收(Serial)并行回收(Parallel):串行回收就是不管系统有多少个CPU,始终只用一个CPU来执行垃圾回收操作;而并行回收就是把整个回收工作拆分成多部分,每个部分由一个CPU负责,从而让多个CPU并行回收。并行回收的执行效率很高,但复杂度增加,另外也有其他一些副作用,比如内存碎片会增加。

  • 并发执行(Concurrent)应用程序停止(Stop-the-world):Stop-the-world的垃圾回收方式在执行垃圾回收的同时会导致应用程序的暂停。并发执行的垃圾回收虽然不会导致应用程序的暂停,但由于并发执行垃圾回收需要解决和应用程序的执行冲突(应用程序可能会在垃圾回收的过程中修改对象),因此并发执行垃圾回收的系统开销比Stop-the-world更高,而且执行时也需要更多的堆内存。

  • 压缩(Compacting)不压缩(Non-compacting)复制(Copying):为了减少内存碎片,支持压缩的垃圾回收器会把所有的活对象搬迁到一起,然后将之前占用的内存全部回收。不压缩式的垃圾回收器只是回收内存,这样回收回来的内存不可能是连续的,因此将会有较多的内存碎片。较之压缩式的垃圾回收,不压缩式的垃圾回收回收内存快,而分配内存时就会更慢,而且无法解决内存碎片的问题。复制式的垃圾回收会将所有可达对象复制到另一块相同的内存中,这种方式的优点是垃圾回收过程不会产生内存碎片,但缺点也很明显,需要复制数据和额外的内存。

标记清除(mark-sweep):也就是不压缩的回收方式。垃圾回收器先从根开始访问所有可达对象,将它们标记为可达状态,然后再遍历一次整个内存区域,把所有没有标记为可达的对象进行回收处理

标记清除(mark-sweep)无需进行大规模的复制操作,而且内存利用率高。但这种算法需要两次遍历堆内存空间,遍历的成本较大,因此造成应用程序暂停的时间随堆空间大小线性增大。而且垃圾回收回来的内存往往是不连续的,因此整理后堆内存里的碎片很多。

标记压缩(mark-sweep-compact):这是压缩方式,这种方式充分利用上述两种算法的优点,垃圾回收器先从根开始访问所有可达对象,将它们标记为可达状态。接下来垃圾回收器会将这些活动对象搬迁在一起,这个过程也被称为内存压缩,然后垃圾回收机制再次回收那些不可达对象所占用的内存空间,这样就避免了回收产生的内存碎片。

现行的垃圾回收器

现行的垃圾回收器用分代的方式来采用不同的回收设计。分代的基本思路是根据对象生存时间的长短,把堆内存分成3个代:

  • Young(年轻代);
  • Old(老年代);
  • Permanent(永久代)。

垃圾回收器会根据不同代的特点采用不同的回收算法,从而充分利用各种回收算法的优点。

采用这种“分代回收”的策略基于如下两点事实:

  • 绝大对数的对象不会被长时间引用,这些对象在其Young期间就会被回收;
  • 很老的对象(生存时间很长)和很新的对象(生存时间很短)之间很少存在相互引用的情况

Young(年轻代)

根据上面两点事实,对于Young代的对象而言,大部分对象都会很快就进入不可达状态,只有少量的对象能熬到垃圾回收执行时,而垃圾回收器只需要保留Young代中处于可达状态的对象,如果采用复制算法只需要少量的复制成本,因此大部分垃圾回收器对Young代都采用复制算法

Young代由1个Eden区2个Survivor区构成。绝大多数对象先分配到Eden区中(有一些大的对象可能会直接被分配到Old代中),Survivor区中的对象都至少在Young代中经历过一次垃圾回收,所以这些对象在被转移到Old代之前会先保留在Survivor空间中。
同一时间2个Survivor空间中有一个用来保存对象,而另一个是空的,用来在下次垃圾回收时保存Young代中的对象。每次复制就是将Eden和第1个Survivor的可达对象复制到第2个Survivor区,然后清空Eden与第1个Survivor区。
Eden和Survivor的比例通过-XX:SurvivorRatio附加选项来设定,默认为32。如果Survivor太大会产生浪费,太小则会使一些Young代的对象提前进入Old代。
在这里插入图片描述

Old代

如果Young代中对象经过数次垃圾回收依然还没有被回收掉,即这个对象经过足够长的时间还处于可达状态,垃圾回收机制就会将这个对象转移到Old代。图4.12显示了这个对象由Young代提升为Old代的过程。

Old代的大部分对象都是“久经考验”的“老人”了,因此它们没那么容易死。而且随着时间的流逝,Old代的对象会越来越多,因此Old代的空间要比Young代空间更大。出于这两点考虑,Old代的垃圾回收具有如下两个特征:

  • Old代垃圾回收的执行频率无需太高,因为很少有对象会死掉;
  • 每次对Old代执行垃圾回收需要更长的时间来完成。

基于以上考虑,垃圾回收器通常会使用标记压缩算法。这种算法可以避免复制Old代的大量对象,而且由于Old代的对象不会很快死亡,回收过程不会大量地产生内存碎片,因此相对比较划算。

Permanent代

Permanent代主要用于装载Class、方法等信息,默认为64M,垃圾回收机制通常不会回收Permanent代中的对象。对于那些需要加载很多类的服务器程序,往往需要加大Permanent代内存,否则可能因为内存不足而导致程序终止。

当Young代的内存将要用完的时候,垃圾回收机制会对Young代进行垃圾回收,垃圾回收机制会采用较高的频率对Young代进行扫描和回收。因为这种回收的系统开销比较小,因此也被称为次要回收(minor collection)。当Old代的内存将要用完时,垃圾回收机制会进行全回收,也就是对Young代和Old代都要进行回收,此时回收成本就大得多了,因此也称为主要回收(major collection)。

通常来说,Young代的内存会先被回收,而且会使用专门的回收算法(复制算法)来回收Young代的内存;对于Old代的回收频率则要低得多,因此也会采用专门的回收算法。如果需要进行内存压缩,每个代都独立地进行压缩。

与垃圾回收的附加选项

下面两个选项用于设置Java虚拟机内存大小

  • -Xmx:设置Java虚拟机堆内存的最大容量,如java -Xmx256m XxxClass。
  • -Xms:设置Java虚拟机堆内存的初始容量,如java -Xms128m XxxClass。

下面选项都是关于Java垃圾回收的附加选项

  • -XX:MinHeapFreeRatio = 40:设置Java堆内存最小的空闲百分比,默认值为40,如java -XX:MinHeapFreeRatio = 40 XxxClass。
  • -XX:MaxHeapFreeRatio = 70:设置Java堆内存最大的空闲百分比,默认值为70,如java -XX:MaxHeapFreeRatio = 70 XxxClass。
  • -XX:NewRatio = 2:设置Young/Old内存的比例,如java -XX:NewRatio = 1 XxxClass。
  • -XX:NewSize = size:设置 Young代内存的默认容量,如java -XX:NewSize = 64m XxxClass。
  • -XX:SurvivorRatio = 8:设置Young代中eden/survivor的比例,如java -XX:SurvivorRatio = 8 XxxClass。
  • -XX:MaxNewSize = size:设置Young代内存的最大容量,如java -XX:MaxNewSize = 128m XxxClass。
  • -XX:PermSize = size:设置 Permanent代内存的默认容量,如java -XX:PermSize = 128m XxxClass。
  • -XX:MaxPermSize = 64m:设置Permanent代内存的最大容量,如java-XX:MaxPermSize = 128m XxxClass。

当设置Young代的内存超过了-Xmx设置的大小时,Young设置的内存大小将不会起作用,JVM会自动将Young代内存设置为与-Xmx设置的大小相等。

常见垃圾回收器

  1. 串行回收器(Serial Collector)
    串行回收器通过运行Java程序时使用-XX: + UseSerialGC附加选项启用。

串行回收器对Young代和Old代的回收都是串行的(只使用一个CPU),而且垃圾回收执行期间会使得应用程序产生暂停。具体策略为,Young代采用串行复制的算法,Old代采用串行标记压缩算法。

图4.13中所有划叉的区域代表不可达的对象,空白区域代表可达的对象。对于图4.13所示的内存分布示意,垃圾回收器将会采用如图4.14所示的方式进行回收。
图4.14简单说明了 Young代的串行回收示意,系统将Eden中的活动对象直接复制到初始为空的Survivor区中(也就是To区),如果有些对象占用空间特别大,垃圾回收器会直接将其复制到Old代中。
在这里插入图片描述
对于From Survivor区中的活动对象(该对象至少经历过一次垃圾回收),到底是复制到To Survivor区中,还是复制到Old代中,则取决这个对象的生存时间:如果这个对象的生存时间较长,它将被复制到Old代中;否则,将被复制到To Survivor区中。
完成上面复制之后,Eden和From Survivor区中剩下的对象都是不可达的对象,系统直接回收Eden区和From Survivor区的所有内存,而原来空的To Survivor区则保存了活动对象。在下一次回收时,原本的From Survivor区将变为To Survivor区,原本的To Survivor区将变为From Survivor区。

串行回收器对Old代的回收采用串行、标记压缩算法(mark-sweep-compact),这个算法有3个阶段:mark(标识可达对象)、sweep(清除)、compact(压缩)。在mark阶段,回收器会识别出哪些对象仍然是可达的,在sweep阶段将会回收不可达对象所占用的内存。在compact阶段回收器执行sliding compaction,把活动对象往Old代的前端启动,而在尾部保留一块连续的空间,以便下次为新对象分配内存空间。图4.16所示为Old代在执行垃圾回收前后的内存分配。

old 代串行回收前后

  1. 并行回收器

    并行回收器通过运行Java程序时使用-XX: + UseParallelGC附加选项启用,它可以充分利用计算机的多个CPU来提高来垃圾回收吞吐量。
    并行回收器对于Young代采用与串行回收器基本相似的回收算法,只是增加了多CPU并行的能力,即同时启动多线程并行来执行垃圾回收。线程数默认为CPU个数,当计算机CPU很多时,可用-XX:ParallelGCThreads = size来减少并行线程的数目。
    并行回收器对于Old代采用与串行回收器完全相同的回收算法,不管计算机有几个CPU,并行回收器依然采用单线程、标记整理的方式进行回收。
    对于并行回收器而言,只有多CPU并行的机器才能发挥其优势。

  2. 并行压缩回收器(Parallel Compacting Collector)

    并行压缩回收器是在J2SE 5.0 update 6开始引入的,它和并行回收器最大的不同是对Old代的回收使用了不同的算法,并行压缩回收器最终会取代并行回收器。并行压缩回收器通过运行Java程序时使用-XX: + UseParallelOldGC附加选项启用,一样可通过-XX:ParallelGCThreads = size来设置并行线程的数目。
    并行压缩回收器对于Young代采用与并行回收器完全相同的回收算法。
    并行压缩回收器的改变主要体现在对Old代的回收上。系统首先将Old代划分成几个固定大小的区域。在mark阶段,多个垃圾回收线程会并行标记Old代中的可达对象。当某个对象被标记为可达对象时,还会更新该对象所在区域的大小以及该对象的位置信息。
    接下来是summary阶段。summary阶段操作直接操作Old代的区域,而不是单个的对象。由于每次垃圾回收的压缩都会在Old代的左边部分存储大量可达对象,对这样的高密度可达对象的区域进行压缩往往很不划算。所以summary阶段会从最左边的区域开始检验每个区域的密度,当检测到某个区域中能回收的空间达到了某个数值的时候(也就是可达对象的密度较小时),垃圾回收器会判定该区域以及该区域右边的所有区域都应该进行回收,而该区域左边的区域都会被会被标识为密集区域,垃圾回收器既不会把新对象移动到这些密集区域中去,也不会对这些密集区域进行压缩。该区域和其右边的所有区域都会被进行压缩并回收空间。summary阶段目前还是串行操作,虽然并行是可以实现的,但重要性不如对mark和压缩阶段的并行重要。
    最后是compact阶段。回收器利用summary阶段生成的数据识别出有哪些区域是需要装填的,多个垃圾回收线程可以并行地将数据复制到这些区域中。经过这个过程后,Old代的一端会密集地存在大量活动对象,另一端则存在大块的空闲块。

  3. 并发标识-清理(Mark-Sweep)回收器(CMS)

    并发标识-清理回收器通过运行Java程序时使用-XX: + UseConcMarkSweepGC附加选项启用。
    CMS回收器对Young代的回收方式和并行回收器的回收方式完全相同。由于对Young的回收依然采用复制回收算法,因此垃圾回收时依然会导致程序暂停,除非依靠多CPU并行来提高垃圾回收的速度。
    通常来说,建议适当加大Young代的内存。如果Young代内存够大就不用频繁地进行垃圾回收,而且增加垃圾回收的时间间隔后可以让更多的Young代对象自己死掉,从而避免复制。但将Young代内存设得过大也有一个坏处:当垃圾回收器回收Young代内存时,复制成本会显著上升(复制算法必须等Young满了之后才开回收),所以回收时会让系统暂停时间显著加长。
    CMS对Old代的回收多数是并发操作,而不是并行操作。垃圾回收开始的时候需要一个短暂的暂停,称之为初始标识(initial mark)。这个阶段仅仅标识出那些被直接引用的可达对象。接下来进入了并发标识阶段(concurrent marking phase),垃圾回收器会依据在初始标识中发现的可达对象来寻找其他可达对象。由于在并发标识阶段应用程序也会同时在运行,无法保证所有的可达对象都被标识出来,因此应用程序会再次很短地暂停一下,多线程并行地重新标记之前可能因为并发而漏掉的对象,这个阶段也被称为再标记(remark)阶段。
    完成了再标记以后,所有的可达对象都已经被标识出来了,接下来就可以运行并发清理操作了。
    图4.17以简单的示意对比了串行、标记压缩回收算法和CMS回收算法的区别,其中左边是串行、标记压缩回收器的示意,右边是CMS回收器的示意,黑色箭头代表Java程序的线程,灰色箭头代表垃圾回收的后台线程。
    在这里插入图片描述
    CMS回收器的最大改进在于对Old代的回收,它只需2次短暂的暂停,而其他过程都是与应用程序并发执行的,因此对实时性要求较高的程序更合适。

对于串行、标记压缩的回收器而言,它可以等到Old代满了之后再开始回收,反正垃圾回收总会让应用程序暂停。但CMS回收器要与应用程序并发运行,如果Old满了才开始回收,那应用程序将无内存可用,所以系统默认在Old代68%满的时候就开始回收。如果系统内存设得比较大,而且程序分配内存速度不是特别快时,可以通过-XX:CMSInitiatingOccupancy Fraction = ratio适当增大这个比例。

而且CMS不会进行内存压缩,也就是说不可达对象占用的内存被回收以后,垃圾回收器不会移动可达对象占用的内存。CMS执行完垃圾回收的内存情况将会如图4.18所示。

由于Old代的可用空间不是连续的,因此CMS垃圾回收器必须保存一份可用空间的列表。当需要分配对象的时候,垃圾回收器就要通过这份列表找到能容纳新对象的空间,这样就会使得分配内存时的效率下降,从而影响了Young代回收过程中将Young代对象移到Old代的效率。

对于CMS回收器而言,当垃圾回收器执行并发标识时,应用程序在运行的同时也在分配对象,因此Old代也同时在增长。而且,虽然可达对象在标识阶段会被识别出来,但有些在标识阶段成为垃圾的对象并不能立即被回收,只有等到下次垃圾回收时才能被回收。因此CMS回收器较之前面的几种回收器需要更大的堆内存。

对于Permanent代内存,CMS可通过运行Java程序时使用-XX: + CMSClassUnloading Enabled -XX: + CMSPermGenSweepingEnabled附加选项强制回收Permanent代内存。

内存管理的小技巧

  1. 尽量使用直接量
    当需要使用字符串,还有Byte、Short、Integer、Long、Float、Double、Boolean、Character包装类的实例时,程序不应该采用new的方式来创建对象,而应该直接采用直接量来创建它们。
    例如,程序需要“hello”字符串,应该采用如下代码。
String str = "hello";

上面这种方式会创建一个“hello”字符串,而且JVM的字符串缓存池还会缓存这个字符串。但如果程序使用如下代码。

String str = new String("hello");

此时程序同样创建了一个缓存在字符串缓存池中的“hello”字符串。除此之外str所引用的String对象底层还包含一个char[]数组,这个char[]数组里依次存放了h、e、l、l、o等字符。

  1. 使用StringBuilder和StringBuffer进行字符串连接
    String、StringBuilder、StringBuffer都可代表字符串,其中String代表字符序列不可变的字符串,而StringBuilder和StringBuffer都代表字符序列可变的字符串。
    如果程序使用多个String对象进行字符串连接运算,在运行时将生成大量临时字符串,这些字符串会保存在内存中从而导致程序性能下降。
  2. 尽早释放无用对象的引用
    大部分时候,方法局部引用变量所引用的对象会随着方法结束而变成垃圾,因为局部变量的生存期限很短,当方法运行结束之时,该方法内的局部变量就结束了生命期限。因此,大部分时候程序无须将局部、引用变量显式设为null
    但换一种情况来看,如果上面程序中info()方法改为如下形式。就是有必要的:可以尽早释放对Object对象的引用
public void info()     {           Object obj = new Object();           System.out.println(obj.toString());           System.out.println(obj.hashCode());           obj = null;           //执行耗时、耗内存操作           //或者调用耗时、耗内存的方法           ...     }
  1. 尽量少用静态变量
    从理论上来说,Java对象何时被回收由垃圾回收机制决定,对程序员来说是不确定的。由于垃圾回收机制判断一个对象是否是垃圾的唯一标准就是该对象是否有引用变量引用它,因此推荐尽早释放对象的引用。
    最坏的情况是某个对象被static变量所引用,那么垃圾回收机制通常是不会回收这个对象所占的内存。示例如下。
class Person     {           static Object obj = new Object();     }

对于上面的Object对象而言,只要obj变量还引用到它,它就不会被垃圾回收机制所回收。

obj变量是Person类的静态变量,因此它的生命周期与Person类同步。在Person类不被卸载的情况下,Person类对应的Class对象会常驻内存,直到程序运行结束。因此obj所引用的Object对象一旦被创建,也会常驻内存,直到程序运行结束。

  1. 避免在经常调用的方法、循环中创建Java对象
    经常调用的方法和循环有一个共同特征,这些代码段会被多次重复调用。示例如下。
public class Test     {
public static void main(String[] args) {
for (int i = 0 ; i < 10 ; i + +) {
Object obj = new Object(); //执行其他操作... } } }

上面代码在循环中创建了10个Object对象,虽然上面程序中的obj变量都是代码块的局部变量,当循环执行结束时这些局部变量都会失效,但由于这段循环导致Object对象会被创建10次,因此系统需要不断地为这10个对象分配内存空间,执行初始化操作。这10个对象的生存时间并不长,接下来系统又需要回收它们所占的内存空间,在这种不断的分配、回收操作中,程序的性能受到巨大的影响。

  1. 缓存经常使用的对象
    如果有些对象需要被经常使用,可以考虑把这些对象用缓存池保存起来,这样当下次需要时就可直接拿出这些对象来用。典型的缓存就是数据连接池,数据连接池里缓存了大量数据库连接,每次程序需要访问数据库时都可直接取出数据库连接。

除此之外,如果系统中还有一些常用的基础信息,比如信息化信息里包含的员工信息、物料信息等,也考虑对它们进行缓存。实现缓存时通常有两种方式:

  • 使用HashMap进行缓存;
  • 直接使用某些开源的缓存项目。

如果直接使用HashMap进行缓存,程序员需要手动控制HashMap容器里key-value对不至于太多,因为当key-value太多时将导致HashMap占用过大的内存,从而导致系统性能下降。

  1. 尽量不要使用finalize方法
    前面介绍垃圾回收机制时已经提到,在一个对象失去引用之后,垃圾回收器准备回收该对象之前,垃圾回收机制会先调用该对象的finalize()方法来执行资源清理。出于这种考虑,可能有些开发者会考虑使用finalize()方法来进行资源清理。

实际上,将资源清理放在finalize()方法中完成是非常拙劣的选择。根据前面介绍的垃圾回收算法,垃圾回收机制的工作量已经够大了,尤其是回收Young代内存时,大都会引起应用程序暂停,使得用户难以忍受。

在垃圾回收器本身已经严重制约应用程序性能的情况下,如果再选择使用finalize()方法进行资源清理,无疑是一种火上浇油的行为,这将导致垃圾回收器的负担更大,导致程序运行效率更差。

  1. 考虑使用SoftReference
    当程序需要创建长度很大的数组时,可以考虑使用SoftReference来包装数组元素,而不是直接让将数组元素来引用对象。

SoftReference是个很好的选择:当内存足够时,它的功能等同于普通引用;当内存不够时,它会牺牲自己,释放软引用所引用的对象。

以前 创建了一个长度为100 000的Person数组,如果直接使用强引用的数组,这个Person数组将会导致程序内存溢出;如果程序改为创建长度为100 000的软引用数组,程序将可以正常运行——当系统内存紧张时,系统会自动释放软引用所引用的对象,这样能保证程序的继续运行。

使用软引用来引用对象时不要忘记了软引用的不确定性。程序通过软引用所获取的对象有可能为null——当系统内存紧张时,SoftReference所引用的Java对象将被释放。由于通过SoftReference获取的对象可能为null,因此应用程序取出SoftReference所引用的Java之后,应该显式判断该对象是否为null;当该对象为null时,应重建该对象。

转载地址:https://blog.csdn.net/ynchyong/article/details/112275611 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:java表达式--读《疯狂java》 笔记
下一篇:C#对象销毁

发表评论

最新留言

留言是一种美德,欢迎回访!
[***.207.175.100]2024年04月15日 03时41分50秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章