GC垃圾回收机制概要

JVM调优主要的目的就是减少Full GC的次数和时间。

堆大小 = 新生代 + 老年代

GC主要负责三项任务:分配内存、确保被引用对象的内存不被错误地回收以及回收不在被引用的对象的内存空间。

使用工具查看GC流转的过程
jvisualvm工具, 这是jdk自带的一个工具。

堆的垃圾回收方式
java堆是GC垃圾回收的主要区域。 GC分为两种: Minor GC、Full GC(也叫做Major GC).

Minor GC(简称GC)
Minor GC是发生在新生代中的垃圾收集动作, 所采用的是复制算法。
GC一般为堆空间某个区发生了垃圾回收,
新生代(Young)几乎是所有java对象出生的地方。即java对象申请的内存以及存放都是在这个地方。java中的大部分对象通常不会长久的存活, 具有朝生夕死的特点。
当一个对象被判定为“死亡”的时候, GC就有责任来回收掉这部分对象的内存空间。
新生代是收集垃圾的频繁区域。
回收过程如下:
当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳(上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。
但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。

Full GC
Full GC 基本都是整个堆空间及持久代发生了垃圾回收,所采用的是标记-清除算法。
现实的生活中,老年代的人通常会比新生代的人 “早死”。堆内存中的老年代(Old)不同于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉” 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长,一般是Minor GC的 10倍以上。
另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作

扩展: Minor GC是如何触发的, 又是如何工作的?
Minor GC是由字节码执行引擎触发的. 当我们的程序中需要new一个对象的时候, 就会将这个对象放入到Eden区域, 当Eden区域中的对象越来越多, 直到满了, 这时放不下了, 就会触发字节码执行引擎发起GC操作. 第一次发起的GC, 将会看看哪些对象还活着, 哪些对象已经不用了, 活着的对象放入survivor中的一个区, 不再被引用的对象, 直接被回收了

如何判断对象是否还活着呢?
字节码执行引擎会去找很多gc root.

什么是gc root呢?
GC Root是一个对象, 以这个对象作为启动点,从这些节点开始向下搜索引用的对象, 找到的对象都标记为非垃圾对象, 其余未标记的对象都是垃圾对象.

GC Root根节点有哪些?
线程栈的局部变量, 方法区中的静态变量, 本地方法栈的变量等等。

垃圾收集的原理
在Math中, 我们看栈中main方法的局部变量表中的math变量. 方法区中的user变量. 他们都是GC Root根对象. 他们指向的是一块堆内存空间.
实质是, GC垃圾回收的过程, 就是寻找GC Root的过程. 从栈中找局部变量, 从方法区中找静态变量. 从GC Root出发, 找到所有的引用变量. 这些变量可能会引用其他的变量, 变量还会再引用其他变量. 直到不再引用其他变量为止, 以上这些都是非垃圾对象. 如果一个对象没有被任何对象引用, 那它就是垃圾对象。

垃圾对象最后就被回收, 非垃圾对象进入到Survivor的一个区域里面. 每次进入sruvivor区域,对象的分代年龄都会+1, 分代年龄保存在哪里呢?保存在对象头里面.

程序还在继续运行, 又会产生新的对象放入到Eden区, 当Eden区又被放满了, 就会再次出发GC, 此时会寻找Eden+sruvivor(一个区域)中的GC Root, 将其标记,
没有被引用的对象被回收, 其他被引用的对象会保存到另一个survivor区域. 分代年龄+1

这样运行, 直到分代年龄为15(默认15,可设置)时, 也就是GC发生了15次还活着的对象, 就会被放到老年代.

通常什么样的对象会被放到老年代呢?
静态变量引用的对象, 静态常量. 比如说: 对象池, 缓存对象, spring容器里面的对象,

Stop The World
在发生GC的时候, 会发生STW, Stop the world.

  1. 什么是Stop The World呢 ?
    举个例子:在一个电商网站,用户正在下单,这是由于内存满了,触发GC,这时候整个线程就会处于停滞状态。用户的感受就是一直在loading。。。。直到GC完毕,应用线程恢复工作。所以,Stop The World对我们的用户是有一定影响的。JVM调优主要的目的就是减少Full GC的次数和时间。minor GC也会stop the world,但是他的时间很短,所以我们重点调优还是在full gc

  2. 那么为什么一定要stop the world呢? 不STW不可以呢?
    回答这个问题, 我们可以使用假设法, 假设没有stop the world 会怎么样?

我们知道, 在垃圾回收之前, 要先找到GC Root, 然后标记是否被引用, 最终没有被引用的对象就是我们要回收的垃圾. 那就是没有对象引用他了.通常会回收这块内存空间地址 这个时候, 如果主线程也在运行, 刚好有一个变量存放在这个内存地址了, 而你并行的触发了GC, 这时候程序就发生混乱了.

这是一种情况,另一种是在触发GC的过程中,一部分变量正在被标记,而GC已经开始了,标记完以后,发现了垃圾,结果由于GC已经扫描完这里了,到这这一块垃圾没有被清理掉,要等待下一次垃圾回收来清理。

**========================================================================**

GC概述
垃圾回收是一种自动的存储管理机制。 当一些被占用的内存不再需要时,就应该予以释放,以让出空间,这种存储资源管理,称为垃圾回收(Garbage Collection)

接下来我从什么时候?对什么东西?做了怎么样的处理?三个方面来进行分析。

GC触发的条件有两种:
(1)程序调用System.gc时可以触发;
(2)系统自身来决定GC触发的时机。

程序计数器:线程私有。是一块较小的内存,是当前线程所执行的字节码的行号指示器。是Java虚拟机规范中唯一没有规定OOM(OutOfMemoryError)的区域。

Java栈:线程私有。生命周期和线程相同。是Java方法执行的内存模型。执行每个方法都会创建一个栈帧,用于存储局部变量和操作数(对象引用)。局部变量所需要的内存空间大小在编译期间完成分配。所以栈帧的大小不会改变。存在两种异常情况:若线程请求深度大于栈的深度,抛StackOverflowError。若栈在动态扩展时无法请求足够内存,抛OOM。

Java堆:所有线程共享。虚拟机启动时创建。存放对象实力和数组。所占内存最大。分为新生代(Young区),老年代(Old区)。新生代分Eden区,Servior区。Servior区又分为From space区和To Space区。Eden区和Servior区的内存比为8:1。 当扩展内存大于可用内存,抛OOM。
(1)新生代:所有新 new 出来的对象都会最先出现在新生代中,当新生代这部分内存满了之后,就会发起一次垃圾收集事件,这种发生在新生代的垃圾收集称为 Minor collections。 这种收集通常比较快,因为新生代的大部分对象都是需要回收的,那些暂时无法回收的就会被移动到老年代。全局暂停事件(Stop the World):所有小收集(minor garbage collections)都是全局暂停事件,也就是意味着所有的应用线程都需要停止,直到垃圾回收的操作全部完成。
(2)老年代:老年代用来存储那些存活时间较长的对象。 一般来说,我们会给新生代的对象限定一个存活的时间,当达到这个时间还没有被收集的时候就会被移动到老年代中。随着时间的推移,老年代也会被填满,最终导致老年代也要进行垃圾回收。这个事件叫做大收集(major garbage collection)。大收集也是全局暂停事件。通常大收集比较慢,因为它涉及到所有的存活对象。所以,对于对相应时间要求高的应用,应该将大收集最小化。此外,对于大收集,全局暂停事件的暂停时长会受到用于老年代的垃圾回收器的影响。

方法区:所有线程共享。用于存储已被虚拟机加载的类信息、常量、静态变量等数据。又称为非堆(NonHeap)。方法区又称“永久代”。GC很少在这个区域进行,但不代表不会回收。这个区域回收目标主要是针对常量池的回收和对类型的卸载。当内存申请大于实际可用内存,抛OOM。
(3)永久代:永久代存储了描述应用程序类和方法的源数据,JVM 运行应用程序的时候需要这些源数据。 永久代由 JVM 在运行时基于应用程序所使用的类产生。 此外,Java SE 类库的类和方法可能也存储在这里。如果 JVM 发现有些类不在被其他类所需要,同时其他类需要更多的空间,这时候这些类可能就会被垃圾回收。

本地方法栈:线程私有。与Java栈类似,但是不是为Java方法(字节码)服务,而是为本地非Java方法服务。也会抛StackOverflowError和OOM。

看完上面的定义之后,接下来再看看GC的系统触发条件,它的触发条件分为
Minor GC ,Full GC,那么就逐个解释一下。

Minor GC触发条件:当Eden区满时,触发Minor GC。

Full GC触发条件:

(1)调用System.gc时,系统建议执行Full GC,但是不必然执行

(2)老年代空间不足

(3)方法区空间不足

(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存

  1. 其次,当触发GC后,对什么东西进行的GC,那我们来看看下面这句话
    自动垃圾回收机制就是寻找Java堆中的对象,并对对象进行分类判别,寻找出正在使用的对象和已经不会使用的对象,然后把那些不会使用的对象从堆上清除。
    那那些会使用和不会使用的对象是什么呢?换句java语言就是,当一个对象没有被引用指向的时候就说明它就是不会使用的对象了。那么问题来了,怎么判断它到底有没有被引用指向呢?那么就有了这两种方式:引用计数法、可达性分析,那么我就逐个解释一下。

引用计数法

引用计数算法是垃圾收集器中的早期策略。 在这种方法中,堆中的每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个引用变量,该对象实例的引用计数设置为 1。当任何其它变量被赋值为这个对象的引用时,对象实例的引用计数加 1(a = b,则b引用的对象实例的计数器加1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数减 1。
特别地,当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器均减 1。 任何引用计数为0的对象实例可以被当作垃圾收集。
引用计数收集器可以很快的执行,并且交织在程序运行中,对程序需要不被长时间打断的实时环境比较有利,但其很难解决对象之间相互循环引用的问题。

可达性分析

可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,通过一系列的名为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。 当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。
在这里插入图片描述

说到GC roots(GC根),在JAVA语言中,可以当做GC roots的对象有以下几种:

 1、虚拟机栈中的引用的对象。
 2、方法区中的类静态属性引用的对象。
 3、方法区中的常量引用的对象。
 4、本地方法栈中JNI的引用的对象。
 第一和第四种都是指的方法的本地变量表,第二种表达的意思比较清晰,第三种主要指的是声明为final的常量值。

最后也就是对那些没有引用指向的对象做了怎么样的处理?呢?GC怎么回收这些没用的对象呢?
这时候就要看看垃圾回收算法。它总共有四种类型:

标记-清除算法
a.标记,也就是垃圾收集器会找出那些需要回收的对象所在的内存和不需要回收的对象所在的内存,并把它们标记出来,简单的说,也就是先找出垃圾在哪儿?

所有堆中的对象都会被扫描一遍,以此来确定回收的对象,所以这通常会是一个相对比较耗时的过程。
b.清除,垃圾收集器会清除掉上一步标记出来的那些需要回收的对象区域。

存在的问题就是碎片问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。它的缺点就是内存不连续。

复制算法
标记清除算法每次执行都需要对堆中全部对象扫面一遍效率不高,为解决效率问题,复制算法将内存按容量划分为大小相等的两块,每次只是用其中的一块。 当这一块使用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。 这样使得每次都对半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。紧接着它的缺点就来了会减少可用内存。
在这里插入图片描述

标记-整理算法
由于简单的标记清除可能会存在碎片的问题,所以又出现了压缩清除的方法,也就是先清除需要回收的对象,然后再对内存进行压缩操作,将内存分成可用和不可用两大部分。

分代收集算法
分代算法是上面几个算法的综合, Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。 而老年代中因为对象存活率较高、没有额外的空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法来回收。

垃圾收集器的分类

串行收集器(serial collector)只有一条GC线程,在运行是会暂停用户程序(stop the world)
并行收集器(parallel collector)有多条GC线程,也需要暂停用户程序
并发收集器(concurrent collector)有一条或多条GC线程,需要在部分阶段暂停用户程序

JAVA提供了多种类型的垃圾收集器, JVM 中的垃圾收集一般都采用“分代收集”,不同的堆内存区域采用不同的收集算法,主要目的就是为了增加吞吐量或降低停顿时间。

1.Serial (用于新生代,采用复制算法)(串行收集器)
2.Serial Old(用于老年代,采用标记整理算法)(串行收集器)
3.ParNew(用于新生代,采用复制算法)(并行收集器)
4.Parallel Old(用于老年代,采用标记整理算法)(并行收集器)
5.Parallel Scavenge(用新生代,采用复制算法)(并行收集器)
6.CMS(用于老年代,采用标记清除算法)(并发收集器)
7.G1(jdk1.7以后的版本推出的,维持高回收率,减少停顿,类似于concurrenthashmap的分区概念)
(java9默认gc算法是G1,且把CMS标记为废弃)

这里着重讲一下CMS,因为它是并发的,速度很快。

**CMS(Concurrent Mark Sweep)**收集器是一种以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。

基于“标记清除”算法,并发收集、低停顿,运作过程复杂,分4步:

1)初始标记:仅仅标记GC Roots能直接关联到的对象,速度快,但是需要“Stop The World”

2)并发标记:就是进行追踪引用链的过程,可以和用户线程并发执行。

3)重新标记:修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要“Stop The World”

4)并发清除:清除标记为可以回收对象,可以和用户线程并发执行

1)、3)会造成停顿,由于整个过程耗时最长的并发标记和并发清除都可以和用户线程一起工作,所以总体上来看,CMS收集器的内存回收过程和用户线程是并发执行的。

JVM的两种模式

client模式和server模式

默认是client模式,server模式启动较慢,但是长时间运行的话,运行会越来越快

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值