Java 的垃圾回收机制

来源:互联网 发布:37轩辕剑神臂进阶数据 编辑:程序博客网 时间:2024/06/05 22:33

GC(Garbage Collection),简单来说,当一个对象的引用(references)不再存在时,被这些对象占用的内存会被自动回收。

Java的内存管理实际上就是对象的管理,其中包括对象的分配与释放(内存分配和内存回收)。当创建对象时,GC就开始监控这个对象的地址、大小及使用情况。通常GC采用有向图的方式记录和管理堆(heap)中的所有对象,当确定一些对象为“不可达”时,GC就有责任回收这些内存空间。

垃圾回收主要做两件事 1)内存回收 2)碎片整理

Java中,GC 的对象主要是堆空间和永久区。GC需要判断对象是否可以收集。通用的垃圾回收器中最常见的为引用计数和对象引用遍历:

引用计数:对于对象A,如果任何一个对象引用了A,则A的引用计数+1,当引用失效时,引用计数器就减1,只要对象A的引用计数器的值为0,则对象A就不可能再被使用,就可以回收了-优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。-缺点:循环引用问题,每次引用和去引用都要加减对象引用遍历:从一组对象开始,沿着整个对象图上的每条链接,递归确定可到达的对象,如果某对象不能从这些根对象的一个(至少一个)到达,则将它作为垃圾收集

Java并未使用过引用计数法,现代Java的垃圾回收使用基本算法思想是标记-清除算法

标记清除算法是现代垃圾回收算法的思想基础,将垃圾回收分为两个阶段:标记阶段和清除阶段。实现:在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象(从GC ROOT 开始标记引用链--又叫可达性算法)。未被标记的对象及时未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象,这样就不怕循环问题。

ps:Java中可以作为GC ROOT 的对象有:

1.静态变量引用的对象
2. 常量引用的对象
3.本地方法栈(JNI)引用的对象
4.Java栈中引用的对象

现在Java虚拟机都是使用的分代回收的设计,分代主要分为新生代和老年代(本文后面会有相关介绍)

新生代:复制(Copying)算法,和标记清除算法相比,复制算法是一种相对高效的回收方法,但是不适用与存活对象较多的场合。

 原理:将原有的内存空间分为两块,两块空间完全相同,每次只用一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收,同样没有内存碎片的产生。 对应到新生代,即是在Eden 和FromSpace或ToSpace 之间copy(名词下文有解释),新生代采用空闲指针的方式控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存。用于检查空间是否足够,不够就触发GC,当连续分配对象时,对象会逐渐从Eden到survivor ,最后到老年代 缺点:内存的浪费,故不能用于存活对象较多的场合。

对于识别可回收的对象,此处有一个可触及性的定义

可触及的:从GC ROOT 这个根节点对象,沿着引用的链条,可以触及到的对象,即为可触及的
可复活的:一旦所有引用被释放,及时可复活状态,因为在finalize()中可能复活该对象(finalize方法只会调用一次)、
不可触及的:在finalize()之后,可能会进入不可触及状态,不可触及对象不可能复活,就可以回收了

在执行机制上JVM提供了串行GC(Serial Copying)、并行回收GC(Parallel Scavenge)和并行GC(ParNew)

1.串行GC在整个扫描和复制过程中采用单线程的方式进行,适用于单CPU、新生代空间较小及都对暂停时间要求不是非常高的应用上。最大问题是停顿时间很长,因为只使用一个线程去回收,可能产生较长的停顿现象。2.并行回收GC在整个扫描和复制过程中采用多线程的方式来进行,适用于多cpu、对暂停时间要求较短的应用上,是server级别默认采用的GC方式。相比于并行GC更关注JVM的吞吐量,可以使新生代和年老代都并行收集。3.并行GC与老年代的并发GC配合使用。设置后新生代就是并行回收,而年老代依然是串行回收,也就是并行回收不会影响年老代

新生代的多线程回收不一定快。需要看多核、单核,还有具体环境

年老代:标记-压缩算法,适用于存活对象较多的场合,如年老代。

 和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后清理边界外所有的空间,有效解决内存碎片问题。

在执行机制上,JVM提供了串行GC(SerialMSC)、并行GC(parallelMSC) 并发GC(CMS—Concurrent Mark Sweep)

JVM 有client 和server 两种模式,这两种模式的gc默认方式不同:

 client模式,新生代选择串行GC,年老代选择串行GC server模式,新生代选择并行回收GC,年老代选择并行GC

一般应用有两种选择方式:吞吐量优先和暂停时间优先,对于吞吐量优先的采用server默认并行GC,对于暂停时间优先选用并发GC(cms)

Java 中有多种GC算法,如Parallel GC、CMS 和 G1(1.7 后),下面会对CMS 和G1 收集器做一些简单的介绍:

   CMS (并发标记清除收集器):所谓并发标记清除就是CMS与用户线程一起执行。主要适用场所是响应时间的重要性大于对吞吐量的要求,能够承受垃圾回收线程和应用线程共享处理器资源,并且存在较多的长生命周期的对象。

收集周期:初始标记(CMS-initial-mark)->并发标记(CMS-concurrent-mark)->重新标记(CMS-remark)->并发清除(CMS-concurrent-sweep)->并发重设状态等待下次CMS 的触发(CMS-concurrent-reset)

   其中1,3两个步骤需要暂停所有的应用程序线程。第一次暂停从root对象开始标记存活的对象,这个阶段称为初始标记;第二次暂停是在并发标记后,暂停所有的应用程序线程,重新标记并发标记阶段遗留的对象(在并发标记阶段结束后对象状态的更新导致)。第一次暂停会比较短,第二次暂停通常会比较长,并且remark这个阶段可以并行标记。   而并发标记、并发清除、并发重设阶段的所谓并发,是指一个或多个垃圾回收线程和应用程序线程并发地进行,垃圾回收线程不会暂停应用程序的执行,如果多个处理器的情况下,并发收集线程将与应用线程在不同的处理器上运行。这样会降低应用的吞吐量。remark阶段的并行,是指暂停了所有的应用程序后,启动一定数目的垃圾回收进程进行标记,此时的应用线程是暂停的。

CMS 的特点:

尽可能降低了JVM的停顿时间,但是会影响系统的整体吞吐量和性能   1.在用户线程运行过程中,分一半cpu去做GC,系统性能在GC 阶段,反应速度就下降一半   2.清理不彻底。因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理。   3,因为和用户线程基本是一起运行的,故不能在空间快满时再清理。

G1(垃圾回收优先):

    在G1 中,堆被划分成许多连续的区域(region)。每个区域大小相等,在1M-32M之间。JVM最多支持2000个区域,可推算G1 能支持的最大内存2000*32M = 62.5G.区域(region)的大小在JVM初始化的时候决定,也可以用--xx:G1HeapReginSize 设置。    在G1 中没有物理上的Yongoing(eden/survivor)/old Generation,他们是逻辑的,使用一些非连续的区域(region)组成

JVM内存结构由程序计数器、堆、栈、本地方法栈、方法区等部分组成:

JVM内存结构

1.程序计数器
几乎不占内存,用于取下一条执行的指令
2.栈
每个线程执行方法的时候都会在栈中申请一个栈帧,每个栈帧包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果。
3.本地方法栈
用于支持native方法的执行,存储每个native方法调用的状态。
4.方法区
存放要加载的类信息、静态变量、final 修饰的常量、属性和方法信息。
5,堆

Java堆的内存结构

Java1.7及以前,分别为eden伊甸园,两个幸存代(复活代)survivor(Survivor 由FromSpace 和ToSpace 组成)。前三个也叫做年轻代新生代),其次是old**年老代** 和永久代permanent。

一个Java对象被创建,先是存在于Eden ,如果存活时间超过了两个Survivor就会转移到老年代保存。而永久代保存了对象的方法、变量等元数据,如果永久代内存不足,就会发生内存泄漏异常错误OutOfMemeoryError:PerGen.
java 8 中堆内存结构做了调整,移除了永久代。就是说不会再有OutOfMemeoryError:PermGen错误,新增了元数据区。

Sun JDK 提供的HotSpot JVM 。按照新生代(yong generation)和老年代(old generation) 的划分执行不同的GC.

新生代:绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可到达,所以很多对象会被创建在新生代,然后消失,对从这个区域消失的过程称为“minor GC”老年代:对象没有变得不可达并且从新生代存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代的GC要比新生代少的多。对象从老年代消失的过程,我们称为"major GC"full GC  full GC 是对新生代、年老代以及持久代的统一回收,由于是对整个空间的回收、因此比较慢,系统中应当尽量减少full GC 的次数。

以下的一些情况会发生full GC:

 1,年老代空间不足 2,持久代空间不足 3,CMS GC时出现了promotion failed 和 concurrent mode failure 4,统计得到新生代minorGC 时晋升年老代的平均大小小于年老代剩余空间 5,直接调用System.gc,可以DIsableExplicitGC来禁止 6.存在rmi调用时,默认会每分钟执行一次System.gc

关于finalize()方法:

三种情况:1.所有对象被Garbage Collection 时自动调用,比如运行System.gc()2.程序退出时为每个对象调用一次finalize 方法3.显式的调用finalize方法。

finalize 是Object 的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法(finalize 与Java中的析构函数不是对应的。C++中的析构函数调用的时机是确定的–对象离开作用域或delete掉,但Java中的finalize的调用具有不确定性)

不建议用finalize方法完成“非内存资源”的清理工作,因为Java并不保证finalize方法会被及时的执行,而且不会保证它们会被执行,而且finalize方法可能会带来性能问题。因为JVM通常在单独的低优先级线程中完成finalize的执行。finalize方法中,可将待回收的对象复制给GC Roots 可达的对象引用,从而达到对象再生的目的。finalize方法至多由GC执行一次(用户可以手动调用对象的finalze方法,但并不影响GC对finalize的行为)

建议用在以下情况:

- 清理本地对象(通过JNI创建的对象)- 作为确保某些非内存资源(Socket、文件等)释放的一个补充,在finalize 方法中显式地调用其他资源的释放方式

GC是分代的,GC的回收条件取决于对象是不是垃圾,而识别垃圾对象又取决于指向该对象的引用类型。Java中有四种引用类型:强、软、弱、虚

如果一个对象只有**弱引用**指向它,GC会立即回收该对象,这是一种急切的回收方式。相对的,如果有**软引用**指向这些对象,则只有在JVM需要内存时才回收这些对象。弱引用和软引用的特殊行为使得它们在某些情况下非常有用。例如:软引用可以很好地用来实现缓存,当JVM需要内存时,垃圾回收器就会回收这些只有被软引用指向的对象。而弱引用非常适合元数据,例如存储ClassLoader引用。如果没有类被加载,那么也就没有指向ClassLoader的引用。一旦上一次的强引用被去除,只有弱引用的ClassLoader就会被回收

强引用:常见的A a = new A();a就叫强引用,任何被强引用指向的对象都不能被GC,这些对象都是在程序中需要的。

软引用:使用Java.lang.SoftReference 类来表示,弱引用很好的用来实现缓存,当JVM需要内存时,垃圾回收器就会回收这些只有被软引用指向的对象,如下:

Counter prime = new Counter();SoftReference soft = new SoftReference(prime);prime =null;

强引用之后,代码的第二行为对象Counter 创建了一个软引用,该引用同样不能阻止垃圾回收器回收对象,但可以延迟回收。

弱引用:使用java.lang.ref.WeakReference 类来表示,若引用类型非常适合存储元数据。
例如,存储ClassLoader 引用。如果没有类被加载,那么也没有指向CLassLoader的引用。一旦上一次的强引用被去除,只有弱引用的ClassLoader 就会被回收。也就是说如果一个对象只有弱引用指向它,GC会立即回收该对象,这是一种急切的回收方式。如:

Counter counter = new Counter();WeakReference<Counter> weakCounter = new WeakReference<Counter>(counter)counter =null;

只要给强引用对象赋null,该对象就可以被垃圾回收器回收。因为该对象不再含有其他强引用,即使指向该对象的弱引用weakCounter 也无法阻止垃圾回收器对该对象的回收。相反的,如果该对象含有软引用,Counter对象不会立即被回收,除非JVM需要保存。

另一个使用虚引用的例子是WeakHashMap,它是除HashMap和TreeMap之外,Map接口的另一种实现。WeakHashMap有一个特点:map中的键值(keys)都被封装成弱引用,也就是说一但强引用被删除,WeakHashMap内部的弱引用也就无法阻止该对象被垃圾回收器回收。

虚引用:没有实际用途,是一种标志。拥有虚引用的对象可以在任何时候GC

ReferenceQueue :在创建任何弱引用、软引用和虚引用的过程中,可以通过如下代码提供引用队列ReferenceQueue

ReferenceQueue refQueue = new ReferenceQueue();DigitalCounter digit = newDigitalCounter();PhantomReference<DigitalCounter> phantom =new PhantomReference<DigitalCounter>(digit,refQueue);

引用实例被添加在引用队列中,可以在任何时候通过查询引用队列回收对象。

应该避免使用finalize方法,可以定义其他方法来释放非内存资源。建议用try -catch-finally 来代替。

如果手动调用了finalize,很容易出错,且它执行的优先级低,何时被调用不确定,不确定何时发生GC,只有当内存告急时,GC才工作。即使GC工作,finalize方法也不一定得到执行,这是由于程序中其他线程的优先级远远高于执行finalize()的线程优先级。因此当finalize还没被执行时,系统的其他资源,比如文件句柄、数据库连接池等已经消耗殆尽已经消耗殆尽。造成系统崩溃。且垃圾回收和finalize 方法的执行本身就是对系统资源的消耗,有可能造成程序的短暂停止,因此在程序中尽量避免使用finalize方法。

在GC或执行finalize中 可能会造成程序暂停,这引出:Stop-The-World 现象

这是Java中一种全局暂停的现象,全局停顿,所有的Java代码停止,类似JVM挂起状态(但是native 代码可以执行,但不能和JVM交互),一般由GC引起,其他的原因:1)Dump线程2)JVM的死锁检查3)堆的Dump

Stop_the_World 会在任何一种GC算法中发生。”Stop-the-World”意味着JVM因为要执行GC而停止了应用程序的执行。当Stop-the-World 发生时,除了GC所需要的线程之外,所以所有线程都处于等待状态,直到GC任务完成。GC优化一般情况下都是指减少Stop-the-world 发生的时间。

Stop-The-World 现象危害

长时间停止后,没有响应,一般新生代的GC停顿的GC 停顿时间很短,零点几秒。而老年代比较时间长,几秒甚至几十分钟,一般堆内存越大,GC 时间越长,也就是Stop-The-World 越久。所以JVM 的内存不是越大越好,要根据实际情况设置。

为了减轻GC 压力,我们需要注意的一些方面:

1,软件架构设计 2,代码的实现 3,堆空间的分配

其他:

增量式GC

GC在运行中可能会导致程序中断,增量式GC通过一定的回收算法,把一个长时间的中断,划分为很多个小的中断,通过这种方式减少GC对用户程序的影响。虽然增量式GC 在整体性能上不如普通GC 效率高,但是它能减少程序的最长停顿时间。

Sun JDK 提供的HotSpot JVM就能支持增量式GC,HotSpot JVM缺省GC方式为不使用增量GC,为了启动增量GC,我们必须在Java程序运行时增加 -Xincgc 的参数。HotSpot JVM增量GC 的实现是采用 Train GC 算法。算法基本思想是:将堆中的所有对象按照创建和使用情况进行分组(分层),将使用频繁的和相关性的对象放在一队,随着程序的运行,不断对组进行调整。当GC运行时,他总是先回收最老的(最近很少访问的)对象,如果整组都为可回收的对象,GC 将整组回收。这样GC运行只回收一定比例的不可达对象,保证程序的顺畅运行。

Java 内存应用的小技巧:
1)尽量使用直接量:String str =”dd”
2)使用StringBuilder 和StringBuffer 进行字符串连接等操作
3)尽早释放无用对象
4)尽量少使用静态变量
5)缓存常用的对象,可以使用开源的缓存框架实现
6)尽量不要使用finalize()方法
7)在必要的时候可以考虑使用软引用SoftReference

JVM载入原理:
通过jdk 中的java.exe,进行以下4步:
1)创建JVM装载环境和配置
2)状态JVM.dll
3)初始化JVM.dll 并挂届到JNIENV (JNI调用接口)实例
4)调用JNIENV实例装载并处理class 类

问答:
1)GC 发生的时间?
思路:不可预测,从新生代,年老代 分析,并回答触发条件(minor GC ,Eden满无法创建新对象。major GC,升到年老代对象大于年老代对象。full GC……详情参上)
2)GC 的对象?
思路:不可达的对象-》GC root 搜索不到-》根据垃圾收集算法思路(标识清除)。 详解参上
3) GC做什么
思路:分代,基本思想,不同的垃圾回收器 (并行,cms,G1等)详情参上

原创粉丝点击