专栏首页爱编码JVM的垃圾回收

JVM的垃圾回收

前言

垃圾回收机制是java的一个特性,相较于c/c++程序员需要自己分配内存,在使用结束后自己回收内存而言,Java实在对程序员太友好了(所以头发较多点)。Java的垃圾回收全部都是由虚拟机自动完成的,不需要程序员额外写啥代码。作为一个Java程序猿,学习GC是非常有必要的,根据项目特性,优化GC也是一个优秀程序猿的基本能力之一。

什么是垃圾?

Java中那些不可达的对象就会变成垃圾。那么什么叫做不可达?其实就是没有办法再引用到该对象了。主要有以下情况使对象变为垃圾:

1.对非线程的对象来说,所有的活动线程都不能访问该对象,那么该对象就会变为垃圾。 2.对线程对象来说,满足上面的条件,且线程未启动或者已停止

例如以下:

(1)改变对象的引用,如置为null或者指向其他对象

Object x=new Object();//object1
Object y=new Object();//object2
x=y;//object1 变为垃圾
x=y=null;//object2 变为垃圾

(2)超出作用域

if(i==0){
Object x=new Object();//object1
}//括号结束后object1将无法被引用,变为垃圾

(3)类嵌套导致未完全释放

class A{
     A a;
}
A x= new A();//分配一个空间
x.a= new A();//又分配了一个空间
x=null;//将会产生两个垃圾

(4)线程中的垃圾

class A implements Runnable{
     void run(){
          //....
     }
}
//main
A x=new A();//object1
x.start();
x=null;//等线程执行完后object1才被认定为垃圾

JVM中将对象的引用分为了四种类型,不同的对象引用类型会造成GC采用不同的方法进行回收:

(1)强引用:默认情况下,对象采用的均为强引用(GC不会回收)

String s = “123”

(2)软引用:软引用是Java中提供的一种比较适合于缓存场景的应用(只有在内存不够用的情况下才会被GC)

public static void main(String[] args) {
        int[] ints = new int[300 * 1024 * 1024];
        SoftReference softReference = new SoftReference(ints);
        ints = null;
        System.out.println(softReference.get());
        ints = new int[300 * 1024 * 1024];
        System.out.println(softReference.get());

    }

(3)弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。(遇到就回收

public static void main(String[] args) {

        String s = new String("123");

        ReferenceQueue<Object> objectReferenceQueue = new ReferenceQueue<>();
        WeakReference weakReference = new WeakReference<>(s, objectReferenceQueue);
        s = null;
        System.out.println(weakReference.get());
        System.gc();
        System.out.println(weakReference.get());

    }

(4)虚引用:在GC时一定会被GC回收

就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。(遇到就回收,必须与ReferenceQueue一起使用) 虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

public static void main(String[] args) {
        String s = new String("123");
        ReferenceQueue<String> objectReferenceQueue = new ReferenceQueue<>();
        PhantomReference phantomReference = new PhantomReference<>(s, objectReferenceQueue);
        System.out.println(phantomReference.get());
        s = null;
        System.gc();
        System.out.println(objectReferenceQueue.poll() == phantomReference);
        System.out.println(phantomReference.get());

    }

JVM的内存空间

JVM的内存空间,从大的层面上来分析包含:新生代空间(Young)和老年代空间(Old)。新生代空间(Young)又被分为2个部分(Eden区域、Survivous区域)和3个板块(1个Eden区域和2个Survivous区域)

下边来看下具体每部分都是用来干什么的。

1)Eden(伊甸园)区域:用来存放使用new或者newInstance等方式创建的对象,默认这些对象都是存放在Eden区,除非这个对象太大,或者超出了设定的阈值 -XX:PretenureSizeThresold,这样的对象会被直接分配到Old区域。

2)2个Survivous(幸存)区域:一般称为S0,S1,理论上他们一样大。

下边将会讲解它们如何工作的。

第一次GC

在不断创建对象的过程中,当Eden区域被占满,此时会开始做Young GC也叫Minor GC

1)第一次GC时Survivous中S0区和S1区都为空,将其中一个作为To Survivous(用来存储Eden区域执行GC后不能被回收的对象)。比如:将S0作为To Survivous,则S1为From Survivous。

2)将Eden区域经过GC不能被回收的对象存储到To Survivous(S0)区域(此时Eden区域的内存会在垃圾回收的过程中全部释放),但如果To Survivous(S0)被占满了,Eden中剩下不能被回收对象只能存放到Old区域。

3)将Eden区域空间清空,此时From Survivous区域(S1)也是空的。

4)S0与S1互相切换标签,S0为From Survivous,S1为To Survivous。

第二次GC

当第二次Eden区域被占满时,此时开始做GC

1)将Eden和From Survivous(S0)中经过GC未被回收的对象迁移到To Survivous(S1),如果To Survious(S1)区放不下,将剩下的不能回收对象放入Old区域

2)将Eden区域空间和From Survivous(S0)区域空间清空;

3)S0与S1互相切换标签,S0为To Survivous,S1为From Survivous。

第三次,第四次一次类推,始终保证S0和S1有一个空的,用来存储临时对象,用于交换空间的目的。反反复复多次没有被淘汰的对象,将会被放入Old区域中,默认15次(由参数 --XX:MaxTenuringThreshold=15 决定)。

GC的一些算法

JVM要实现自动回收垃圾,那么它就需要判断,哪些内存可以回收,哪些不行。

引用计数算法

正如算法名,这个算法就是给对象增加一个引用计数,每当对象被别的对象引用时,就将该对象的引用计数加一。所以当一个对象的引用计数为0的话,那么就说明这个对象没有被任何对象使用,那么JVM就可以认为这个对象是可以回收的对象啦。

优点:它的效率非常高,能非常直观的判断一个对象是否能被回收。 缺点: 1.无法区分循环引用的对象(A引用了B,B引用了A),这2个对象的引用计数永远不可能为0,这2个对象无法被JVM回收。 2.需要维护对象引用计数的值。

可达性算法

 为了解决引用计数算法的缺点,所以就有了可达性算法,这个算法就是通过 GC Roots 的对象作为起始点,然后通过这个节点往下找他引用的对象,直到最外层的叶子节点。当一个对象无法被 GC Roots 找到时,那么它就是可回收对象。

GC Roots对象的包括如下几种:

1).虚拟机栈中的本地变量表中的引用的对象 2).方法区中的类静态属性引用的对象 3).方法区中的常量引用的对象 4).本地方法栈中JNI的引用的对象

标记清除算法

这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。

该算法会产生大量的内存碎片,可能会导致当JVM要分配大对象内存时,不能找到可用的内存空间的问题。

复制

 将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,然后把满的那块内存清掉。

不会产生内存碎片了,但是可利用的内存将变成原来内存的一半,而且需要付出复制内存对象带来的消耗。

标记压缩

结合了以上两个算法,为了避免缺陷而提出。*先找出存活对象,然后把存活的对象移向内存的一端。然后清除端边界外的对象。 *

分代收集算法

分代收集法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆划分为新生代,老生代,永久代(元数据区)。因为不同的区域,其存储对象的特点不同,因此可以根据不同区域选择不同的算法。新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,回收频率很高,老生代的特点是每次垃圾回收时只有少量对象需要被回收,回收频率很低。

垃圾回收器

年轻代垃圾回收器

serial、parallel new、parallel sacvenge垃圾回收器。

serial

单线程回收垃圾,它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。Serial收集器依然是虚拟机运行在Client模式下默认新生代收集器,对于运行在Client模式下的虚拟机来说是一个很好的选择。

parallel new

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial 收集器完全一样。

parallel sacvenge

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

 该收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可用高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

 Parallel Scavenge收集器提供两个参数用于精确控制吞吐量,分别是控制最大垃圾收起停顿时间的

-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的 -XX:GCTimeRatio参数

 Parallel Scavenge收集器还有一个参数 -XX:+UseAdaptiveSizePolicy: 这是一个开关参数,当这个参数打开后,就不需要手工指定新生代的大小( -Xmn)、Eden与Survivor区的比例( -XX:SurvivorRatio)、晋升老年代对象年龄( -XX:PretenureSizeThreshold)等细节参数,只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用 MaxGCPauseMillis参数或 GCTimeRation参数给虚拟机设立一个优化目标。

自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别

老年代垃圾回收器

老年代的垃圾回收器,它是一个单线程收集器,使用标记整理算法。

主要两大用途:

(1)在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用

(2)作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

Serial Old收集器的工作过程

Parallel Old

Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。

concurrent mark sweep

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求

CMS收集器是基于“标记-清除”算法实现的。它的运作过程相对前面几种收集器来说更复杂一些,整个过程分为4个步骤:

1.初始标记,2.并发标记,3.重新标记,4.并发清除

其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”.

CMS收集器主要优点:并发收集,低停顿

CMS的缺点:

(1)CMS回收器采用的基础算法是Mark-Sweep。所有CMS不会整理、压缩堆空间。这样就会有一个问题:经过CMS收集的堆会产生空间碎片。CMS不对堆空间整理压缩节约了垃圾回收的停顿时间,但也带来的堆空间的浪费。为了解决堆空间浪费问题,CMS回收器不再采用简单的指针指向一块可用堆空 间来为下次对象分配使用。而是把一些未分配的空间汇总成一个列表,当JVM分配对象空间的时候,会搜索这个列表找到足够大的空间来存下这个对象。

(2)CMS的另一个缺点是它需要更大的堆空间。因为CMS标记阶段应用程序的线程还是在执行的,那么就会有堆空间继续分配的情况,为了保证在CMS回 收完堆之前还有空间分配给正在运行的应用程序,必须预留一部分空间。也就是说,CMS不会在老年代满的时候才开始收集。相反,它会尝试更早的开始收集,已 避免上面提到的情况:在回收完成之前,堆没有足够空间分配!在JDK1.6中,CMS收集器的启动阀值已经提升至92%。CMS就开始行动了。–XX:CMSInitiatingOccupancyFraction=n 来设置这个阀值。

(3)需要更多的CPU资源。从上面的图可以看到,为了让应用程序不停顿,CMS线程和应用程序线程并发执行,这样就需要有更多的CPU,单纯靠线程切 换是不靠谱的。并且,重新标记阶段,为空保证STW快速完成,也要用到更多的甚至所有的CPU资源。

G1JAVA8之后广泛使用,G1 将整个对区域划分为若干个Region,每个Region的大小是2的倍数(1M,2M,4M,8M,16M,32M,通过设置堆的大小和Region数量计算得出。Region区域划分与其他收集类似,不同的是单独将大对象分配到了单独的region中,会分配一组连续的Region区域(Humongous start 和 humonous Contoinue 组成),所以一共有四类Region(Eden,Survior,Humongous和Old), G1 作用于整个堆内存区域,设计的目的就是减少Full GC的产生。在Full GC过程中由于G1 是单线程进行,会产生较长时间的停顿。G1的OldGc标记过程可以和yongGc并行执行,但是OldGc一定在YongGc之后执行,即MixedGc在yongGC之后执行。

各收集器间的搭配关系

GC常用参数配置

为GC进行JVM进行调优有非常多的书可以看。但是为面试准备了解几个是很好的。

-XX:-UseConcMarkSweepGC: 对老年代使用CMS收集器
-XX:-UseParallelGC: 对新生代使用 Parallel GC
-XX:-UseParallelOldGC: 在老年代和新生代都使用 Parallel GC
-XX:-HeapDumpOnOutOfMemoryError: 当应用发生OOM时创建一个线程转储(dump)。对诊断非常有用。
-XX:-PrintGCDetails: 打印GC的详细日志
-Xms512m: 设置初始堆大小为 512m
-Xmx1024m: 设置最大堆大小为 1024m
-XX:NewSize 和 -XX:MaxNewSize: 指定新生代的默认和最大空间。
-XX:NewRatio=3: 设置新生代和老年代大小的比例为 1:3
-XX:SurvivorRatio=10: 设置 Eden space 和 Survivor space 的比例

送上一张大图

参考文章

https://www.cnblogs.com/yy3b2007com/p/10975870.html https://blog.csdn.net/u010779707/article/details/81001258 https://blog.csdn.net/niunai112/article/details/81071438 https://blog.csdn.net/dadiyang/article/details/82823447 https://www.jianshu.com/p/8eabaf631d15

本文分享自微信公众号 - 爱编码(ilovecode),作者:zero

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-08-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 分布式ID

    系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,也常常为这个问题而纠结。生成ID的方法有很多,适应不同的场景、需求以及性能要求。所以有些比较复杂的系统会...

    用户3467126
  • 经典算法之二叉搜索树

    二叉树(Binary Tree)是一种特殊的树类型,其每个节点最多只能有两个子节点。这两个子节点分别称为当前节点的左孩子(left child)和右孩子(rig...

    用户3467126
  • SpringBoot全局异常处理

    估计大家都会了的^_^,本文代码为主,在做Web应用的时候,请求处理过程中发生错误是非常常见的情况,那我们如何才能统一且友好地返回系统异常给前台呢。

    用户3467126
  • 深入理解JVM垃圾收集机制

    程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对...

    李红
  • Java工程师成神之路(一)之jvm基础篇

    每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java ...

    哲洛不闹
  • JVM GC算法

    在判断哪些内存需要回收和什么时候回收用到GC 算法,本文主要对GC 算法进行讲解。 JVM垃圾判定算法 常见的JVM垃圾判定算法包括:引用计数算法、可达性分析算...

    武培轩
  • JVM的垃圾回收机制 总结(垃圾收集、回收算法、垃圾回收器)

      按照套路是要先装装X,谈谈JVM垃圾回收的前世今生的。说起垃圾回收(GC),大部分人都把这项技术当做Java语言的伴生产物。事实上,GC的历史比Java久远...

    哲洛不闹
  • Java程序员必须了解的JVM性能调优知识,全都在这里了

    传引用的错觉是如何造成的呢?在运行栈中,基本类型和引用的处理是一样的,都是传值,所以,如果是传引用的方法调用,也同时可以理解为“传引用值”的传值调用,即引用的处...

    IT大咖说
  • Java对象引用四个级别(强、软、弱、虚)

    最近,高级Java技术栈微信群中,有一些猿友在讨论JVM中对象的周期问题,有谈到引用的级别,现在为大家做个总结吧,虽然大多数公司并没有意识或者用到这些引用,但了...

    Java技术栈
  • 强引用、软引用、弱引用、幻象引用有什么区别?

    特点:我们平常典型编码Object obj = new Object()中的obj就是强引用。通过关键字new创建的对象所关联的引用就是强引用。 当JVM内存空...

    葆宁

扫码关注云+社区

领取腾讯云代金券