认识GC

 
1、基础概念
  • 初步认知
  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时,确定是没有软引用存在的。
弱引用 弱引用与软引用相似,都是做为缓存来使用。但与软引用不一样,弱引用在进行垃圾回收时,是必定会被回收掉的,所以其生命周期只存在于一个垃圾回收周期内。
 强引用不用说,咱们系统通常在使用时都是用的强引用。而“软引用”和“弱引用”比较少见。他们通常被做为缓存使用,并且通常是在内存大小比较受限的状况下作为缓存。由于若是内存足够大的话,能够直接使用强引用做为缓存便可,同时可控性更高。于是,他们常见的是被使用在桌面应用系统的缓存。
 
2、垃圾回收算法
 
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%).
 
相关文章
相关标签/搜索