GC垃圾回收

  • 垃圾回收为自动,手动只能提醒
  • GC作用于堆+方法区
  • GC大部分针对新生代
  • 轻GC ----- 普通GC
  • 重GC ----- 全局GC
  • GC算法
  • 复制算法 —GC算法-复制算法
  • 该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去
  • 幸存区01, from…to…, 0和1互相不断交换,进行gc进行复制算法
  • 若一直没有死进入到养老区
  • 优点:实现简单,不产生内存碎片
  • 缺点:浪费一半的内存空间
  • 标记清除算法 -----扫描对象,对活着的对象进行标记, 对没有标记的对象进行清除
  • 优点:不需要额外空间,优于复制算法
  • 两次扫描,浪费时间,会存在内存碎片
  • 标记压缩算法 ----- 再优化,压缩:防止碎片的产生, 方法: 向一端移动活的对象,多了一个移动成本
  • 标记清除压缩算法 ----- 先标记清除几次再进行压缩,等碎片多了之后
  • 引用计数算法 ------ 每个对象一个计数器,一般不用,因为计数器有消耗,用过多次的不删,0次的就删除了 —引用出现+1,引用删除-1
  • 总结:
  • 内存效率:时间复杂度:复制算法 > 标记清除 >标记压缩
  • 内存整齐度:复制算法=标记压缩>标记清除
  • 内存利用率 ----- 标记压缩=标记清除 > 复制算法
  • 分代收集算法(JVM调优): 没有最好的算法,只有最合适的
  • 年轻代 ----- 存活率低,复制算法
  • 老年代 ----- 存活率高, 标记清除与标记压缩混合实现

面试题:下面程序中,有几个线程在运行

GC垃圾回收_G1


Answer:有两个线程,一个是main线程,一个是后台的gc线程。

GC垃圾回收_G1_02


知识点:

  • JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC or Young GC),一种是全局GC(major GC or Full GC)
  • Minor GC和Full GC的区别
  • 普通GC(minor GC):只针对新生代区域的GC,指发生在新生代的垃圾收集动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快。
  • 全局GC(major GC or Full GC):指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC(但并不是绝对的)。Major GC的速度一般要比Minor GC慢上10倍以上 (因为养老区比较大,占堆的2/3)

对象实例化

GC垃圾回收_G1_03


从创建对象的执行步骤来分析 对象的创建过程:

  1. 判断对象对应的类是否加载、链接、初始化虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化(即判断类元信息是否存在)。如果没有,那么在双亲委派模式下,使用当前类加载器以ClassLoader+包名+类名为key进行查找对应的.class文件。如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class类对象。
  2. 为对象分配内存。 首先计算对象占用空间大小,接着在堆中划分一块内存给新对象,如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小。如果内存是规整的,那么虚拟机将采用指针碰撞法来为对象分配内存。假设java堆中内存是绝对规整的,所有用过的内存放一边,未使用过的放一边,中间有一个指针作为临界点,如果新创建了一个对象则是把指针往未分配的内存挪动与对象内存大小相同距离,这个称为指针碰撞。如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带有整理过程的收集器时使用指针碰撞;如果内存不规整,则使用空闲列表法(Free List)。事实上,Java堆的内存并不是完整的,已分配的内存和空闲内存相互交错,JVM通过维护一个列表,记录可用的内存块信息,当分配操作发生时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录。使用的GC收集器:CMS,适用堆内存不规整的情况下。
  3. 处理并发安全问题。 在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:一是采用CAS, CAS 是乐观锁的一种实现方式。所谓乐观锁就是每次不加锁,而是假设没有冲突而去完成某项操作,如果因为冲突失败,就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性;二是为每个线程预先分配一块TLAB——通过-XX:+/-UseTLAB参数来设定(JDK8及之后默认开启),为每一个线程预先分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。
  4. 初始化分配到的空间。 所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用。(这里要区别一下类加载过程的准备阶段)
  5. 设置对象的对象头。 将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM的实现。
  6. 执行init方法进行初始化。 在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。因此一般来说(由字节码中是否跟随有invokespecial指令所决定),new指令之后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。

    对象的访问定位

GC垃圾回收_老年代_04


一、句柄访问。虚拟机栈的局部变量表中记录了对象引用,指向堆空间中对应的句柄,句柄位于Java堆空间的句柄池中,一个句柄包含两个指针,分别是到堆空间的实例池的对应对象实例数据的指针和到方法区的对象类型数据的指针。

GC垃圾回收_老年代_05


二、直接指针,虚拟机栈的局部变量表中记录的对象引用直接指向了对象实例数据,而在对象实例数据中有一个到对象类型数据的指针,指向方法区中相应的对象类型数据。HotSpot采用的就是这种直接指针法。

GC垃圾回收_老年代_06


三、两者比较。两种访问定位方式各有优劣,一方面,很明显直接指针法要比句柄访问的效率高一些,另一方面,对于句柄访问而言,reference中存储稳定的句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只需改变句柄中实例数据指针即可,reference本身不需要被修改,而对于直接指针而言,对象移动的话reference也要修改。

逃逸分析-

堆是分配对象存储的唯一选择吗?

随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。

在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识,但是,有一种特殊的情况,就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就有可能被优化成栈上分配。这样就无需在堆上分配内存,也无需进行垃圾回收了。这也是常见的堆外存储技术

此外,前面提到的基于OpenJDK深度定制的TaoBaoVM,其中创新的GCIH(GC invisible heap)技术实现off-heap,将生命周期较长的Java对象从heap中移到heap外,并且GC 不能管理GCIH内部的Java对象,以此达到降低GC回收频率跟提升GC的回收效率的目的。

逃逸分析

如何将堆上的对象分配到栈,需要使用逃逸分析手段

这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。

通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

逃逸分析的基本行为就是分析对象的动态作用域:

当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸,那就使用栈上分配,随着方法执行的结束,栈空间就被移除。
当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如,作为调用参数传递到其他地方中。
几种常见的情况进行逃逸分析:

public class EscapeAnalysis {
public EscapeAnalysis obj;
/*
*方法返回EscapeAnalysis对象,发生逃逸
*/
public EscapeAnalysis getInstance(){
return obj == null ? new EscapeAnalysis() : obj;
}
/*
*为成员属性赋值,发生逃逸
*/
public void setObj(){
this.obj = new EscapeAnalysis();
}
/*
* 对象的作用域只在当前方法中有效,没有发生逃逸
*/
public void useEscapeAnalysis(){
EscapeAnalysis escapeAnalysis = new EscapeAnalysis();
}
/*
*引用成员变量的值,发生逃逸
*/
public void useEscapeAnalysis1(){
EscapeAnalysis escapeAnalysis = getInstance();
}

}

在JDK 6u23之后,HotSpot就默认开启了逃逸分析,较早的版本可以通过“-XX:+DoEscapeAnalysis”显示开启逃逸分析,“-XX: +PrintEscapeAnalysis”查看逃逸分析的筛选结果。

为了提高性能,使用逃逸分析,编译器可以对代码做如下优化:

栈上分配。将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配,这就要求开发中能使用局部变量的,就不要在方法外定义。
同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。线程同步的代价是相当高的,同步的后果是降低并发性和性能。在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步,这个过程就叫做同步省略,也叫锁消除。
分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。所谓标量,是指一个无法再分解成更小的数据的数据,Java中的原始数据类型就是标量。相对的,那些还可以分解的数据叫做聚合量。在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个成员变量来代替,这个过程就是标量替换。可以通过-XX:+EliminateAllocation开启标量替换,默认是打开的,允许将对象打散分配在栈上。

其实即使到如今逃逸分析技术也不是特别成熟,其根本原因就是无法保证逃逸分析的性能消耗一定能低于其他的消耗,虽然经过逃逸分析可以以做标量替换、栈上分配和锁消除,但是逃逸分析本身也是需要进行一系列复杂的分析的。一个比较极端的例子就是,经过逃逸分析后,发现没有一个对象是不逃逸的,那这个逃逸分析的过程就白白浪费掉了。

四类垃圾收集器

Java 8可以将垃圾收集器分为四类。

串行垃圾收集器Serial

为单线程环境设计且只使用一个线程进行GC,会暂停所有用户线程,不适用于服务器。就像去餐厅吃饭,只有一个清洁工在打扫。

并行垃圾收集器Parrallel

使用多个线程并行地进行GC,会暂停所有用户线程,适用于科学计算、大数据后台,交互性不敏感的场合。多个清洁工同时在打扫。停顿的时间会比串行垃圾收集器短。

并发垃圾收集器CMS

用户线程和GC线程同时执行(不一定是并行,交替执行),GC时不需要停顿用户线程,互联网公司多用,适用对响应时间有要求的场合。清洁工打扫的时候,也可以就餐。

GC垃圾回收_垃圾收集器_07

G1垃圾收集器

对内存的划分与前面3种很大不同,G1将堆内存分割成不同的区域,然后并发地进行垃圾回收

默认收集器有哪些?

有​​Serial​​​、​​Parallel​​​、​​ConcMarkSweep​​​(CMS)、​​ParNew​​​、​​ParallelOld​​​、​​G1​​​。还有一个​​SerialOld​​,快被淘汰了。

查看默认垃圾收集器

使用​​java -XX:+PrintCommandLineFlags​​即可看到,Java 8默认使用-XX:+UseParallelGC​。

-XX:InitialHeapSize=132375936 -XX:MaxHeapSize=2118014976 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops 
-XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC

七大垃圾收集器(上面四种垃圾收集器的实现,重要!)

七种回收器的使用:
​​​Serial​​​(串行)
​​​Parallel Scavenge​​​(并行)
​​​ParNew​​(只在新生代使用并行);


​SerialOld​​​(原本用在养老区,已经不用啦)
​​​ParallelOld​​​(老年代的并行)
​​​CMS​​(并发标记清除,用于回收老年代)


​G1​​收集器,既可以回收新生代,也可以回收老年代。

GC垃圾回收_垃圾收集器_08


连线表示可以搭配使用,红叉表示不推荐一同使用,比如新生代用​​Serial​​​,老年代用​​CMS​​搭配使用。并且,配置好新生代后,会默认配置好老年代相搭配的回收器

GC垃圾回收_垃圾收集器_09

1. Serial收集器(Serial/Serial Copying)

一句话:一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它收集结束。

串行收集器是最古老,最稳定以及效率高的收集器,只使用一个线程去回收但其在进行垃圾收集过程中可能会产生较长的停顿(Stop- The-World”状态)。虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此 Serial垃圾收集器依然是java虛拟机运行在 Client模式下默认的新生代垃圾收集器。其使用复制算法

  • 优点:单个线程收集,没有线程切换开销,拥有最高的单线程GC效率
  • 缺点:收集的时候会暂停用户线程。

使用​​-XX:+UseSerialGC​​​可以显式开启,开启后默认使用​​Serial​​​+​​SerialOld​​的组合。

GC垃圾回收_垃圾收集器_10


GC垃圾回收_老年代_11

2. ParNew收集器

ParNew收集器其实就是 Seria收集器新生代的并行多线程版本,最常见的应用场景是配合老年代的 CMS GC工作,其余的行为和Seria收集器完全一样, ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。它是很多java虚拟机运行在 Server的默认新生代收集器,采用复制算法

使用​​-XX:+UseParNewGC​​​可以显式开启,开启后默认使用​​ParNew​​​+​​SerialOld​​​的组合。但是由于​​SerialOld​​​已经过时,所以建议配合​​CMS​​使用。ParNew收集器只影响新生代,不影响老年代。

GC垃圾回收_垃圾收集器_12

3. Parallel Scavenge收集器(JDK 1.8后默认)

​ParNew​​​收集器仅在新生代使用多线程收集,老年代默认是​​SerialOld​​​,所以是单线程收集。而​​Parallel Scavenge​​​在新、老两代都采用多线程收集。​​Parallel Scavenge​​还有一个特点就是吞吐量优先收集器,可以通过自适应调节,保证最大吞吐量。采用复制算法

它重点关注的是:可控制的吞吐量( Thoughput = 运行用户代码时间/ (运行用户代码时间+垃圾收集时间),也即比如程序运行100分钟,垃圾收集时间1分钟,吞吐量就是99%)。高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务。

自适应调节策略也是parallelScavenge收集器与 ParNew收集器的一个重要区别。(自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间: MaxGCPause Millis)或最大的吞吐量。

常用JVM参数: ​​XX: +Use ParallelGC​​​或​​-XX:+ Use ParallelOldGC​​**(可互相激活)**使用 Paralle! Scavenge收集器开启该参数后:新生代使用复制算法,老年代使用标记-整理算法。

使用​​-XX:+UseParallelGC​​​可以开启, 同时也会使用​​ParallelOld​​​收集老年代。其它参数,比如​​-XX:ParallelGCThreads=N​​​可以选择N个线程进行GC,​​-XX:+UseAdaptiveSizePolicy​​使用自适应调节策略。

GC垃圾回收_老年代_13

4. SerialOld收集器

Serial Old是 Seria 垃圾收集器老年代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的老年代垃圾收集器。

在 Server模式下,主要有两个用途(了解,版本已经到8及以后)

  • 在JDK1.5之前版本中与新生代的 Parallel Scavenge收集器搭配使用。( Parallel Scavenge+ Serial old)
  • 作为老年代版中使用CMS收集器的后备垃圾收集方案

5. ParallelOld收集器

Parallel Old收集器是 Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,parallel Old收集器在JDK1.6才开始提供。

在JDK1.6之前,新生代使用 ParalleIScavenge收集器只能搭配年老代的 Serial old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量。在JDK1.6之前( Parallel Scavenge+ Serial old),Parallel old正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,JDK1.8后可以优先考虑新生代Parallel Scavenge和老年代 Parallel old收集器的搭配策略。在JDK1.8及后( Parallel Scavenge+ Parallel old)

使用-XX:+UseParallelOldGC可以开启, 同时也会使用​Parallel​收集新生代(JDK 1.8之后默认的就是这种组合)。

6. CMS收集器

CMS并发标记清除收集器,是标记清除(Mark-Sweep)算法的实现,是一种以获得最短GC停顿为目标的收集器。适用在互联网或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望停顿时间最短。是​​G1​​收集器出来之前大型应用的首选收集器。

在GC的时候,会与用户线程并发执行,不会停顿用户线程。但是在标记的时候,仍然会STW,只不过时间非常短。

使用-XX:+UseConcMarkSweepGC开启。开启过后,新生代默认使用​ParNew​,如果CMS收集效果不太理想,老年代会使用​SerialOld​作为CMS的后备收集器。

GC垃圾回收_老年代_14

CMS有四步过程:

  1. 初始标记:只是标记一下GC Roots能直接关联的对象,速度很快,需要STW(暂停所有的工作线程)。
  2. 并发标记:主要标记过程,标记全部对象,和用户线程一起工作,不需要STW。
  3. 重新标记:修正在并发标记阶段出现的变动,需要STW
  4. 并发清除:和用户线程一起,清除垃圾,不需要STW。

优缺点

优点:停顿时间少,响应速度快,用户体验好 (并发收集低停顿)

缺点

  • 由于并发进行,CMS在收集与应用线程会同时增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间
  • 对CPU资源非常敏感:由于需要并发工作,多少会占用系统线程资源。
  • 无法处理浮动垃圾:由于标记垃圾的时候,用户进程仍然在运行,无法有效处理新产生的垃圾。
  • 产生内存碎片:由于使用标记清除算法,会产生内存碎片。

回收器的选择:

GC垃圾回收_垃圾收集器_15

7. G1收集器

G1(Garbage-First)收集器,是一款面向服务器端应用的收集器。

​G1​​收集器与之前垃圾收集器的一个显著区别就是——之前收集器都有三个区域,新、老两代和元空间。而G1收集器只有G1区(garbage-first heap)和元空间(Metaspace)。而G1区,不像之前的收集器,分为新、老两代,而是一个一个Region,每个Region既可能包含新生代,也可能包含老年代。

​G1​​收集器既可以提高吞吐量,又可以减少GC时间。最重要的是STW可控,增加了预测机制,让用户指定停顿时间。

CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还是存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器——G1垃圾收集器。

主要改变是Eden, Survⅳor和 Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的 region,每个 region从1M到32M不等。一个 region有可能属于Eden, Survivor或者 Tenured内存区域。

G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色,G1与CMS的区别

  • G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
  • G1的 Stop The World(STW)更可控,G1在停顿时间上添加预测机制,用户可以指定期望停顿时间
  • G1整理空闲空间更快
  • G1需要更多的时间来预测GC停顿的时间
  • G1不希望牺牲大量的吞吐性能
  • G1不需要更大的Java Heap

使用​​-XX:+UseG1GC​​​开启,还有​​-XX:G1HeapRegionSize=n​​​、​​-XX:MaxGCPauseMillis=n​​等参数可调。

特点对比:

  • G1之前收集器的特点:
  1. 年轻代和老年代是各自独立且连续的内存块;
  2. 年轻代收集使用单Eden+S0+S1进行复制算法;
  3. 老年代收集必须扫描整个老年代区域;
  4. 都是以尽可能少而快速地执行GC为设计原则。
  • G1收集器的特点:
  1. 并行和并发:像CMS一样,能与应用程序线程并发执行。充分利用多核、多线程CPU,尽量缩短STW
  2. 分代收集:虽然还保留着新、老两代的概念(逻辑上分代),但物理上不再隔离,而是融合在Region中,Region也不要求连续,且会采用不同的GC方式处理不同的区域
  3. 空间整合:​​G1​​整体上看是标整算法,在局部看又是复制算法,不会产生内存碎片
  4. 可预测停顿:用户可以指定一个GC停顿时间,​​G1​​收集器会尽量满足

G1过程

与​​CMS​​类似,最大的好处是化整为零,只需要按照区域来进行扫描即可。

在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数​​-XX:G1HeapRegionSize=n​​可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。

大小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为:32MB*2048=65536MB=64G 内存。

GC垃圾回收_jvm_16


G1 YGC过程:

针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集+形成连续的内存块,避免内存碎片

  • Eden区数据移动到Survivor区,假如出现Survivor区空间不够,Eden区数据会部分晋升到Old区
  • Survivor区的数据移动到新的Survivor区,部分数据晋升到Old区
  • 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行

    G1收集过程小结:
  1. 初始标记。
  2. 并发标记。
  3. 最终标记。
  4. 筛选回收。

判断对象已经死亡

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不能再被任何途径使用的对象)。

GC垃圾回收_G1_17

引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。

public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;

}
}

可达性分析算法

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

GC垃圾回收_老年代_18

再谈引用

无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。

JDK1.2 之前,Java 中引用的定义很传统:如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。

JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)

1.强引用(StrongReference)

以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

2.软引用(SoftReference)

如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。

3.弱引用(WeakReference)

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:**只具有弱引用的对象拥有更短暂的生命周期。**在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

4.虚引用(PhantomReference)

"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

虚引用主要用来跟踪对象被垃圾回收的活动。

虚引用与软引用和弱引用的一个区别在于: **虚引用必须和引用队列(ReferenceQueue)联合使用。**当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

不可达的对象并非“非死不可”

即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。

被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

如何判断一个常量是废弃常量

运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?

假如在常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池。

注意: JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

如何判断一个类是无用的类

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

GC四大算法详解

  1. 引用计数法(现在一般不采用)
  2. GC垃圾回收_垃圾收集器_19

  3. 代码示例如下:虽然objectA和objectB都置空,但是他们之前曾发生过相互引用,所以调用system.gc(手动版唤醒GC,后台也开着自动档)并不能进行垃圾回收。并且,system.gc执行完之后也不是立刻执行垃圾回收。

GC垃圾回收_老年代_20


注意:在实际工作中,禁用system.gc()

2. 复制算法(Copying)

年轻代中使用的是Minor GC(YGC),这种GC算法采用的是复制算法(Copying)。

Minor GC会把Eden中的所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Old generation中,即一旦收集后,Eden是就变成空的了。

当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,通过 -XX:MaxTenuringThreshold 来设定参数),这些对象就会成为老年代。

-XX:MaxTenuringThreshold — 设置对象在新生代中存活的次数

GC垃圾回收_jvm_21


年轻代中的GC,主要是复制算法(Copying)。 HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1,一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块(from),当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法的优点是不会产生内存碎片,缺点是耗费空间。在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

GC垃圾回收_G1_22


因为Eden区对象一般存活率较低,一般的,使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的from活动区间与另外80%中存活的eden对象转移到10%的to空闲区间,接下来,将之前90%的内存全部释放,以此类推。

GC垃圾回收_老年代_23


上面动画中,Area空闲代表to,Area激活代表from,绿色代表不被回收的,红色代表被回收的。

复制算法它的缺点也是相当明显的:

  • 它浪费了一半的内存,这太要命了。

  • 如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。 所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。

3 .标记清除(Mark-Sweep)

复制算法的缺点就是费空间,其是用在年轻代的,老年代一般是由标记清除或者是标记清除与标记整理的混合实现。

GC垃圾回收_G1_24

用通俗的话解释一下标记清除算法,就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将要回收的对象标记一遍,最终统一回收这些对象,完成标记清理工作接下来便让应用程序恢复运行。

主要进行两项工作,第一项则是标记,第二项则是清除。

  • 标记:从引用根节点开始标记遍历所有的GC Roots, 先标记出要回收的对象。
  • 清除:遍历整个堆,把标记的对象清除。

缺点:此算法需要暂停整个应用,会产生内存碎片

GC垃圾回收_垃圾收集器_25


GC垃圾回收_垃圾收集器_26


GC垃圾回收_老年代_27


标记清除算法小结:

  • 1、首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止 应用程序,这会导致用户体验非常差劲
  • 2、其次,主要的缺点则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随即的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。

4. 标记压缩(Mark-Compact)

标记压缩(Mark-Compact)又叫标记清除压缩(Mark-Sweep-Compact),或者标记清除整理算法。老年代一般是由标记清除或者是标记清除与标记整理的混合实现

GC垃圾回收_G1_28


GC垃圾回收_jvm_29


GC垃圾回收_jvm_30

GC垃圾回收_jvm_31


GC垃圾回收_老年代_32

面试题:四种算法那个好
Answer:没有那个算法是能一次性解决所有问题的,因为JVM垃圾回收使用的是分代收集算法,没有最好的算法,只有根据每一代他的垃圾回收的特性用对应的算法。**新生代使用复制算法,老年代使用标记清除和标记整理算法。**没有最好的垃圾回收机制,只有最合适的。

面试题:请说出各个垃圾回收算法的优缺点

  • 内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
  • 内存整齐度:复制算法=标记整理算法>标记清除算法。
  • 内存利用率:标记整理算法=标记清除算法>复制算法。

可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程

总结:

  • 年轻代(Young Gen)

    年轻代特点是区域相对老年代较小,对象存活率低。

这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。

  • 老年代(Tenure Gen)

老年代的特点是区域较大,对象存活率高。

这种情况,存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。

**Mark(标记)**阶段的开销与存活对象的数量成正比,这点上说来,对于老年代,标记清除或者标记整理有一些不符,但可以通过多核/线程利用,对并发、并行的形式提标记效率。

**Sweep(清除)**阶段的开销与所管理区域的大小正相关,但Sweep“就地处决”的特点,回收的过程没有对象的移动。使其相对其它有对象移动步骤的回收算法,仍然是效率最好的。但是需要解决内存碎片问题。

**Compact(压缩)**阶段的开销与存活对像的数量成正比,如上一条所描述,对于大量对象的移动是很大开销的,做为老年代的第一选择并不合适。

基于上面的考虑,老年代一般是由标记清除或者是标记清除与标记整理的混合实现。以hotspot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对像的回收效率很高,而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器做为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。

GC垃圾回收_jvm_33