拿饭网对java垃圾回收大杂烩

  张一帆   2020年01月27日


  • 概念

  垃圾回收(Garbage Collection,GC)是一种在堆内存中找出哪些对象在被使用,哪些对象没有被使用,并且通过一种机制能够把后者持有的内存进行回收。


  • 意义

  保证内存有效的使用,防止出现内存泄露(OOM)。


  • 垃圾的发现

  所谓的垃圾是产生在内存之中,通过什么办法能够准确的找到垃圾呢?

  • 引用计数法

  引用计数法是GC的早期策略。此方法是通过将堆中的每个对象添加一个引用计数器。每当一个地方引用了这个对象时,就会在这个计数器上加1,每当一个地方取消了引用这个对象,就会在这个计数器上减1.直到计数器的值为0时这个对象就会被GC识别到并删除。

  这种算法有种弊端,两个对象互相引用。那么,他们的引用计数器永远不能为0.也就是说,这两个对象就会常驻在内存中。GC永远也不可能清楚他俩。通过下面的例子,我们可以看到object1和object2虽然已经被赋值为null了,但是当方法结束时,因为其都有一个对方的引用,所以这两个对象并不会被GC发现

  
public class Main {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();
          
        object1.object = object2;
        object2.object = object1;
          
        object1 = null;
        object2 = null;
    }
}


  • 跟搜索算法(可达性分析算法)

  由于引用计数法存在着缺陷,所以现在一般采用的方法就是根搜索算法。这种方式可以将所有对象的引用关系想象成一棵树,从树的根节点GC Root遍历所有引用的对象,树的节点就为可达对象,其他没有处于节点的对象则为不可达对象。

1

什么样的对象可以作为GC的根节点?可以参考这篇文章

CLass:由系统加载器(class loader)加载的对象,这些类是不能为回收的,他们可以以静态字段的方式保存持有其他对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其他的某种(或多种)方式成为roots,否则他们并不是roots。

Thread:存活的线程,线程也是一种资源,很自然的会是roots。

Stack Local:java方法的local变量或参数。

JNI Local:native方法的变量或参数。

JNI Global:native全局引用。

Monitor Used:一些起到监控系统作用的对象。

Held by JVM:JVM因为他的目的让GC持有的对象。实际上这些对象是依赖于JVM的实现的。可能的情况:系统的类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。

  我看好多文章都说的是:

  1.虚拟机栈(栈帧中的本地变量表)中引用的对象;

  2.方法区中类静态属性引用的对象;

  3.方法区中常量引用的对象;

  4.本地方法栈中JNI(即一般说的Native方法)引用的对象;

  最终,总结出的就是方法运行时,方法中引用的对象;类的静态变量引用的对象;类中常量引用的对象;Native方法中引用的对象。


  • 垃圾回收算法

  • 标记-清除(Mark-Sweep)算法

  1.标记阶段:标记出需要被回收的对象。

  2.清除阶段:回收被标记的可回收对象的内部空间。

2

  标记-清除算法容易实现,不需要移动对象,但存在的问题:

  1.算法过程比较耗时,效率不高。

  2.标记清除后会产生大量不连续的内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

  另外,这种算法在清理完内存以后,会产生大量的碎片。这种算法不再进行排序,原因很简单这种重新排列很大概率不稳定,也即是说随时可能会再触发一次标记-清除,所以排序的意义不大。而且这种排序的算法比较困难,最终就不会排序了。

  • 复制(Copying)算法

  为了解决标记-清除算法的缺陷,所以才出来了复制算法。

  复制算法将内存分为两块,每次只用其中一块,当一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次性清理掉。

3

  这种算法优点是实现简单,不易产生空间碎片。但是,缺点就是内存空间缩减为了原来的一半了。算法的效率和存活对象的数目有关,存活对象越多,效率越低。

  • 标记-整理(Mark-Compact)算法

  为了更充分利用内存空间,提出了标记-整理算法。此算法结合了“标记-清除”和“复制”两个算法的优点。该算法标记阶段和“标志-清除”算法一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

4

  • 分代收集(Generational Collection)算法

  分代收集算法是目前大部分 JVM 的垃圾收集器采用的算法。核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

5

区域划分:

年轻代(Young Generation

  1. 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
  2. 新生代内存按照8:1的比例分为一个eden区和一个大块的 survivor(由survivor1+survivor2组成)区。每次新生代中可用内存空间为整个新生代内存的90%,剩下的10%会被“浪费”。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor1区,然后清空eden区,当这个survivor1区也存放满了时,则将eden区和survivor1区存活对象复制到另一个survivor2区,然后清空eden和这个survivor1区,此时survivor1区是空的,然后将survivor1区和survivor2区交换,即保持survivor2区为空, 如此往复。
  3. 当survivor2区不足以存放eden和survivor1的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
  4. 新生代发生的GC也叫做Minor GC。Minor GC发生频率比较高(不一定等Eden区满了才触发,随时可能触发。)。

6


年老代(Old Generation

  1. 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
  2. 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC。Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

持久代(Permanent Generation) 用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

产生Full GC的原因:

  1. 老年代被写满。
  2. 持久代别写满。
  3. System.gc()被显示调用。
  4. 上一次GC之后HEAP的各域分配策略动态变化。(不理解)

  • 四种引用状态

  实际开发过程中,我们对new出来的对象也会根据重要程度,有个等级划分。有些必须用到的对象,我们希望它在其被引用的周期内能一直存在;没那么重要的对象,当内存不足时能够让位。由此,java对引用划分为4种:Strong,weak,soft,phantom。

1.strong References(强引用)

  代码中普遍存在的类似MyClass obj = new MyClass()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

  

      //obj是刚刚创建创建的MyClass实例的强引用,目前obj不会被回收
    MyClass obj = new MyClass ();          

  只有当强引用的变量指向null时才会被回收。

  

      //obj不在是任何实例的引用了,所以MyClass类型的obj对象现在可以被回收
    obj = null; 

7

      // Java program to illustrate Strong reference
    class Gfg
    {
        //Code..
    }
    public class Example
    {
        public static void main(String[] args)
        {
             //Strong Reference - by default
            Gfg g = new Gfg();    
              
            //Now, object to which 'g' was pointing earlier is 
            //eligible for garbage collection.
            g = null; 
        }
    } 

2.soft References(软引用)

  即使对象可以被GC回收但也不会被GC回收,直到JVM急切的需要内存的时候才会回收。一般情况下就是快要发生OOM的情况下才会发生。Java 中的类 SoftReference 表示软引用。

  8

      //讲解弱引用的代码
    import java.lang.ref.SoftReference;
    class Gfg
    {
    	//code..
    	public void x()
    	{
    		System.out.println("GeeksforGeeks");
    	}
    }
    
    public class Example
    {
    	public static void main(String[] args)
    	{
    		// Strong Reference
    		Gfg g = new Gfg();	
    		g.x();
    		
    		//创建指向Gfg类型对象的软引用,让g指向。
    		SoftReference<Gfg> softref = new SoftReference<Gfg>(g);
    	
    		//让g可以被垃圾回收
    		g = null;
    		
    		//现在可以从软引用中获取到g。调用g的方法依然ok
    		g = softref.get();
    		
    		g.x();
    	}
    }

  下面是输出。可以看到第一次输出是强引用的时候,然后我们创建了个软引用,再把g指向null,让gc回收,最后我们再从软引用中获取g,通过调用g的方法仍然可以输出。这就说明了,软引用的变量在还没有发生OOM的时候,即使可以被回收了也不会被回收,知道OOM前才会。我们不妨模拟下OOM的情况,看看g会不会被回收哈。

  
GeeksforGeeks
GeeksforGeeks


3.weak References(弱引用)

  弱引用对象不是引用对象的默认类型/类,应该在使用它们时显式指定它们:

  在WeakHashMap中使用这种类型的对象。

  如果JVM检测到一个对象只有弱引用(即没有链接到任何对象的强引用或软引用),该对象将被标记为垃圾收集。

  建立的时候使用java.lang.ref.WeakReference类。

  这些引用在建立DBConnection的实时应用程序中使用,当使用数据库的应用程序关闭时,垃圾收集器可能会清理DBConnection。    9

  //用来讲解弱引用的例子
import java.lang.ref.WeakReference;
class Gfg
{
	//code
	public void x()
	{
		System.out.println("GeeksforGeeks");
	}
}

public class Example
{
	public static void main(String[] args)
	{
	   //强引用
		Gfg g = new Gfg();	
		g.x();
		
		//创建指向Gfg类型对象的弱引用,让g指向。
		WeakReference<Gfg> weakref = new WeakReference<Gfg>(g);
		
		//让g可以被垃圾回收。但是只有在JVM需要内存时才会回收
		g = null;
		
		//现在可以从弱·引用中获取到g。调用g的方法依然ok
		g = weakref.get();
		
		g.x();
	}
}


输出

  
GeeksforGeeks
GeeksforGeeks


两种不同的弱引用层级,分别是软引用和虚引用  

4.phantom References(虚引用)

  被虚引用引用的对象是可以被GC的。但是,从内存中移除之前JVM会将他们放到一个叫做references queue的队列里。 他们是通过被调用了finalize()后才会进到这个队列里的。Java 中的类 PhantomReference 表示虚引用。

  //讲解虚引用的代码
import java.lang.ref.*;
class Gfg
{
	//code
	public void x()
	{
		System.out.println("GeeksforGeeks");
	}
}

public class Example
{
	public static void main(String[] args)
	{
		//强应用
		Gfg g = new Gfg();	
		g.x();
		
		//构造引用队列
		ReferenceQueue<Gfg> refQueue = new ReferenceQueue<Gfg>();

		//构造Gfg类型的对象的虚引用。并由g指向
		PhantomReference<Gfg> phantomRef = null;
		
		phantomRef = new PhantomReference<Gfg>(g,refQueue);
		
		//g可以被回收了,但是,在从内存移除之前它都在refQueue中呆着
		g = null;
		
		//虚引用会一直返回null的。
		g = phantomRef.get();
		
		//相当于调用的是null.x()。会报NPE
		g.x();
	}
}


  输出

      RunTime Error
    Exception in thread "main" java.lang.NullPointerException
    at Example.main(Example.java:31)
    
    Output:
    GeeksforGeeks

  通过上面的列举,大致有了对4种引用的概念。但是,不知道你们注意没注意到弱引用和软引用具体有什么区别其实还是没有体现出来。我通过搜索,看到人家有一句话:弱引用与软引用的区别在于只具有弱引用的对象拥有更短暂的生命周期。看完是不是还是很蒙?我去问了一下我们部门的java大佬,结果发现大佬也没有研究,可能是这个问题比较细了,在这里我也就不展开了,等以后再说吧。


  • 垃圾收集器

  不同虚拟机所提供的垃圾收集器可能会有很大区别,下面的例子是hotspot>

  新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge。   老年代收集器使用的收集器:Serial Old、Parallel Old、CMS。

10

  • Serial收集器

  最基本,发展历史最悠久的收集器,在JDK1.3.1之前是虚拟机新生代收集的唯一选择。他是个单线程收集器,所谓的单线程指的不是他只开一条线程去做垃圾收集,而是他必须暂停其他所有的工作线程,直到他手机结束,也就是stop the world。Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器,它简单而高效。

11

  • ParNew收集器

  是Serial的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数(-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、stop the world、对象分配规则、回收策略等都和serial收集器完全一样。运行在Server模式下大的虚拟机中首选的新生代收集器,除了Serial以外,目前只有它能够与CMS配合工作。ParNew收集器使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。

12

  • Paraller Scavenge收集器

  Parallel Scavenge收集器是一个新生代收集器,使用复制算法,并行的多线程收集器,

  和ParNew最大的区别是:自适应调节策略。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)

  • Serial Old收集器

  它是Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,它的主要两个用途为:

  1.用在JDK1.5以及之前版本中与Paraller Scavenge收集器搭配使用。

  2.作为CMS收集器的后背预案,在并发收集发生Concurrent Mode Failure时使用。

  • Parallel Old收集器

  Parallel Old是Paraller Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在JDK1.6中开始提供的。

  • CMS收集器

  CMS(Concurrent Mark Sweep)收集器是一种获取最短回收停顿时间为目的的收集器。基于“标记-清除”算法。整个过程分为4个步骤:

  1.初始标记(initial mark): stop the world,标记GC Roots能直接关联到的对象,速度快,t1

  2.并发标记(concurrent mark):进行GC Roots Tracing过程,时间长 ,t2

  3.重新标记(remark):stop the world,修正并发标记期间因用户进程继续运作而导致的标记,产生变动的部分,t3

  4.并发清除(concurrent sweep) 时间长

  t1<t3<t2

  整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

13

  • G1收集器

  G1(Garbage-First)收集器是当今收集器技术发展最前沿成果之一,他是一款面向服务端应用的垃圾收集器。Hotspot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。

参考链接: