浅析Java垃圾回收机制

Java垃圾回收机制

 在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象;而在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾。JVM的一个系统级线程会自动释放该内存块。垃圾回收意味着程序不再需要的对象是”无用信息”,这些信息将被丢弃。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用。事实上,除了释放没用的对象,垃圾回收也可以清除内存记录碎片。由于创建对象和垃圾回收器释放丢弃对象所占的内存空间,内存会出现碎片。碎片是分配给对象的内存块之间的空闲内存洞。碎片整理将所占用的堆内存移到堆的一端,JVM将整理出的内存分配给新的对象。
  垃圾回收能自动释放内存空间,减轻编程的负担。这使Java 虚拟机具有一些优点。首先,它能使编程效率提高。在没有垃圾回收机制的时候,可能要花许多时间来解决一个难懂的存储器问题。在用Java语言编程的时候,靠垃圾回收机制可大大缩短时间。其次是它保护程序的完整性, 垃圾回收是Java语言安全性策略的一个重要部份。
  垃圾回收的一个潜在的缺点是它的开销影响程序性能。Java虚拟机必须追踪运行程序中有用的对象,而且最终释放没用的对象。这一个过程需要花费处理器的时间。其次垃圾回收算法的不完备性,早先采用的某些垃圾回收算法就不能保证100%收集到所有的废弃内存。当然随着垃圾回收算法的不断改进以及软硬件运行效率的不断提升,这些问题都可以迎刃而解。

  • “垃圾对象”的判定
  • 垃圾回收方式
  • 分代模型
  • 总结

1.“垃圾对象”的判定

作为一个垃圾回收机制,首要的任务必然就是判定出哪些对象是所谓的“垃圾对象”,方便回收。
这里介绍两种“垃圾对象”的判定方法。

①引用计数

首先在java的世界中,一切皆是对象,然后每一个对象都有一个引用计数器,每当有引用连接到某一对象的时候,那么引用计数增加1。当引用离开作用域或者 被置为null,那么引用计数减少1。当发生垃圾回收的时候,垃圾回收器会标记出引用计数为0的对象然后进行统一回收。这种算法的优点是高效,实现起来简单。

②根集扫描

GC Roots Tracing算法,也就是根搜索算法,那么什么叫根搜索算法呢,在讲根搜索算法之间首先要明确一个概念,就是什么是根,最常用的就是存活在堆栈 区或者静态存储区中的引用,GC roots有很多种,我们这里只是说了存在堆栈区的java local variables,根搜索算法的工作原理就是根据GC roots然后向下搜索,其实可以想象成一个BFS。如果一个对象到所有的GC roots都没有引用链跟这个对象相连,那么这个对象就会被标记,等待回收。
对于根集扫描,这里做一个例子:

Person p = new Person();
p.car = new Car(RED);
p.car.engine = new Engine();
p.car.horn = new AnnoyingHorn();

这部分的引用链如下:

            p
            ↓
        car(red)
        /      \
     engine    horn

此时,所有对象都在引用链上,所以没有对象会被回收。

若是添加一句:

p.car = new Car(BLUE);      

其引用链更改为:

            p
            ↓
        car(blue)           car(red)
        /      \            /      \
     engine    horn     engine     horn

也就是说car(red)不在引用链上,所以在GC时,会被回收

2. 四种垃圾回收方式

①标记-清理(mark-sweep)

浅析Java垃圾回收机制_第1张图片

该回收方式是基于根集扫描来完成的。

优点:

简单

缺点:

产生大量内存碎片,且效率不高。
过多内存碎片的后果就是,可能在堆中找不到足够大的连续空间而提前出发GC。

②停止-复制(stop-copy)

浅析Java垃圾回收机制_第2张图片

这种算法是为了解决上面的标记-清扫的效率问题以及内存碎片的问题,首先这种算法将内存一份为二,每次只是使用其中的一块内存,然后当将一块内存上仍然存活的对象移到另一块内存上,然后将这块内存上使用的空间一次清理掉,内存分配的时候也不需要考虑内存碎片的问题,只需要移动堆顶指针即可完成连续存储,解决了内存碎片的问题(执行原理图如上所示)

③标记-整理

浅析Java垃圾回收机制_第3张图片

相比于标记-清除,这种方式在清除掉垃圾对象后将存活的对象重新进行移动整理。

优点:

减少内存碎片,提高利用率。

缺点:

移动对象,尤其是大对象,是不那么容易的。

④分代回收

这里有必要说明下Java的内存模型。
浅析Java垃圾回收机制_第4张图片

Young Generation(新生代)

首先看一下堆内存中新生代区是如何划分区域的,新生代是由三块区域构成,Eden区,两块Survivor区(一般,一块成为from,一块成为to)。当发生回收的时候,就是利用停止-复制算法,将Eden和地一块Survivor区域中存活的对象转移到另一块Survivor区域,然后清理掉Eden和第一块Survivor区域。Hotspot虚拟机中默认的Eden:Survovor=8:1,然后在运行中可以利用的新生代内存区域为90%,剩下的10%用来复制对象,但是你不能保证在发生回收的时候,只有10%的对象存活下来,一种极端情况,在Eden和一块Survivor区域的对象全部存活下来,那么这时候剩下的80%对象该何去何从,为了解决这种问题,就会出现内存的分配担保机制,剩下的80%对象就会被复制到老年代。

Old Generation(老年代)

Old代也称老年代,与Young代相对应。相对于Young代,Old代的对象更不容易死,也就是生命周期相对较长。那么,发生FullGC时,由于Old代的大小原来就比Young要大许多,而且其内的对象也不容易死,也就是短时间内不容易被注销,一直持有内存,一次FullGC的时间会变的很长。长此以往,每次FullGC之间的间隔会越来越短。而且,对于Old代来说,我们认为其中大部分对象都是存活的,活着的对象越多,整理后产生的内存碎片越多,对象移动越耗时。FullGC在Old满时,会被触发。
FullGC的悲观策略:
JVM会计算每次从Young晋升到Old的对象的大小,取平均值,当Old代剩余内存小于平均值时,会做一次FullGC,尝试释放内存,也就是说,Old还有空间,但是已经做了FullGC。
一种情况就是,假设每次晋升Old代的对象大小都是一百兆,那么当Old还有99兆的时候,由于悲观策略,FullGC启动,但事实是Old还有99兆的剩余空间。

Permanent Generation(永生代)

永久代一般是指我们说的方法区。
其内成员通常为Class信息(类加载时的信息),常量池(String常量池等)永久代满后,也会触发FullGC常量池,占有的是永久代内存,比如String的intern()方法,若该字符串在常量池中不存在,则在常量池中创建它,长此以往,永久代也会被填充满,触发FullGC,但是,常量池中的常量若是被引用持有,则无法被回收,最后导致的情况可能就是OOM(out of momery内存溢出)像静态引用,在类被加载时就已经分配了内存,声明周期通常是从类被加载开始到类被卸载。换句话说,静态引用通常以为这对象是长命的。
最开始,永生代的设计初衷是认为他是不可变的。但是随着技术的发展,字节码增强技术可以来改变Class的内部结构。而且常量池也存在于永生代,例如String的intern()方法,会影响到常量池的大小,产生不稳定性,不当的操作可能引发FullGC,但是这又不是我们期望看到的。同时,永生代的管理越来越贴近于堆区。所以在之后的设计中(JAVA8中取消了永生代),这块区域可能会被撤销。

基于以上分代信息,分代回收其实就是对前面提到的几种回收方式的恰当利用。

每一种算法都有自己的适用场景,例如:

1.由于新生代上的对象98%都是朝生夕死的,所以我们可以利用“停止-复制”算法来对新生代进行垃圾回收,因为新生代存活的对象不多,复制起来的成本可以接受。
2.老年代上的对象存活率比较高,而且我们没有额外的内存用来内存的分配担保,所有我们在老年代采用“标记-整理”算法。

3.总结

1.两种“垃圾对象”的判定方式
2.4种垃圾回收方式(实质上是3种)
3.JVM的分代模型(新生代、老年代、永久代)
4.分代回收对于分代模型的处理方式

你可能感兴趣的:(java,垃圾回收)