JVM学习(垃圾收集器和内存分配)

JVM学习(垃圾收集器和内存分配)

通过前面几篇对JVM内存以及垃圾收集的一点点初步介绍,相信看过的都对JVM有了一点认识,这一篇我们来主要说JVM中的垃圾收集器以及他们的作用和分代收集算法里的新生代老年代的内存分配。
垃圾收集器都有各自的特点,针对新生代或针对老年代,每个的设计都是别出一裁的,接下来让我们来认识认识他们。
在介绍垃圾收集器之前,要先为大家介绍一个知识点,叫安全点也有时候是安全区域

安全点/安全区域

众所周知,程序是在执行过程中也就是运行时发生垃圾收集的,那垃圾收集的时候会不会干扰到程序运行呢?这个时候就涉及到安全点和安全区域了,这是可以让线程暂时进行停留的地方,来完成这次短暂的GC垃圾收集。安全点的选定不能太少以至于每次要跑很久才到安全点,也不能太多以至于太频繁过分增大运行时的负荷。所以安全点的选定基本上是以“程序是否能长时间执行的特征”去选定的。
这里有两种办法来让线程“跑”到最近安全点停顿下来的方式。
抢先式中断,实现原理是不需要洗成的执行代码去主动配合,在GC发生时,将所有线程全部中断,如果发现中断的地方不在安全点上,则恢复线程,让它“跑”到最近的安全点上。现在几乎没有虚拟机采用抢先式中断来暂停线程实行GC。
主动式中断,主动式中断的思想是当GC需要中断线程时,不直接对贤臣操作,放置一个标志,各个线程执行时去主动轮询这个标志。发现中断标志为真的时候就是自己和中断标志的安全点是重合的,另外再加上创建对象需要分配内存的地方。不是的话则移动到最近的安全点上。
安全区域则是可以理解为扩大化的安全点,在一段代码片段中,在这个区域中开始的GC都是安全的。

1、垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。垃圾收集器分为新生代垃圾收集器和老年代垃圾收集器,下面给大家一一介绍。

1.1、Serial

Serial是最基本的、发展历史最悠久的收集器,看名字就能知道,它是一个单线程的收集器,它的新生代使用复制算法、老年代使用标记整理算法。它的单线程不仅仅是只会使用一个CPU或一条收集线程去完成垃圾收集,更重要的是他在收集垃圾时需要暂停其他的所有工作线程,也就是“Stop The World”(SWT),这点对用户来说是难以接受的,缺点就是这一点导致其他线程的暂停这带来的体验是很差的,而它的优点也就在于因为是单线程,所以效率高并且简单,只要它不是频繁发生,它的短暂的暂停时间控制的好,也是可以接收的。所有Serial收集器对于运行在Client模式下的虚拟机是一个不错的选择。

1.2、ParNew

ParNew收集器其实说白了就是Serial的多线程版,除了使用多线程进行垃圾收集之外与Serial并无不同,他与Serial的使用取决于你运行的CPU环境,ParNew在单环境下绝对不会有比Serial收集器有更好的效果,当然在通过超线程技术实现的两个CPU的环境都不能百分百的超越Serial,但是当使用的CPU数量增加的环境下,它对在GC时系统资源的有效利用还是很有好处的。

1.3、Parallel Seavenge

Parallel Seavenge收集器是一个新生代收集器,它也是使用赋值算法的收集器,也是并行的多线程收集器。它的特点在于这个收集器关注的点并不是与之前完全一样,它的目的是打到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU消耗总时间的壁纸,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)垃圾收集越短的同时运行代码时间越长,代表吞吐量越高。
高吞吐量意味着什么呢?高吞吐量以为着可以高效率使用CPU时间,尽快的完成程序的运算任务,主要适合在后台运算而不是需要太多交互的任务,Parallel Seavenge是称为吞吐量优先的收集器。

1.4、CMS

CMS收集器是一种以获取最短回收停顿时间为目标的收集器,目前很多Java应用集中在网站或者B/S系统的服务端上,这类应用需要重视响应速度,希望系统停顿时间越短越好,这时候CMS收集器就响应了这类用户的号召。CMS收集器是基于标记—清除算法的,它的运作可以分为四个步骤,初始标记、并发标记、重新标记、并发清除,初始标记、重新标记需要STW操作,但是这两个过程都很短暂,耗时最长的并发标记和并发清除都是由于是并发可以跟用户线程一起工作,所以停顿是体现不出来的。CMS是一款优秀的收集器,Sun公司的官方文档也称之为并发低停顿收集器。但是它还有一定的缺点①CMS收集器对CPU资源非常敏感,它由于占用了一部分线程在并发阶段,会导致应用程序变慢②CMS无法处理浮动垃圾,可能失败导致另一次FullGC的发生。③CMS基于标记清除,这会以为着有大量的空间碎片产生,当碎片过多时,这对内存分配讲师一个很大的问题,CMS收集器提供了一个-XX:UserCMSCompactAtFullCollection开关参数,用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程。

1.5、Serial Old

Serial Old是Serial收集器的老年代收集版本,同样也是一个单线程收集器,它主要有两种用途,一是在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备预案,这个之后在介绍,总的来说跟Serial单线程收集器是一样的,采用标记-整理算法。

1.6、Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本,原因是Parallel Scavenge是新生代收集器,在Parallel没有出现之前,一直没有合适的老年代收集器与他进行配合,导致Parallel新生代收集器只能选择SerialOld收集器别无选择,由于他们追求的不是同一种效率,这样导致组合起来的功能并不给力,所以这时候就出现了Parallel Old老年代收集器与它进行配合,这个就是名副其实的应用在注重吞吐量以及CPU资源敏感的场合的两个搭配使用的收集器了。

1.7、G1

G1是当前垃圾收集器技术发展的最前沿的成果之一,G1是一款面向服务端应用的垃圾收集器,G1它具有以下特点
①并行与并发:G1能充分利用多CPU、多核环境下的硬件优势
②分代收集:G1可以对新生代老年代都进行收集
③空间整合:G1在运作期间不会产生空间碎片,有利于程序长时间运行
④可预测的停顿:这是G1相对于CMS的一大优势,追求低停顿的G1还可以建立可预测的停顿时间模型
G1虽然听上去功能十分齐全并且优势也十分大,但是由于G1成熟版本的发布时间还很短,G1几乎可以说还没有经过什么考验,所以这里还没有什么可靠的实验报告,谁也不知道G1到底用起来是个什么感觉,有时效率高于收集器的搭配,有时却又不能超过两个好用收集器的搭配,所以G1的使用规则暂时还是一个未知数

下图是各收集器对应的位置以及如何哪些之间能搭配使用。
JVM学习(垃圾收集器和内存分配)
如上图所示,各个不同的收集器对应的新生代老年代收集器之间能搭配使用的是不一样的。
Serial新生代收集器可以搭配CMS和SerialOld老年代收集器
ParNew可以搭配CMS和Serial Old
ParallelScavenge可以搭配 Serial Old和ParallelOld
G1可以自己干活

2、内存分配

对象的内存分配,往大方向说,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配,少数情况也可能会直接分配在老年代中,分配的规则并不是百分百固定的,主要取决于使用的是哪一种垃圾收集器以及虚拟机中内存相关的参数的设置

2.1、对象优先在Eden区分配

大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机将发生一次minor GC,虚拟机提供了-XX:+PrintGCDetails来进行日志打印,由下图可以看出前面三次的6个MB大小的内存都分配在了Eden区域内,这时候内存区域还没满所以没有进行GC,最后一个4MB的直接跳过了新生代 因为大小已经接近于新生代eden区的一半并且也进入不了to和from,系统直接判断进入了老年代,也就是老年代的那40%
接下来我们将根据下图来对不同的分配情况进行讲解。
JVM学习(垃圾收集器和内存分配)
对应的虚拟机参数。JVM学习(垃圾收集器和内存分配)
附上 -XX:SurvivorRatio=2 (可以调整eden区与to和Survivor空间的比例 eden/to/survivor = 2:1:1)默认是8:1:1
有兴趣的小伙伴可以自己去打个应用自己调整参数来试试回收的情况。

2.2、大对象直接进入老年代

所谓大对象是需要大量连续内存空间的Java对象,大对象对虚拟机的内存分配来说就是一个坏消息,虽然遇到短命大对象是更加糟糕的情况。比如上图遇到的大对象,就直接判断进入了老年代,当对象接近或大于新生代内存区域的一半时,这时候会直接将大对象归入老年代,这样会造成的后果是什么呢?会可能这个是死得快的大对象,结果进入了老年代,不到FullGC它是不会去清理这个垃圾的,那么这样就加快了下一次的FullGC 这样就会很快就进入STW这个环节,对程序体验很不友好。假如想要解决这个问题,那么就要自己看着合理分配新生代的内存区域,不让大对象有这样直接进入的机会,合理分配新生代老年代的内存。

2.3、长期存活的对象进入老年代

当然假如不是大对象,那么怎么进入老年代呢?JVM给每个对象定义了一个对象年龄计数器,在每次经过一次Minor GC(也就是新生代的垃圾回收)年龄就会增加一岁,默认年龄是十五岁,当达到十五岁时,它就会在下一次MinorGC进入老年代。对于对象进入老年代的年龄的阈值我们可以通过 -XX:MaxTenuringThreshold=x 来设置。因为理解起来并不困难,我在这里就不详细用图举例了。

2.4、动态对象年龄判断

为了能更好的适应不同程序的内存状况,虚拟机并不是永远的要求对象的年龄都必须达到阈值才能晋升老年代,如果在Survivor空间中也就是新生代中默认的to或from空间,当这两个空间中有相同年龄的对象同时存在并且达到了内存大小的一半或以上,虚拟机就会把大于等于这个年龄的对象全部移入老年代,无须等到年龄的阈值。

2.5、空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么MinorGC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次MinorGC是有风险的;如果小于,或者HandlePromotionFailure设置不允许毛线,那这时就会进行一次FullGC
MinorGC 俗称 新生代垃圾回收动作,当新生代不能再进行内存分配时候发动,时间较短。FullGC是当老年代满了的时候对全局都进行垃圾回收,FullGC的同时会发动STW,所以尽量少发生FullGC。)


今天的垃圾收集器和内存分配的知识就讲到这了(大部分摘抄自书本),假如有哪里写的不对,请私信我更正错误。学习进步。