深入理解JVM虚拟机 垃圾收集和内存分配

如何确认一个对象已经死了

垃圾收集器在对堆进行回收前,第一件事情就是要确认那些对象已经死了,哪些对象还活着。我们看以下几种方法

  1. 引用计数算法
    给对象添加一个引用计数器,当有一个地方引用它的时候,计数器+1,当引用失效的时候就-1;0表示已经不能再被使用了,需要被回收。这种效率很高,但是当遇到相互循环依赖的时候就出现问题了,Java虚拟机没有使用这种方法来管理内存。
    我们来看一个例子
      ReferenceA referenceA=new ReferenceA();
      ReferenceB referenceB=new ReferenceB();

      referenceA.obj=referenceB;
      referenceB.obj=referenceA;

      referenceA=null;
      referenceB=null;

referenceA和referenceB相互依赖,但是他们其实已经没有用了,如果使用引用计数算法,则无法将他们GC。
2. 可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点,如果从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象没有引用链时,则进行GC。
“GC Roots”:包括 虚拟机栈中引用的对象、方法区中的静态属性引用对象、方法去的常量引用对象、本地方法栈JNI引用的对象

引用

引用分为:强、软、弱、虚

  • 强引用:代码中普遍存在的,类型new Object()这类。只要强引用存在,则不会被回收
  • 软引用(SoftReference):有用但非必须的对象,在系统发生内存溢出异常前,会吧这些对象列入回收范围,如果回收后内存还是不够,则抛出内存溢出异常
  • 弱引用(WeakReference):非必须对象,比软引用更弱,弱引用关联的对象只能生存到下一次垃圾回收前,无聊是否内存足够,都会回收
  • 虚引用:最弱的引用,完全可以说没有,唯一目的是在对象被回收时可以收到一个系统通知

finalize

即使不可达的对象,也不是肥死不可,至少会经历两次标记过程。第一次:如果没有引用链,则会被第一次标记,并且进行第一次筛选,筛选条件是是否有必要执行finalize()方法,如果没覆盖此方法或者已经被调用过一次这个方法,则就没必要执行了。
如果有必要执行finalize()方法,则对象会被放入F-Queue对象中,并稍后,低优先级的Finalizer线程执行它,只是触发,不一定等到他执行完成,防止方法中写了死循环之类的。之后GC将对对象F-Queue中的对象进行第二次标记,如果finalize()方法拯救了自己,则会被移除回收集合,否则就会被回收。
一下方法拯救了自己

@Override
    protected void finalize() throws Throwable {
        super.finalize();
        //重新把自己引用给变量obj
        obj=this;
    }

建议不要使用finalize()方法,因为他的运行代价高昂,不确定性大。

垃圾收集算法

  1. 标记-清除算法
    最基础的收集算法。首先标记需要回收的对象,标记完成统一回收所有被标记的对象。
    缺点:(1)效率不高,(2)清除后会产生大量不连续的内存碎片,导致之后可能会无法找到一个连续的足够大的内存区域
    这里写图片描述
  2. 复制算法
    为了解决效率问题,出现了复制算法,它将可用内存按内容划分大小相等的两块,每次只使用其中的一块。当一块的内存用完了,就将还存活的对象复制到另一块上,然后把已经使用的内存空间清理掉。每次堆整个半边进行回收,不用考虑内存碎片
    现代商业虚拟机使用这种算法来处理新生代的回收。
    缺点:(1)内存缩小为原来的一半,(2)对象的存活率较高的时候需要进行较多的复制操作,效率低
    这里写图片描述
  3. 标记-整理算法
    前面和标记-清除算法一样,但是后续不是直接进行回收,而是让所有存活的对象向一端移动,然后清理掉端边界的内存。
    这里写图片描述
  4. 分代收集算法
    把Java堆分为新生代和老年代,根据不同的代的特点,采用最适合的收集算法。
    新生代:有大批对象死去,适合使用复制算法。
    老年代:对象存活高,适合使用标记-清理或者标记-整理算法

HotSpot算法实现

遍历引用链,需要消耗大量时间,GC将会发生长时间的停顿。HotSpot使用一组成为OopMap的数据结构,在类加载完成时,就把对象内的偏移量上什么类型的数据计算出来,这样,GC扫描就可以直接得到这些信息。
在OopMap的协助下,HotSpot就可以快速的万能充GC Roots枚举。

垃圾收集器

收集算法是方法论,收集器就是具体实现。

这里写图片描述

  1. Serial收集器(新生代)
    最基本、发展最久的收集器。单线程的收集器。他在工作的时候需要暂停所有的工作线程,直到他回收结束。它现在依然是虚拟机运行在Client模式下的默认新生代收集器。一两百兆的新生代,停顿时间控制在几十毫秒最多100多毫秒内。
    优点:简单、高效。
  2. ParNew收集器(新生代)
    Serial的多线程版本,多条线程收集垃圾。他是许多运行在Server模式下的虚拟机首选的新生代收集器,他可以和CMS收集器配合工作。ParNew收集器在单CPU中没有Serial好,甚至由于线程开销,更慢,但是在多CPU中,效率高很多,默认线程数与CPU数量相同,可以使用-XX:ParallelGCThreads来限制线程数量
  3. Parallel Scavenge收集器
    新生代,复制算法收集器,并行多线程收集器。它的特点是达到一个可控的吞吐量,CPU运行用户代码的时间与CPU总消耗的时间比。-XX:MaxGCPauseMillis控制最大垃圾收集停顿时间,小了就会频繁进行回收,一次回收一部分。-XX:GCTimeRatio控制吞吐量的大小,
  4. Parallel Old收集器
    Parallel Scavenge老年代的版本,使用多线程和标记-整理算法
  5. CMS 收集器
    以获取最短回收停顿时间为目标的收集器。基于标记-清除算法实现。(1)初始标记,(2)并发标记,(3)重新标记(4)并发清除,
    (1)(2)需要停止所有线程,但是只是标记一下GC Roots直接关联的对象,速度很快,(3)(4)可以和用户程序一起运行,所有总体上来说可以和用户线程并发执行。CMS是一款优秀的收集器,并发收集、低停顿。
    缺点:(1)对CPU资源非常敏感,(2)无法处理浮动垃圾(3)标记清除产生大量碎片

  6. G1收集器
    G1(Garbage First)收集器是当前收集器技术发展的最前沿成果,在JDK 1.6_Update14中提供了Early Access版本的G1收集器以供试用。在将来JDK 1.7正式发布的时候,G1收集器很可能会有一个成熟的商用版本随之发布。这里只对G1收集器进行简单介绍①。
    G1收集器是垃圾收集器理论进一步发展的产物,它与前面的CMS收集器相比有两个显著的改进:一是G1收集器是基于“标记-整理”算法实现的收集器,也就是说它不会产生空间碎片,这对于长时间运行的应用系统来说非常重要。二是它可以非常精确地控制停顿,既能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
    G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力地避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是Garbage First名称的来由)。区域划分及有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。

GC日志

33.125:[GC[DefNew:3324K->152K(3712K),0.0025925secs]3324K->152K(11904K),0.0031680 secs]

100.667:[FullGC[Tenured:0K->210K(10240K),0.0149142secs]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs]

最前面的数字“33.125:”和“100.667:”代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数。
GC日志开头的“[GC”和“[Full GC”说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有“Full”,说明这次GC是发生了Stop-The-World的,例如下面这段新生代收集器ParNew的日志也会出现“[Full GC”(这一般是因为出现了分配担保失败之类的问题,所以才导致STW)。如果是调用System.gc()方法所触发的收集,那么在这里将显示“[Full GC(System)”。

[Full GC 283.736:[ParNew:261599K->261599K(261952K),0.0000288 secs]

接下来的“[DefNew”、“[Tenured”、“[Perm”表示GC发生的区域,这里显示的区域名称与使用的GC收集是密切相关的,例如上面样例所使用的Serial收集器中的新生代名为“Default New Generation”,所以显示的是“[DefNew”。

如果是ParNew收集器,新生代名称就会变为“[ParNew”,意为“Parallel New Generation”。

如果采用Parallel Scavenge收集器,那它配套的新生代称为“PSYoungGen”,老年代和永久代同理,名称也是由收集器决定的。

后面方括号内部的“3324K->152K(3712K)”含义是“GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)”。
而在方括号之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)”。

再往后,“0.0025925 secs”表示该内存区域GC所占用的时间,单位是秒。

内存分配与回收策略

大多数情况下,对象在新生代Eden区中分配,当Eden没有足够空间,虚拟机将发生一次Minor GC,

程序创建3个2MB和1个4MB的byte[],运行时-Xms20M,-Xmx20M,Xmn10M表示Java堆为20M,不可扩展,新生代分配10M,则老年代为20M-10M,-XX:SurvivorRatio=8表示新生代Eden区与Survivor区为8:1(复制算法,每次使用Eden和一个Survivor,复制时候,和这些存活的移动到另一个Survivor中,清除其他的空间)

运行,发生一次Minor GC生生代从6651KB变成148KB,但是总内存占用不变。
过程:首先在新生代分配6MB,当在分配4MB的时候发现空间不足了(10M*90%)<10MB,因此发生一次Minor GC,发现前面的6MB都需要用的,但是Survivor只有1MB的空间,所以只能移动到老年代中,GC后,4MB进入新生代Eden,6MB进入老年代

大对象最好直接放入老年代,减少移动的可能。-XX:PretenureSizeThreshlod参数,设置大于多少值直接进入老年代。

存活年龄

如果对象在Eden出生并经历第一次MinorGc后任然存活,并被Survivor容纳,将被移动到Survivor中,对象年龄+1,每经历一次+1,当默认大于15的时候则晋升为老年代。也可以通过设置-XX:MaxTenuringThreshold设置晋升年龄。

如果Survivor空间中相同年龄对象大小的总和大于Survivor空间的一半,年龄大于或者等于年龄对象的就直接进入老年代。

空间分配担保

发生Minor GC前,需要检查老年代可用连续空间十分大于新生代对象总空间,如果成立免责是安全的。如果不成立,则检查HandlePromotionFailure十分允许担保失败,如果可以,则检查老年代连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,尝试Minor GC,尽管有风险。如果小于或者HandlePromotionFailure不允许冒险,则进行一次Full GC

Survivor无法容纳的对象直接进入老年代。

发布了15 篇原创文章 · 获赞 5 · 访问量 1万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章