JVM GC

java 垃圾回收机制

GC即垃圾收集机制是指JVM用于释放那些不再使用的对象所占用的内存。
Java的内存管理实际上就是对象的管理,其中包括对象的分配和释放。

垃圾回收重点关注的是堆和方法区部分的内存

JVM因为需要执行GC的执行,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成

jvm内存

jvm启动时进行一系列的工作,其中一项就是开辟一块运行时内存。而这一块内存中又分为了五大区域,分别是程序计数器、虚拟机栈、本地方法栈、方法区、直接内存。

  • 程序计数器

    记录程序运行的下一条指令的地址。

  • VM Stack

    用于存储局部变量表、操作数栈、动态链接、方法出口。
    方法开始调用时,会创建栈帧并入栈,方法执行结束时会出栈。每个线程都有自己的栈。

  • 本地方法栈

  • 堆是用于存放对象实例的地方。
    堆是线程共享的,这是多线程时同步机制的原因。
    堆是GC管理的主要区域。

  • 方法区

    方法区也为所以线程所共享,用于存放已加载的类信息、静态变量、常量和即时编译器编译后的代码。

垃圾回收算法

1.引用计数法

在这种方法中,堆中的每个对象实例都有一个引用计数。

开始:
当一个对象被创建时,且将该对象实例分配给一个引用变量,该对象实例的引用计数设置为1。

计数加1:
当任何其它变量被赋值为这个对象的引用时,对象实例的引用计数加1(classA a2=a1; 则a2引用的对象实例的计数器加 1)。

计数减1:

  • 但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数减1。(a1/a2超过生命周期或者a2=a3;)
  • 当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器均减1。(对象实例中有其他的对象作为成员变量或在方法中出现其他类对象)
  • 任何引用计数为0的对象实例可以被当作垃圾收集。

引用计数收集器可以很快的执行,并且交织在程序运行中,对程序需要不被长时间打断的实时环境比较有利,但其很难解决对象之间相互循环引用的问题。

2.根搜索算法

程序把所有引用关系看作一张图,从一个节点GC ROOT 开始,寻找对应的引用节点,搜索所走过的路径称为引用链,找到这个节点后,继续寻找这个节点的引用节点。当所有的引用节点寻找完毕后,剩余的节点则被认为是没有被引用到的节点,即该对象不可达,垃圾收集器将回收其所占的内存。

  • 在java语言中,可作为GC Root的对象包括以下几种对象:

    a. java虚拟机栈(栈帧中的本地变量表)中的引用的对象。
    b.方法区中的类静态属性引用的对象。
    c.方法区中的常量引用的对象。
    d.本地方法栈中JNI本地方法的引用对象(Native对象)。

java方法区被称为永久代,java虚拟机规范也没有对该部分内存的垃圾收集做规定,但是方法区中的废弃常量和无用的类还是需要回收以保证永久代不会发生内存溢出

  • 判断废弃常量的方法:

    如果常量池中的某个常量没有被任何引用所引用,则该常量是废弃常量。

  • 判断无用的类:

    (1).该类的所有实例都已经被回收,即java堆中不存在该类的实例对象。
    (2).加载该类的类加载器已经被回收。
    (3).该类所对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射机制访问该类的方法。

3.标记-清除算法

算法分为“标记”和“清除”两个阶段:该算法首先从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象并进行回收。


标记清除算法.png

优点:
不需要进行对象的移动,仅对不存活的对象进行处理,在存活的对象比较多的情况下极为高效

缺点:
直接回收不存活的对象,并没有对还存活的对象进行整理,因此会导致内存碎片。空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

4.复制算法

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

复制算法在存活对象比较少的时候,极为高效,但是带来的成本是牺牲一半的内存空间用于进行对象的移动。所以复制算法的使用场景,必须是对象的存活率非常低才行,比如新生代。而且最重要的是,造成了50%内存的浪费。

5.标记整理算法

标记-整理算法采用 标记-清除 算法一样的方式进行对象的标记、清除,但在回收不存活的对象占用的空间后,会将所有存活的对象往左端空闲空间移动,并更新对应的指针。

标记-整理 算法是在标记-清除 算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。

标记整理算法.png

复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记-整理算法效率会大大提高。所以JVM为了优化内存的回收,使用了分代回收的方式,对于新生代内存的回收主要采用复制算法。而对于老年代的回收,大多采用标记-整理算法。

6.分代收集算法
  • 新生代:

绝大多数最新被创建的对象都会被分配到这里,新生代使用复制算法标记-清除算法

新生代分为Eden,Survivor from和Survivor to三部分,占新生代内存容量默认比例为8:1:1,其中Survivor from和Survivor to总有一个区域是空白,当新生代内存空间不足需要进行垃圾回收时,仍然存活的对象被复制到空白的Survivor内存区域中,Eden和原来非空白的Survivor进行标记-清理回收。(也就是说对象在刚刚被创建之后,是保存在Eden的,Eden 回收后存活的保存在其中一个非空的Survivor,当这个Survivor满了,就对它回收,将存活的保存在另一个空的Survivor,并把这个原来非空的Survivor一次性清空,那些长期存活的对象会经由Survivor转存到老年代空间。)

例外:在Survivor 空间不足的情况下,对于一些需要分配一块比较大的连续内存空间的对象直接进入到老年代。

对象从这个区域消失的过程我们称之为”minor GC“。

  • 老年代:
    对象没有变得不可达,并且从新生代周期中存活了下来,会被拷贝到这里。这个区域分配的空间要比新生代多,默认的新生代、老年代所占空间比例为 1 : 2 。因为老年代有相对大的空间,所以发生在老年代的GC次数要比新生代少得多。

老年代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在老年代中使用标记-整理算法回收。回收次数相对比较少,每次回收的时间也比较长。

老年代中存在一个 card table。所有老年代的对象指向新生代对象的引用都会被记录在这个表中。当针对新生代执行GC的时候,需要查询 card table 来决定是否可以被回收,而不用查询整个老年代。

对象从老年代中消失的过程,我们称之为”major GC“(或者”full GC“)

  • 永久代(也称之为 方法区):
    用于保存类常量以及字符串常量,是被各个线程共享的内存区域。这个区域不是用于存储那些从老年代存活下来的对象,这个区域也可能发生GC,永久代使用标记-整理算法进行垃圾回收在这个区域发生GC的条件非常严苛,必须符合以下三种条件才会被回收:

    1) 所有实例被回收
    
    2) 加载该类的ClassLoader 被回收
    
    3) Class 对象无法通过任何途径访问(包括反射)
    

发生在这个区域上的GC事件也会被算为major GC。

引用的种类

  • 强引用
    就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类引用。 只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用
    用来描述一些还有用,但并非必需的对象。 SoftReference类表示软引用,对于被软引用关联的对象,在系统将要发生内存溢出时,会把这些对象列入回收范围后,进行二次回收。
  • 弱引用
    用来描述非必需对象的,但是它的强度比软引用更弱一些,WeakReference类表示弱引用,对于被弱引用关联的对象,只能生存到下一次垃圾回收发生之前。
  • 虚引用
    是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。PhantomReference类表示虚引用,虚引用不对关联的对象的生存时间构成影响,也无法取得对象实例,它唯一的作用是在对象被GC回收是收到一条系统通知。

垃圾收集器

1、串行垃圾回收器

串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。它最适合的是简单的命令行程序。

通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。

2、并行垃圾回收器

并行垃圾回收器也叫做 throughput collector 。它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,它也会冻结所有的应用程序线程当执行垃圾回收的时候

3、并发标记扫描垃圾回收器

并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。

  • 当标记的引用对象在tenured区域;
  • 在进行垃圾回收的时候,堆内存的数据被并发的改变。

相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。

通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。

4、G1垃圾回收器

G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域

通过JVM参数 –XX:+UseG1GC 使用G1垃圾回收器

  • Serial收集器:
    采用复制算法;
    新生代收集器;
    单线程的收集器;

但它的单线程的意义不仅仅说明它会只使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束

  • ParNew 收集器:
    采用复制算法;
    新生代收集器;
    Serial收集器的多线程版本;

并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态

并发:指用户线程与垃圾收集线程同时执行(不一定是并行的,可能会交替执行),用户程序在继续执行,而垃圾收集程序运行于另一个CPU上

  • Parallel Scavenge
    使用复制算法;
    新生代收集器;
    并行的多线程收集器。

吞吐量:就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

  • Serial Old 收集器:
    使用标记整理算法;
    Serial收集器的老年代版本;
    单线程收集器;

  • Parallel Old 收集器:
    标记整理算法;
    Paraller Seavenge收集器的老年代版本;
    使用多线程;

  • CMS收集器:

标记清除算法;
老年代收集器;
并发标记并发清除;

  • G1收集器:

它是一款面向服务器应用的垃圾收集器

1.并行与并发:利用多CPU缩短STOP-The-World停顿的时间

2.分代收集

3.空间整合:不会产生内存碎片

4.可预测的停顿

运作方式:初始标记,并发标记,最终标记,筛选回收

在 JDK1.7之前,HotSpot 使用永久代实现方法区;HotSpot 使用 GC 分代实现方法区带来了很大便利;
从 JDK1.7 开始 HotSpot 开始移除永久代。其中符号引用(Symbols)被移动到 Native Heap中,字符串常量和类引用被移动到 Java Heap中。
在 JDK1.8 中,永久代已完全被元空间(Meatspace)所取代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

一些Java编码的建议

根据GC的工作原理,我们可以通过一些技巧和方式,让GC运行更加有效率,更加符合应用程序的要求。以下就是一些程序设计的几点建议。

  1. 最基本的建议就是尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域后,自动设置为 null.我们在使用这种方式时候,必须特别注意一些复杂的对象图,例如数组,队列,树,图等,这些对象之间有相互引用关系较为复杂。对于这类对象,GC 回收它们一般效率较低。如果程序允许,尽早将不用的引用对象赋为null.这样可以加速GC的工作。

  2. 尽量少用finalize函数。finalize函数是Java提供给程序员一个释放对象或资源的机会。但是,它会加大GC的工作量,因此尽量少采用finalize方式回收资源。

  3. 如果需要使用经常使用的图片,可以使用soft应用类型。它可以尽可能将图片保存在内存中,供程序调用,而不引起 OutOfMemory。

  4. 注意集合数据类型,包括数组,树,图,链表等数据结构,这些数据结构对GC来说,回收更为复杂。另外,注意一些全局的变量,以及一些静态变量。这些变量往往容易引起悬挂对象,造成内存浪费。

  5. 当程序有一定的等待时间,程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。使用增量式GC可以缩短Java程序的暂停时间。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 15,515评论 3 83
  • 一. 垃圾回收的意义 在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对...
    Stan_Z阅读 1,917评论 0 25
  • 原文阅读 前言 这段时间懈怠了,罪过! 最近看到有同事也开始用上了微信公众号写博客了,挺好的~给他们点赞,这博客我...
    码农戏码阅读 5,940评论 2 31
  • JVM架构 当一个程序启动之前,它的class会被类装载器装入方法区(Permanent区),执行引擎读取方法区的...
    cocohaifang阅读 1,637评论 0 7
  • # 前言 在 深入浅出 JVM GC(1) 中,限于上篇文章的篇幅,我们留下了一个问题 : 如何回收? 这篇文章将...
    莫那一鲁道阅读 1,192评论 0 8