认识GC

 
一、基础概念
  • 初步认知
  1. 什么是java虚拟机,什么是java的虚拟机实例?java的虚拟机相当于我们的一个java类,而java虚拟机实例,相当我们new一个java类,不过java虚拟机不是通过new这个关键字而是通过java.exe或者javaw.exe来启动一个虚拟机实例。
  2. java的虚拟机种有两种线程,一种叫叫守护线程,一种叫非守护线程main函数就是个非守护线程,虚拟机的gc就是一个守护线程。java虚拟机的生命周期,当一个java应用main函数启动时虚拟机也同时被启动,而只有当在虚拟机实例中的所有非守护进程都结束时,java虚拟机实例才结束生命。
  3. java虚拟机的体系结构

 

  • 数据类型
Java虚拟机中,数据类型可以分为两类:基本类型和引用类型。基本类型的变量保存原始值,即:他代表的值就是数值本身;而引用类型的变量保存引用值。“引用值”代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置,所以修改对象的引用数据变量不会对对象本身数据有任何改变。
基本类型包括:byte、short、int、long、char、float、double、Boolean、returnAddress;
引用类型包括:类类型、接口类型、数组;
 

 

1)栈是运行时的单位,而堆是存储的单位。
2)栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
3)从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。
4)堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。
5)对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。
堆:存储new出来的对象
栈:基本数据类型和堆中对象的引用
 
  • 引用类型
对象引用类型分为:强引用、软引用、弱引用、虚引用
强引用:就是我们一般声明对象是时虚拟机生成的引用,强引用环境下,垃圾回收时需要严格判断当前对象是否被强引用,如果被强引用,则不会被垃圾回收
软引用软引用一般被做为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间;如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟机在发生OutOfMemory时,肯定是没有软引用存在的。
弱引用弱引用与软引用类似,都是作为缓存来使用。但与软引用不同,弱引用在进行垃圾回收时,是一定会被回收掉的,因此其生命周期只存在于一个垃圾回收周期内。
 强引用不用说,我们系统一般在使用时都是用的强引用。而“软引用”和“弱引用”比较少见。他们一般被作为缓存使用,而且一般是在内存大小比较受限的情况下做为缓存。因为如果内存足够大的话,可以直接使用强引用作为缓存即可,同时可控性更高。因而,他们常见的是被使用在桌面应用系统的缓存。
 
二、垃圾回收算法
 
1)按照基本回收策略分
(1)引用计数(Reference Counting):
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,引用收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
 
(2)标记-清除(Mark-Sweep)

 

此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。
 
(3)复制(Copying):

 

此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
 
(4)标记-整理(Mark-Compact):

 

此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
 
2)按分区对待的方式分
增量收集(Incremental Collecting)实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么原因JDK5.0中的收集器没有使用这种算法的。
分代收集(Generational Collecting)基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回收。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。
 
3)按系统线程分
(1)串行收集串行收集使用单线程处理所有垃圾回收工作,因为无需多线程交互,实现容易,而且效率比较高。但是,其局限性也比较明显,即无法使用多处理器的优势,所以此收集适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。
(2)并行收集并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上CPU数目越多,越能体现出并行收集器的优势。
(3)并发收集相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂停整个运行环境,而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且暂停时间会因为堆越大而越长。
 
  • 为什么要分代
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
 
  • 如何分代
  •  

如上图,虚拟机中的共划分为三个代:年轻代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)
其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
  1. 年轻代
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
  1. 年老代
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
  1. 持久代(Java 8改为元空间)
用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置。
随着JDK8的到来,JVM不再有PermGen。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中。原因:类的元数据信息转移到Metaspace的原因是PermGen很难调整。PermGen中类的元数据信息在每次FullGC的时候可能会被收集,但成绩很难令人满意。而且应该为PermGen分配多大的空间很难确定,因为PermSize的大小依赖于很多因素,比如JVM加载的class的总数,常量池的大小,方法的大小等。
 
由于类的元数据可以在本地内存(native memory)之外分配,所以其最大可利用空间是整个系统内存的可用空间。这样,你将不再会遇到OOM错误,溢出的内存会涌入到交换空间。最终用户可以为类元数据指定最大可利用的本地内存空间,JVM也可以增加本地内存空间来满足类元数据信息的存储。
 

 

  • GC有两种类型:Scavenge GC和Full GC
(1)Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。
 
(2)Full GC
    对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:
· 年老代(Tenured)被写满
· 持久代(Perm)被写满 
· System.gc()被显示调用 
·上一次GC之后Heap的各域分配策略动态变化
·内存空间碎片太多
 
  • 常见配置汇总
堆设置
  -Xms:初始堆大小
  -Xmx:最大堆大小
  -XX:NewSize=n:设置年轻代大小
  -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
  -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
  -XX:MaxPermSize=n:设置持久代大小
 
收集器设置
  -XX:+UseSerialGC:设置串行收集器
  -XX:+UseParallelGC:设置并行收集器
  -XX:+UseParalledlOldGC:设置并行年老代收集器
  -XX:+UseConcMarkSweepGC:设置并发收集器
 
垃圾回收统计信息
  -XX:+PrintGC
  -XX:+PrintGCDetails
  -XX:+PrintGCTimeStamps
  -Xloggc:filename
 
并行收集器设置
  -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
  -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
  -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
 
并发收集器设置
  -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
  -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
 
调优总结
年轻代大小选择
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
 
年老代大小选择
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
  1. 并发垃圾收集信息
  2. 持久代并发收集次数
  3. 传统GC信息
  4. 花在年轻代和年老代回收上的时间比例
减少年轻代和年老代花费的时间,一般会提高应用的效率
吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
 
较小堆引起的碎片问题
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
1. -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
2. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩
 
 
  • G1GC、ParallelGC和CMSGC区别
概念:G1 GC 是一种新的垃圾回收策略,从 JDK7 开始,主要适用于服务器端的JVM,和大内存的应用,其目标是达到类似 CMS 的高吞吐量。G1 中依然有分代管理的思想,主要采用分块管理的思想,通过将内存分为不超过2048个块,每块大小在 1M-32M 之间, Eden、Survivor space 和 年老代 都是一系列不连续的逻辑区域,颗粒度更细,选择最需要回收的段进行gc。
 
吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)
  1. Parallel收集器
采用多线程来通过扫描并压缩堆,
特点:停顿时间短,回收效率高,对吞吐量要求高。
适用场景:大型应用,科学计算,大规模数据采集等。
通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
 
2.CMS收集器
采用“标记-清除”算法实现,使用多线程的算法去扫描堆,对发现未使用的对象进行回收。
(1)初始标记
(2)并发标记
(3)并发预处理
(4)重新标记
(5)并发清除
(6)并发重置
特点:响应时间优先,减少垃圾收集停顿时间,把一次长暂停,改为2次短暂停。
适应场景:服务器、电信领域等。
通过JVM参数 -XX:+UseConcMarkSweepGC设置
 

 

 
3.G1收集器
在G1中,堆一整块内存空间,被分为多个连续的heap区(regions)。采用G1算法进行回收,吸收了CMS收集器特点。
特点:支持很大的堆,高吞吐量,减少gc耗时
年老代不存在清理阶段而是采用并发标记的方式,在标记过程中进行清理。
--支持多CPU和垃圾回收线程
--在主线程暂停的情况下,使用并行收集
--在主线程运行的情况下,使用并发收集
实时目标:可配置在N毫秒内最多只占用M毫秒的时间进行垃圾回收
通过JVM参数 –XX:+UseG1GC 使用G1垃圾回收器
-XX:MaxGCPauseMillis=200 - 设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal), JVM 会尽力 去达成这个目标. 所以有时候这个目标并不能达成. 默认值为 200 毫秒.
-XX:InitiatingHeapOccupancyPercent=45 - 启动并发GC时的堆内存占用百分比. G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。值为 0 则表示“一直执行GC循环)'. 默认值为 45 (例如, 全部的 45% 或者使用了45%).
 

 

 
posted @ 2018-04-13 13:40  磨牙君  阅读(201)  评论(0编辑  收藏  举报