《Java程序员的基本修养》读书笔记之内存回收
《Java程序员的基本修养》读书笔记之内存回收
一条大河波浪宽 发表于4年前
《Java程序员的基本修养》读书笔记之内存回收
  • 发表于 4年前
  • 阅读 170
  • 收藏 4
  • 点赞 0
  • 评论 0

腾讯云 新注册用户 域名抢购1元起>>>   

1.Java引用的种类

1.1 对象在内存中的状态

Java对象在内存中被创建出来以后,垃圾回收机制会实时地监控每个对象的运行状态,包括对象的申请、引用、被引用、赋值等。当垃圾回收机制实时地监控到某个对象不再被引用变量所引用时,垃圾回收机制就会回收它所占的空间。
可以把JVM内存中的对象引用理解成一种有向图,把引用变量、对象都当做有向图的端点,将引用关系理解为有向图的边。有向图总是从引用端指向被引用的Java对象。因为Java的所有对象都是由一条条线程创建出来的,所以可以把线程对象当成有向图的起始顶点。对于单线程而言,这个线程只有一条main线程,所以该图就是以main进程为起点的有向图,在这个有向图中,main顶点可达的对象都处于可达状态,垃圾回收机制不会回收它们,如果某个对象在这个有向图中处于不可达状态,那么就认为这个对象不再被引用,就可以被垃圾回收机制回收了。

JVM的垃圾回收机制采用有向图的方式来管理内存中的对象,这样做的好处是可以方便的解决循环引用的问题。对于某个对象而言,只要从有向图的起始顶点不可达,那么它就会被垃圾回收机制回收。

当一个对象在堆内存中运行时,根据它在对应有向图中的状态,可以分为以下三种:

1)可达状态:当一个对象被创建以后,有一个以上的引用变量引用它。在有向图中,如果可以从顶点导航到该对象,那么它就处于可达状态,程序就可以通过引用变量来调用该对象的属性和方法。

2)可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它将先进入可恢复状态,此时从有向图的起始顶点不能导航到该对象。在这种状态下,系统的垃圾回收机制准备回收该对象所占的内存。在回收该对象之前,系统会调用可恢复状态的对象的finalize方法进行资源清理,如果系统调用finalize方法重新让一个以上的引用变量引用该对象,则这个对象会再次变为可达状态;否则,该对象将进入不可达状态。

3)不可达状态:当对象的所有关联都被切断,且系统个调用所有对象的finalize方法依然没有使该对象变成可达对象后,这个对象将永久的失去引用,最后变成不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占的资源。

一个对象可以被一个方法的局部变量引用,也可以被其他类的类变量引用,或者被其他对象的实例变量引用。当某个对象被其他类的类变量引用时,只有该类被销毁后,该对象才会进入可恢复状态;当某个对象被其他对象的实例变量引用时,只有当引用该对象的对象被销毁或者变成不可达状态后,该对象才会进入不可达状态。

1.2 Java中的引用类型

Java提供了四种引用类型:强引用、软引用、弱引用、虚引用。

它们分别对应java.lang.ref包中的FinalReference、SoftReference、WeakReference、PhantomReference四个类,但FinalReference是包内可见的,不能被直接使用,其它类都可以被直接使用。

1)强引用(FinalReference):Java程序中最常见的引用方式,程序创建一个对象,并把这个对象赋给一个引用变量,这个引用变量就是强引用。强引用的特点是:A.可以直接访问目标对象B.被强引用所引用的Java对象绝不会被垃圾回收机制回收,即使系统内存非常紧张;即使有些Java对象以后永远都不会被用到,JVM也不会回收被强引用引用的Java对象。C.由于不可能被回收,所以强引用是造成Java内存泄露的主要原因之一。

2)软引用(SoftReference):软引用的实现需要借助SoftReference来实现。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可以使用该对象,当系统中内存空间不足时,它将被回收。软引用是除了强引用之外最强的引用类型。软引用可以用于实现对内存敏感的Cache。

3)弱引用(WeakReference):软引用需要借助WeakReference实现,弱引用所引用对象的生存期比软引用更短。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,该对象的内存总是会被回收。当然,并不是一个对象只有弱引用时就立即被回收,它需要等到系统垃圾回收机制运行时才被回收。因为弱引用很容易被回收,而且垃圾回收机制的运行又不受程序员控制,所以在使用弱引用获取所引用的对象的时候需要小心空指针异常。(与弱引用类似功能的还有WeakHashMap,可以自行参考资料

4)虚引用(PhantomReference):虚引用是所有引用类型中最弱的一个,如果一个对象只有一个虚引用,那么它和没有引用的效果大致相同,当试图通过虚引用的get()方法获取所引用的对象时,结果总是会返回null。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,必须和引用队列(ReferenceQueue)联合使用。

需要注意的是,使用弱引用、软引用、虚引用的时候,不要忘记切断对象的强引用。比如:

String str = new String("AthenaJava");//注意,这里必须使用new的形式
WeakReference<String> wr = new WeakReference<String>(str);
str = null ; //一定要记得切断强引用哦~~~

//其余代码省略..

2.Java的内存泄露

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

2.1垃圾回收机制

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

1)跟踪并监控每一个Java对象,当某个对象处于不可达状态时,回收该对象所占用的内存。

2)清理内存分配、回收过程中产生的内存碎片。

2.2垃圾回收的基本算法

1)串行回收(Serial)和并行回收(Parallel):串行回收就是不管系统有多少个CPU,始终使用给一个CPU来执行垃圾回收操作;并行回收就是把整个回收工作拆分成很多部分,每一部分由一个CPU负责,从而让多个CPU进行并行回收。并行回收的执行效率很高,但复杂度增加,同时也会使系统的内存碎片增加等。

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

3)压缩(compacting)/不压缩(Non-compacting)和复制(Copying):支持压缩的垃圾回收器会把所有的活对象搬到一起,然后将之前的内存空间全部回收掉。不压缩的垃圾回收器仅仅是垃圾回收,但会产生较多的内存碎片。不压缩的垃圾回收机制比压缩的回收机制在回收内存的时候要快,但在内存分配的时候会更慢,而且无法解决内存碎片的问题。复制垃圾回收会将所有的可达对象复制到另一块相同的内存中,这样也不会产生内存碎片,但是需要复制数据和额外的内存。下面是它们的详细解释:

A. 复制:将堆内存分为两个相同的空间,从根(有向图的起始顶点)开始访问每一个关联的可达对象,将空间A的可达对象全部复制到空间B,然后一次性回收整个空间A.

对于复制算法而言,因为只需要访问所有的可达对象,将所有可达对象复制完成后就回收整个空间,完全不用理会那些不可达对象,所以遍历空间的成本比较小,但需要较大的复制成本和较多的内存。

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

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

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

现行的垃圾回收器用分代的方式来采用不同的回收设计,分代的基本思路是根据对象生存时间的长短,把堆内存分成三个代:Young代(新生代),Old(老年代),Permanent(永生代)。

2.3堆内存的分代回收

分代回收的一个依据就是对象生存时间的长短,然后根据不同代采取不同的垃圾回收策略。采用这种”分代回收“的策略是基于以下两点事实(官方文档称之为the Weak Generational Hypothesis):

1)绝大多数的对象不会被长时间引用,这些对象在其Young期间就会被回收。

2)很老的对象(生存时间很长)和很新的对象(生存时间很短)之间很少存在相互引用的情况。

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

1.Young代

Young代由一个Eden区和两个Survivor区构成。绝大多数对象先分配到Eden区中(有一些大的对象可能会直接被分配到Old代中),Survivor区中的对象都至少在youngd代中经历过一次垃圾回收,所以这些对象在被转移到Old代之前会先保留在Survivor空间中。同一时间两个Survivor空间中有一个用来保存对象,另一个是空的,用来在下次垃圾回收时保存Young代中的对象。每次复制就是将Eden和第一个Survivor区的可达对象复制到第二个Survivor区,然后清空Eden和第一个Survivor区。Young代的分区如图:

Eden和Survivor区的比例通过-XX:SurvivorRatio附加项来设置,默认为32,如果Survivor区太大则会产生浪费,太小则使一些Young代的对象提前进入Old代。

2.Old代

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

Old代的对象大部分都是”久经考验“的”老人“了,因此它们不容易死掉,Old代的对象会随着时间的增长而越来越多,因此Old代的空间要比Young代的空间更大。基于以上两个方面,Old代的垃圾回收具有如下特征:

1)Old代垃圾回收的执行效率无须太高,因为很少对象会死掉。

2)每次对Old代执行垃圾回收都需要很长时间来完成(Old代的空间大)。

所以,Old代的垃圾回收通常使用标记压缩算法,这种算法可以避免复制Old代的大量对象,而且由于Old代的对象不会很快死亡,回收过程不会产生大量内存碎片。

当Old代中的空间不足够的时候,就会触发Full GC!!

3.Permanent代

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

注:对于像Hibernate、Spring这类喜欢AOP动态生成来的框架,往往会生成很多的动态代理类,因此需要更大的Permanent代内存。我们经常见到的java.lang.OutOfMemoryError:PermGen space错误就是由于Permanent代内存耗尽而导致的。

当Young代的内存将要用完时,垃圾回收机制会对Young代进行垃圾回收,垃圾回收机制会采用较高的频率对Young代进行扫描和回收。因为这种回收的系统开销比较小,因此也被成为次要回收(minor collection)。当Old代或者Permanent代的内存将要用完时,垃圾回收机制会进行全回收,也就是对所有的代都要回收,此时的回收成本就大多了,因此也称为主要回收(major collection)。通常来说:Young代的内存会先被回收,而且会使用专门的回收算法(复制算法)来回收Young代的内存;对于Old代的回收频率则要低得多,因此会采用专门的回收算法。如果需要进行内存压缩,那么每个代都独立地进行压缩。(注:这段在《Java程序员的基本修养》一书中的描述与官方文档的内容稍有出入,下面的是官方文档原文)

When the young generation fills up , a  young generation coll ection (sometimes r eferred to as a minor collection ) of just that gener ation is performed. When the old or permanent generation fills up, what is known as a  full collection(sometimes referred to as a  major collection) is typically done. That is, all generations are collected.Commonly, the young generation is collected first, using the collection algorithm designed specifically for that generation, because it is usually the most efficient algorithm f or identifying garbage in the young generation.Then what is referred to below as the  old generation collection algorithm for a given collector is run on both the old and permanent gener ations. If compaction occurs, each generation is compacted separately.

有时候,Old代的空间太满了以至于不能够接收来自Young代的对象,那么这种情况下,除了CMS回收器(后面会介绍)之外,将不执行Young代的垃圾回收算法,相反,整个堆都将使用Old代的回收算法。(CMS的Old代回收算法是一个特例,因为它不能回收Young代的内存)。

Sometimes the old generation is too full to accept all the objects that would be likely to be promoted from the young generation to the old generation if the young generation was collected first. In that case, for all but the CMS coll ector , the young gener ation coll ection algorithm is not run. Instead, the old generation collection algorithm is used on the entire heap. (The CMS old generation algorithm is a special case because it cannot coll ect the young generation.

3.常见的垃圾回收器:

1)串行回收器:通过运行Java程序时使用-XX:+UseSerialGC来启动。

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

2)并行回收器:通过运行程序是使用-XX:+UseParallelGC来启动,它可以充分利用计算机的多个CPU来提高垃圾回收吞吐量。

并行回收器对Young代也采用复制算法,只是增加了多个CPU并行的能力,即同时启动多个线程并行来执行垃圾回收。线程数默认为CPU个数,当计算机CPU个数很多时,可使用-XX:ParallelGCThreads=size来减少并行线程的数目。

并行回收器对Old代仍采用串行标记压缩算法,无论计算机有几个CPU,依然采用单线程、标记整理的方式进行回收。对于并行回收器来说,只有多CPU并行的机器才能发挥其优势。

3)并行压缩回收器(Parallel Compacting Collector),它是在JDK5 update6才引入的,与并行回收器最大的不同就在于对于Old代的回收算法上(具体算法这里暂时不做介绍)。在运行程序时使用-XX:+UseParallelOldGC来启动,一样也可以通过-XX:ParallelGCThreads=size来设置并行线程数目。

4)并发标识-清理(Mark-Sweep)回收器(CMS):在程序运行时使用-XX:+UseConcMarkSweepGC来启动。它对于Young代的回收方式与并行回收器的回收方式完全相同。对于Old代的回收多是并发操作而不是并行操作(什么区别?)。对于Permanent代内存,该回收器可以通过在程序运行时使用-XX:+CMSClassUnloadingEnabled来强制回收Permanent代内存。

4.内存管理小技巧

1.尽量使用直接量:当使用字符串,还有Byte、Short、Integer、Long、Float、Double、Boolean、Character包装类的实例时,程序不应该采用new的方式来创建,而应该直接采用直接量来创建。

2.使用StringBuilder和StringBuffer来进行字符串连接。

3.尽早释放无用对象的引用:大部分时候,方法的局部引用变量所引用的对象会随着方法的结束而变成垃圾,因此大部分时候无需将局部引用变量显示设为null,但如果某个方法中使用完某个引用变量后,仍需要进行很多耗时的操作,那么将引用显示设为null就是有必要的。

4.尽量少使用静态变量

5.避免在经常调用的方法、循环中创建Java对象。

6.缓存经常使用的对象:1)使用HashMap缓存。2)使用某些开源的缓存框架

7.尽量不要使用finalize方法

8.考虑使用SoftReference


关于内存回收,你还可以参考官方文档:http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf 这里有一篇对该文档的翻译:http://my.oschina.net/u/568779/blog/166891

更多资料:http://www.oracle.com/technetwork/java/javase/tech/index-jsp-140228.html

共有 人打赏支持
粉丝 115
博文 27
码字总数 42547
×
一条大河波浪宽
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: