Java GC(垃圾回收机制)

(PS:篇幅很长的,样式都会重新整一下,比如使用二级)

首先我们得知道,一个对象被判定为垃圾的标准是什么?

对于Java对象来说,当不被其他的对象引用的时候,就可以看作为垃圾

那么知道了标准,就需要制定算法来判定对象是不是垃圾,主要有两种,一个是引用计数算法,一个是可达性分析算法

判定算法

引用计数算法

先说一下引用计数算法,顾名思义,就是用过判断对象的引用数量来决定这个对象是不是可以被回收,在这种机制下,每一个对象实例都有一个引用计数器,被引用则就+1,完成引用就-1

在一个对象被创建时,如果这个对象实例分配给一个引用变量,那么这个对象的引用计数就会被设置为1,如果这个对象又被另一个对象引用,则计数器就会变成2

而这个对象的某个引用超过了生命周期或者被设置一个新的值时,这个对象的引用计数就会-1,比如在某个方法中定义了一个引用变量指向这个对象实例,那么在方法结束的时候,由于这个引用变量是局部变量,是存储在虚拟机栈上的,方法结束以后就会自己灭了自己,此时这个对象的引用计数器就会-1

那么在经过这一些七七八八的操作以后,任何引用计数为0的对象实例都可以被当做垃圾收集

使用引用计数器算法的优势:

因为我们只需要找到引用计数器为0的对象进行回收就可以,所以是可以很快执行的,执行效率很高,又因为这个可以在程序运行中进行寻找,在垃圾回收的时候几乎不会打断程序的执行,对于需要长时间执行的程序的情况比较有利,对程序执行来说,受影响较小

使用引用计数器算法的劣势:

由于实现过于简单,会出现无法检测出循环引用的情况,比如父对象有一个对子对象的引用,而子对象又反过来引用了父对象,这样他俩的引用计数很明显永远不会为0,导致内存泄漏

可达性分析算法(主流的算法)

通过判断对象的引用链是否可达来决定对象是不是可以被回收,其是通过离散数学中的图论引用的,当一个对象没有任何一个引用链相连,从图论上来说,这个对象就是不可达的,这个时候就证明了这个对象是不可用的,这个对象也就被标记为垃圾了

可以作为GC Root的对象:

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

方法区中的常量引用对象

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

本地方法栈中JNI(native方法)的引用对象

活跃线程的引用对象

垃圾回收算法

标记-清除算法

标记指的是从根集合进行扫描,对存活的对象进行标记,而清除就是对堆内存进行从头到尾的线性遍历,回收不可达的对象的内存,容易造成碎片化

复制算法

分为对象面空闲面对象在对象面上进行创建,当被定义为对象面的内存用完了以后,就将还存活着的对象被从对象面复制到空闲面,然后再将已经使用过内存空间一次性清理掉,也就是将对象面的所有对象内存全部清除,这种算法适用于对象存活率低的场景

复制算法解决了碎片化的问题,而且顺序分配内存,简单高效,但是不太适合于对象存活率高的场景

标记-整理算法

和标记-清除算法类似,但是不同的是在标记完了以后,是移动所有的存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收,其是在标记-清除算法的基础上进行了移动,成本更高,但是解决了内存碎片的问题

标记-整理算法的好处是避免了内存的不连续行,而且不用设置两块内存互换,这种适用于对象存活率高的场景

分代收集算法(主流)

这种就是组合各种算法的结果,可以按照对象生命周期的不同划分区域以采用不同的垃圾回收算法,可以提高JVM的垃圾回收执行效率

对于JDK6以及JDK7,对内存一般可以分为年轻代老年代以及永久代

对于JDK8以后的版本,就去掉了永久代,只留下了年轻代和老年代

分代收集算法的GC分类有两种

一种是Minor GC,其是发生在年轻代的垃圾收集动作,采用的是复制算法,年轻代是几乎所有Java对象出生的地方,即Java对象申请的内存以及存放都是在这里进行的,Java中的大部分对象通常不需要活很久,所以新生代是GC收集的频繁区域

另一种是Full GC,其是发生在老年代的垃圾收集动作,由于对于老年代的回收,一般伴随着年轻代的收集,因此这种也可以称为富GC,

年轻代

年轻代一般分为两个区域:Eden区(对象刚被创建出来的时候,其内存空间就被分配在此,当然,放不下的时候就会放在survivor区甚至是老年代中,默认为8/10的新生代区域大小)和两个survivor区(分别定义为from区和to区,且不是固定的,会随着垃圾回收的进行而相互转换),年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象

每次都是使用Eden区和一个survivor区,在垃圾回收的时候,将这两个区的存活对象一次性复制到另一个区中,最后清理掉Eden区和刚才使用过的survivor空间,当survivor不够用的时候,就需要老年代的空间担保了

年轻代垃圾回收的演示

第一次的时候,将Eden区的存活对象放到其中两个survivor区的任意一个区中,然后对Eden区进行清除,并且将这个存活的对象的年龄+1,这个s0就是from区

第二次即Eden区又一次被填满的时候,将Eden去的存活对象和先前保存了存活对象的survivor区的全部存活对象放在另一个survivor区中,同时,这些存活对象的年龄+1,此时s1从to区变成了from区,s0从from区变成了to区,然后对Eden区和之前使用过的survivor区进行清除

第三次,即Eden区又满了,此时假设s1中的一个对象无用了,这个时候又将Eden区的存活对象和s1中的存活对象拷贝到s0中,同时年龄+1,然后清除,可以通过-XX:maxtenuringthreshold来调整岁数限制

剩下的就和上面没差别了

那么在分代收集算法中,有哪些对象可以从年轻代晋升到老年代

第一种是经历一定minor次数依然存活的对象

第二种是survivor区中存放不下的对象

第三种就是新生成的大对象(-XX:+pretenuersizethreshold)

分代收集算法的常用的调优参数:

老年代

说完了年轻代,那自然要说老年代了

在年轻代中存活了很久的对象,其年龄超过了某个数值以后就会被放在老年代中,因此老年代中存放的都是一些生命周期比较长的对象

老年代因为都是一些生命周期较长的对象,所以没法用复制算法,一般使用标记-清除算法或者是标记-整理算法

在触发老年代的垃圾回收的时候,一般会伴随着对年轻代堆内存的回收,即对整个堆内存进行垃圾回收,这就是Full GC(富GC)

一般来说,Major GC和Full GC是等价的,Major GC以前是专门的老年代的GC,但是后面语义混乱了,所以要问清楚这个Major GC是指的老年代的GC还是其上面的Full GC

Full GC比Minor GC慢很多,但是其执行频率低

触发Full GC的条件有很多:

1.老年代空间不足

2.永久代空间不足(JDK8以前的版本)

3.CMS GC时出现了promotion failed,concurrent mode failure

4.Minor GC晋升到老年代的平均大小大于老年代的剩余空间

5.在程序中直接调用system.gc()

6.使用RMI来进行RPC或者管理的JDK应用,每小时执行1次Full GC

常见的垃圾收集器

需要先说两个知识点

stop-the-world

其意味着JVM由于要执行GC而停止了应用程序的执行,并且这种操作会在任何一种GC算法中发生,多数的GC优化就是通过减少stop-the-world发生的时间来提高程序的性能,从而使系统具有高吞吐(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),低停顿的特点

safepoint

其是垃圾收集器里的安全点,其就是分析过程中对象引用关系不会发生变化的点,程序只有到了安全点才会停顿下来

一般在方法调用,循环跳转,异常跳转等操作才会产生安全点,一旦GC发生,让所有的线程都跑到最近的安全点停顿下来,如果发现有线程不在安全点,就恢复线程,等其到了安全点再说

安全点的数量要适中

其次,需要注意的是,JVM的运行模式有两种:serverclient

client的启动较快(启动的JVM采用的是轻量级的虚拟机),但是在稳定的时候不如server的速度快

server的启动比较慢(启动的JVM采用的是重量的虚拟机,对程序的优化好),但是进入稳定期以后,server的速度比client快

使用Java -version就能看到当前的运行模式

垃圾收集器之间的联系

有连线就可以搭配使用

年轻代中常见的垃圾收集器

serial收集器(-XX:+UseSerialGC,复制算法)

其是采用复制算法的单线程垃圾收集器,也因为是单线程,所以在进行垃圾收集的时候,必须暂停所有的工作进程,直到收集结束,关注的是用户停顿时间

不过这个收集器依然是client模式下的默认年轻代收集器,因为其简单高效

ParNew收集器(-XX:+UseParNewGC,复制算法)

其是多线程的,其他的和serial收集器一模一样

在单核下,执行效率不如serial收集器,但是在多核下,执行会有优势

Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)

和parnew收集器类似,都是多线程的复制算法收集器,但是比起关注用户线程的停顿时间,更加关注系统的吞吐量

在多核下执行才有优势,是server模式下的默认的年轻代垃圾收集器

老年代常见的垃圾收集器

Serial old收集器(-XX:+UseSerialOldGC,标记-整理算法)

除了算法是使用的标记-整理算法,其余的和年轻代一样,就是serial的老年代版本,都是单线程收集,且在垃圾收集的时候,必须暂停所有的工作进程,是client模式下的默认的老年代收集器

Parallel Old收集器(-XX:+UseParallelOldGC,标记-整理算法)

其是使用多线程的标记-整理算法收集的,以吞吐量优先

CMS收集器(-XX:+UseConcMarkSweeoGC,标记-清除算法)

CMS收集器在老年代收集器中有着很重要的地位,其垃圾回收线程几乎可以和用户线程做到同时工作,如果程序对停顿比较敏感,而且能在程序运行的时候可以提供更大的内存和更大的CPU,也就是说更加强力的硬件,那么使用起来优势很大,还有就是在JVM中,如果有存活时间较长的对象,更加适合使用CMS

因为使用的是标记-清除算法所以会带来碎片化的问题

CMS的步骤可分为六步:

第一步,初始标记:需要stop-the-world

第二步,并发标记:并发追溯标记,程序不会停顿

第三步,并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象

第四步,重新标记:暂停虚拟机,扫描CMS堆中的剩余对象,需要stop-the-world

第五步,并发清理:清理垃圾对象,程序不会停顿

第六步,并发重置:重置CMS收集器的数据结构

G1(Garbage First)收集器(-XX:+UseG1GC,复制+标记-整理算法)

既可以用于年轻代,又可以用于老年代的收集器

G1收集器的特点

1.并行和并发

2.分代收集

3.空间整合

4.可预测的停顿

使用G1收集器的时候,Java堆的内存布局和其他的收集器有明显的差别,其会将整个Java堆内存划分成多个大小相等的region,虽然有年轻代和老年代的概念,但是年轻代和老年代不再是物理隔离的

一些面试中可能出现的问题

object的finalize()方法的作用是否与c++的析构函数作用相同?

与c++的析构函数不同,c++中的析构函数的调用时机是确定的,即对象离开作用域时,但是Java中的finalize具有不确定性

当垃圾回收器要寻找一个对象死亡的时候,至少要经过两次标记过程,如果对象在进行可达性分析后,发现没有和GC root相连接的引用,那样就会被第一次标记,并且判断是否执行finalize方法,如果对象覆盖finalize方法,且未被引用过,这个对象就会放置在F-Queue队列,并在稍后由一个虚拟机自动建立的一个低优先级的finalize线程,去执行触发finalize方法

因为优先级比较低,所以触发这个方法以后,没法保证这个能运行完,也就是说这个方法的执行随时可能被终止

finalize方法的作用是给予对象最后一次重生的机会

Java中的强引用,软引用,弱引用,虚引用都有什么用?

强引用

是最普遍的引用,例如object obj=new object(),这个obj就属于强引用了,如果一个对象具有强引用,当内存空间不足的时候,Java虚拟机宁可抛出outofmemoryerror终止程序,也不会回收具有强引用的对象

如果不使用这个对象了,就可以通过将对象设置为null来弱化引用,使其回收,或者是等待期超出对象的生命周期范围,让GC回收

软引用

软引用表示一个对象处在有用但非必须的状态,如果一个对象具有软引用,那么在内存空间充足的时候,GC就不会回收该对象,但是在内存空间不足的时候,GC就会回收该引用的对象的内存

软引用的作用是可以用来实现高速缓存

弱引用

弱引用是用来描述非必需的对象,类似于软引用,但是其强度比软引用更弱一些,弱引用的生命更短,GC在扫描的时候一旦发现只有弱引用关联的对象,就会将其回收,不过因为GC的优先级比较低,所以被回收的概率不大

这种适合用于引用偶尔被使用且不影响垃圾回收的对象

虚引用

和上面三种不同,虚引用不会决定对象的生命周期,如果一个对象仅有虚引用,那么这个对象在任何时候都可能被垃圾收集器回收,相当于没有引用一样,那虚引用能干啥呢?虚引用的作用主要是跟踪对象被垃圾收集器回收的活动,起到一个哨兵的作用

虚引用和其他引用的区别在于,虚引用必须和引用队列Reference Queue联合使用,在GC要回收的时候,会将虚引用加入到引用队列中,程序可以通过引用队列中是否加入了虚引用来了解被引用的对象是不是被GC回收,这就是哨兵作用

那么引用队列是啥呢?

引用队列和这几个都有联系,说是一个队列,但是其实没有实际的存储结构存储逻辑依赖于内部节点之间的关系来表达,Queue类似于链表的结构,这里的节点其实就是Reference本身,自己仅仅存储当前的head节点,后面的节点由每个Reference节点自己通过next来保持

此外引用队列是用来存储关联的且被GC的软引用,弱引用以及虚引用,如果在创建一个引用对象时,指定了一个引用队列,那么当引用对象指向对象到了合适的状态时,GC就会把引用对象本身添加到这个队列中,方便我们处理

引用队列的意义就是可以在外部对引用队列进行监控,如果有对象即将被回收,那么相应的引用队列的对象就会被放在queue中,我们就拿到reference中再做一些事情,如果不带这个queue的话,就只有不断的去轮寻reference对象,通过判断里面的get方法是否返回new,来判断是否被回收

总的来说

强引用>软引用>弱引用>虚引用

Java GC【笔记】的更多相关文章

  1. Java系列笔记(3) - Java 内存区域和GC机制

    目录 Java垃圾回收概况 Java内存区域 Java对象的访问方式 Java内存分配机制 Java GC机制 垃圾收集器 Java垃圾回收概况 Java GC(Garbage Collection, ...

  2. 【转载】Java系列笔记(3) - Java 内存区域和GC机制

    Java系列笔记(3) - Java 内存区域和GC机制 转载:原文地址http://www.cnblogs.com/zhguang/p/3257367.html 目录 Java垃圾回收概况 Java ...

  3. Java虚拟机笔记(二):GC垃圾回收和对象的引用

    为什么要了解GC 我们都知道Java开发者在开发过程中是不需要关心对象的回收的,因为Java虚拟机的原因,它会自动回收那些失效的垃圾对象.那我们为什么还要去了解GC和内存分配呢? 答案很简单:当我们需 ...

  4. 0030 Java学习笔记-面向对象-垃圾回收、(强、软、弱、虚)引用

    垃圾回收特点 垃圾:程序运行过程中,会为对象.数组等分配内存,运行过程中或结束后,这些对象可能就没用了,没有变量再指向它们,这时候,它们就成了垃圾,等着垃圾回收程序的回收再利用 Java的垃圾回收机制 ...

  5. 《Java学习笔记(第8版)》学习指导

    <Java学习笔记(第8版)>学习指导 目录 图书简况 学习指导 第一章 Java平台概论 第二章 从JDK到IDE 第三章 基础语法 第四章 认识对象 第五章 对象封装 第六章 继承与多 ...

  6. Java GC回收机制

    优秀Java程序员必须了解的GC工作原理 一个优秀的Java程序员必须了解GC的工作原理.如何优化GC的性能.如何与GC进行有限的交互,因为有一些应用程序对性能要求较高,例如嵌入式系统.实时系统等,只 ...

  7. Java系列笔记(4) - JVM监控与调优

    目录 参数设置收集器搭配启动内存分配监控工具和方法调优方法调优实例     光说不练假把式,学习Java GC机制的目的是为了实用,也就是为了在JVM出现问题时分析原因并解决之.通过学习,我觉得JVM ...

  8. Java系列笔记(6) - 并发(上)

    目录 1,基本概念 2,volatile 3,atom 4,ThreadLocal 5,CountDownLatch和CyclicBarrier 6,信号量 7,Condition 8,Exchang ...

  9. JAVA自学笔记14

    JAVA自学笔记14 1.正则表达式 1)是指一个用来描述或者匹配一系列符合某个句法规则的字符串的单个字符串.其实就是一种规则.有自己的特殊应用 2)组成规则: 规则字符在java.util.rege ...

随机推荐

  1. Springboot:单元测试日志打印@Slf4j 注解的使用方法

    当自己写日志的时候,肯定需要: private final Logger logger = LoggerFactory.getLogger(LoggerTest.class); 每次写新的类,就需要重 ...

  2. python base64(图片)编码

    参考:https://blog.csdn.net/Good_Luck_Kevin2018/article/details/80953312 通常会在网页中遇到用src="data:image ...

  3. 单选按钮(radio)的取值和点击事件

    笔记走一波:获取单选按钮(radio)的选中值,以及它的点击事件的实现 首先要引入Jquery <script type="text/javascript" src=&quo ...

  4. abp知识

    领域驱动开发的特点:1.分层更多,前期代码量大,后期维护方便2.业务进行了专业的领域划分,业务逻辑更加清晰,便于业务扩展.3.代码工程高内聚,更加精简.4.主要是解决复杂业务逻辑编写问题 为什么要使用 ...

  5. mongodb oplog详解和格式分析

    1. 基本概念 oplog使用固定大小集合记录了数据库中所有修改操作的操作日志(新增.修改和删除,无查询),mongodb收到修改请求后,先在主节点(Primary)执行请求,再把操作日志保存到opl ...

  6. 从新建文件夹开始构建ShadowPlay Engine游戏引擎(3)

    本篇序言 各位可能看到博文的名字换了,也就是引擎名字换了,其实是在下想到了一个更棒的名字:皮影戏(ShadowPlay),取这个名字的含义是因为,游戏中的角色(Puppet)不也是由于我们的操作而动起 ...

  7. 【多线程】C++ 互斥锁(mutex)的简单原理分析

    多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个或两个以上的程序.一般情况下,分为两种类型的多任务处理:基于进程和基于线程. 1)基于进程的多任务处理是程序的并发执行. 2)基于线程 ...

  8. 家庭账本开发day04

    对之前的构架进行修改,对成员类新加属性余额,在进行账单的新增时 ,对余额进行相应的修改.并且对账单类加入属性:id方便之后的查询和 删除操作

  9. Java基础00-Stream流34

    1. Stream流 Stream流 1.1 体验Stream流 代码示例: //需求:按照下面的要求完成集合的创建和遍历 public class StreamDemo { public stati ...

  10. CentOS更换网易yum源

    最新内容和地址参见http://mirrors.163.com/.help/centos.html 1 首先备份/etc/yum.repos.d/CentOS-Base.repo mv /etc/yu ...