其余更多java基础文章:
java基础学习(目录)html
经过前一篇JVM学习(一)——内存结构对JVM内存结构的讲解。咱们知道程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,在这几个区域内就不须要过多考虑回收的问题,由于方法结束或者线程结束时,内存天然就跟随着回收了。栈中的栈帧随着方法的进入和退出就有条不紊的执行者出栈和入栈的操做,每个栈分配多少个内存基本都是在类结构肯定下来的时候就已经肯定了,这几个区域内存分配和回收都具备肯定性java
而堆和方法区则不一样,一个接口的实现是多种多样的,多个实现类须要的内存可能不同,一个方法中多个分支须要的内存也不同,咱们只能在程序运行的期间知道须要建立那些对象,分配多少内存,这部分的内存分配和回收都是动态的。这篇所讲的GC垃圾回收机制就是回收堆和方法区数据的机制。算法
给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任什么时候刻计数器为0的对象就是不可能再被使用的。
引用计数法有一个重大的漏洞,那即是没法处理循环引用对象。举个例子,假设对象 a 与 b 相互引用,除此以外没有其余引用指向 a 或者 b。在这种状况下,a 和 b 实际上已经死了,但因为它们的引用计数器皆不为 0,在引用计数法的心中,这两个对象还活 着。所以,这些循环引用对象所占据的空间将不可回收,从而形成了内存泄露。数组
经过一系列的成为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC ROOTS没有任何引用链相连时,则证实此对象时不可用的 Java语言中GC Roots的对象包括(包括但不限于)下面几种:安全
虽然可达性分析的算法自己很简明,可是在实践中仍是有很多其余问题须要解决的。
好比说,在多线程环境下,其余线程可能会更新已经访问过的对象中的引用,从而形成误报(将引 用设置为 null)或者漏报(将引用设置为未被访问过的对象)。多线程
误报并无什么伤害,Java 虚拟机至多损失了部分垃圾回收的机会。漏报则比较麻烦,由于垃圾回 收器可能回收事实上仍被引用的对象内存。一旦从原引用访问已经被回收了的对象,则颇有可能会 直接致使 Java 虚拟机崩溃。并发
怎么解决这个问题呢?在 Java 虚拟机里,传统的垃圾回收算法采用的是一种简单粗暴的方式,那便 是 Stop-the-world,中止其余非垃圾回收线程的工做,直到完成垃圾回收。这也就形成了垃圾回收 所谓的暂停时间(GC pause)。源码分析
Java 虚拟机中的 Stop-the-world 是经过安全点(safepoint)机制来实现的。当 Java 虚拟机收到 Stop-the-world 请求,它便会等待全部的线程都到达安全点,才容许请求 Stop-the-world 的线程 进行独占的工做。post
safepoint 安全点顾名思义是指一些特定的位置,当线程运行到这些位置时,线程的一些状态能够被肯定(the thread's representation of it's Java machine state is well described),好比记录OopMap的状态,从而肯定GC Root的信息,使JVM能够安全的进行一些操做,好比开始GC。性能
safepoint指的特定位置主要有:
之因此选择这些位置做为safepoint的插入点,主要的考虑是“避免程序长时间运行而不进入safepoint”,好比GC的时候必需要等到Java线程都进入到safepoint的时候VMThread才能开始执行GC。
常见的垃圾回收算法包括:标记-清除算法,复制算法,标记-整理算法,分代收集算法。
之因此说标记/清除算法是几种GC算法中最基础的算法,是由于后续的收集算法都是基于这种思路并对其不足进行改进而获得的。标记/清除算法的基本思想就跟它的名字同样,分为“标记”和“清除”两个阶段:首先标记出全部须要回收的对象,在标记完成后统一回收全部被标记的对象。
标记阶段:标记的过程其实就是前面介绍的可达性分析算法的过程,遍历全部的GC Roots对象,对从GC Roots对象可达的对象都打上一个标识,通常是在对象的header中,将其记录为可达对象;
清除阶段:清除的过程是对堆内存进行遍历,若是发现某个对象没有被标记为可达对象(经过读取对象header信息),则将其回收。
不足:
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另外一块上面,而后再把使用过的内存空间进行一次清理。
如今的商业虚拟机都采用这种收集算法来回收新生代,可是并非将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另外一块 Survivor 空间上,最后清理 Eden 和 使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。若是每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时须要依赖于老年代进行分配担保,也就是借用老年代的空间。
不足:
标记—整理算法和标记—清除算法同样,可是标记—整理算法不是把存活对象复制到另外一块内存,而是把存活对象往内存的一端移动,而后直接回收边界之外的内存,所以其不会产生内存碎片。标记—整理算法提升了内存的利用率,而且它适合在收集对象存活时间较长的老年代。
不足:
效率不高,不只要标记存活对象,还要整理全部存活对象的引用地址,在效率上不如复制算法。
分代回收算法其实是把复制算法和标记整理法的结合,并非真正一个新的算法,通常分为:老年代(Old Generation)和新生代(Young Generation),老年代就是不多垃圾须要进行回收的,新生代就是有不少的内存空间须要回收,因此不一样代就采用不一样的回收算法,以此来达到高效的回收算法。
新生代:因为新生代产生不少临时对象,大量对象须要进行回收,因此采用复制算法是最高效的。
老年代:回收的对象不多,都是通过几回标记后都不是可回收的状态转移到老年代的,因此仅有少许对象须要回收,故采用标记清除或者标记整理算法。
在 Java 中,堆被划分红两个不一样的区域:新生代 ( Young )、老年代 ( Old )。 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2( 该值能够经过参数 –XX:NewRatio 来指定 )
新生代 ( Young ) 又被划分为 三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 可以更好的管理堆内存中的对象,包括内存的分配以及回收。
主要是用来存放新生的对象。通常占据堆的1/3空间。因为频繁建立对象,因此新生代会频繁触发MinorGC进行垃圾回收。 新生代又分为 Eden区、ServivorFrom、ServivorTo三个区,默认比例8:1:1。
MinorGC的过程:MinorGC采用复制算法。首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(若是有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(若是ServicorTo不够位置了就放到老年区);而后,清空Eden和ServicorFrom中的对象;最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。
老年代的对象比较稳定,因此fullGC不会频繁执行。在进行fullGC前通常都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,致使空间不够用时才触发。当没法找到足够大的连续空间分配给新建立的较大对象时也会提早触发一次fullGC进行垃圾回收腾出空间。
fullGC根据不一样垃圾回收器采用标记—清除算法或标记-整理算法:首先扫描一次全部老年代,标记出存活的对象,而后回收没有标记的对象。fullGC的耗时比较长,由于要扫描再回收。fullGC会产生内存碎片,为了减小内存损耗,咱们通常须要进行整理或者标记出来方便下次直接分配。
当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。
大多数状况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
大对象即须要大量连续内存空间的Java对象,如长字符串及数组。常常出现大对象致使内存还有很多空间时就提早触发垃圾收集以获取足够的连续空间来安置他们。 虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。 这样作的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(新生代采用复制算法收集内存)。
虚拟机给每一个对象定义了一个对象年龄计数器,在对象在Eden建立并通过第一次Minor GC后仍然存活,并能被Suivivor容纳的话,将会被移动到Survivor空间,并对象年龄设置为1。每经历过Minor GC,年龄就增长1岁,当到必定程度(默认15岁,能够经过参数-XXMaxTenuringThreshold设置),就将会晋升年老代。
为了更好地适应不一样程序内存情况,虚拟机并不硬性要求对象年龄达到MaxTenuringThreshold才能晋升老年代,若是在Survivor空间中相同年龄全部对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就能够直接进入年老代。
在发生Minor GC以前,虚拟机会先检查年老代最大可用的连续空间是否大于新生代全部对象的总空间。
下面解释一下“冒险”是冒了什么风险,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来做为轮换备份,所以当出现大量对象在MinorGC后仍然存活的状况(最极端的状况就是内存回收后新生代中全部对象都存活),就须要老年代进行分配担保,把Survivor没法容纳的对象直接进入老年代。
与生活中的贷款担保相似,老年代要进行这样的担保,前提是老年代自己还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收以前是没法明确知道的,因此只好取以前每一次回收晋升到老年代对象容量的平均大小值做为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
取平均值进行比较其实仍然是一种动态几率的手段,也就是说,若是某次Minor GC存活后的对象突增,远远高于平均值的话,依然会致使担保失败(Handle Promotion Failure)。
若是出现了HandlePromotionFailure失败,那就只好在失败后从新发起一次Full GC。 虽然担保失败时绕的圈子是最大的,但大部分状况下都仍是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。
这是我在学习过程当中,发现的一个简单易懂的GC过程学习文章,经过图的方式,清晰明了。图解JVM GC过程
这几篇是我在学习过程当中,以为讲得不错的文章。
我将学习资料中的内容简单归纳为下表:
名字 | 特色 | 线程 | 回收区域 | 回收算法 |
---|---|---|---|---|
Serial收集器 | 最高的单线程收集效率 | 单线程 | 新生代 | 复制 |
ParNew收集器 | 能够认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。 | 多线程 | 新生代 | 复制 |
Parallel Scavenge收集器 | 关注系统吞吐量,目标是达到一个可控制的吞吐量,也常常称为“吞吐量优先”收集器 | 多线程 | 新生代 | 复制 |
Serial Old收集器 | 年老代收集器,能够和全部的年轻代收集器组合使用(Serial收集器的年老代版本) | 单线程 | 老年代 | 标记-整理 |
Parallel Old收集器 | Parallel Scavenge收集器的老年代版本,关注吞吐量,这个收集器是在JDK 1.6中才开始提供的。 | 多线程 | 老年代 | 标记-整理 |
CMS收集器 | 一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤为重视服务的响应速度,但愿系统停顿时间最短,以给用户带来较好的体验。CMS收集器存在3个缺点:1.对CPU资源敏感。通常并发执行的程序对CPU数量都是比较敏感的。2.没法处理浮动垃圾。在并发清理阶段用户线程还在执行,这时产生的垃圾没法清理。3.因为标记-清除算法产生大量的空间碎片 。此外除了CMS的GC,其实其余针对old gen的回收器都会在对old gen回收的同时回收young gen。 |
多线程并发收集 | 老年代 | 标记-清除 |
G1收集器 | 1.能够像CMS收集器同样,GC操做与应用的线程一块儿并发执行。2.紧凑的空闲内存区间且没有很长的GC停顿时间(标记整理算法,复制算法)。3.须要可预测的GC暂停耗时。4.不想牺牲太多吞吐量性能。5.启动后不须要请求更大的Java堆。 | 多线程并发收集 | 整个Java堆 | 标记-整理 |