垃圾回收
- 程序的运行必然需要申请内存资源,无效的对象资源如果不清除就会一直占用资源,
- 不再使用的对象并且一直在内存中存活,越来越多 就会导致内存溢出.
- 垃圾回收的目的让程序员专注于代码实现,不用过多考虑内存释放问题.
常见算法
引用计数法:
原理:
- 有一个对象A, 任何一个对象对A进行引用,那么A的引用计数器+1,引用失败-1,如果A计数器值为0说明没有引用,可以回收
优点:
- 实时性高,无需等待内存不够,运行时根据计数器直接进行操作
- 垃圾回收过程中,应用无需挂起,如过申请内存,内存不足,立刻报错(忘了是什么错)
- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象
缺点:
- 每次对象被引用,都需要去更新计数器,有一点时间开销,浪费CPU资源
- 无法解决循环引用问题
延伸:循环引用问题:
如果对象A持有一个对象B的引用,同时对象B也持有一个对象A的引用,且这两个引用之间没有其他对象或数据结构与之相连以打破这个循环,那么这两个对象就会形成一个循环引用。在这种情况下,即使这些对象不再被程序的其他部分使用,它们也会因为相互引用而无法被垃圾回收器正确识别为不再需要的对象,从而导致内存中的这些对象无法被回收,进而造成内存泄漏。
或者:在A类中创建一个b的引用,B类中创建一个a的引用,
在主方法中创建a,b对象然后 a,b=b; b.a=a; a=null; b=null;
虽然a和b都为null ,但是由于a和b存在循环引用,这样a和b永远都不会被回收
标记清除法:
原理:
- 将垃圾回收分为2个阶段,,分别为标记和清除
- 标记:从根节点开始标记引用的对象
- 清除:未标记引用的对象就是垃圾对象,可以被清理.
概述:程序运行期间所有对象的状态,标志位都为0,当有效内存空间耗尽,jvm停止程序运行,并开启GC线程,进行标记工作,根据搜索算法,所有从root(根)对象可到达的对象,都标记为存活对象,至此开始清除(不可到达的为垃圾对象)
优点:
- 解决了循环引用的问题,没有从root节点引用的对象都会被回收
缺点:
- 效率低,标记和清除是遍历所有对象,并且GC时需要停止程序,如果交互性要求高,这个就不行
- 标记清除出来的内存,碎片化较为严重,因为被回收的对象存在于各个角落,所有清出来的内存不是连贯的
标记压缩法:
原理:
- 在标记清除的算法上做优化, 不同的是在清除阶段,是将存活的对象压缩到内存的一端,然后清理边界以外的垃圾来解决碎片化的问题.
优缺点:
解决了碎片化的问题,但是 多了一个压缩步骤,就是移动内存位置,效率上会所有一定的影响
复制算法:
原理:
- 将内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间.
- 然后将该内存空间清空
- 交换两个内存的角色,完成GC
- 如果内存中的垃圾对象较多,需要复制的对象就较少,这个方法就合适,否则就不合适.
JVM中就是使用的复制算法
原理:
- 对象只会存在于Eden区,和名为From的Survivor区中,Survivor区中的To是空的
- 开始GC,Eden区中的对象全部转移到To,而From区中仍然存活的对象会根据他们的年龄(可以通过-XX:MaxTenuringThreshold来设置)被移动到To区或者年老代中
- 经过此次GCEden和From区被清空, 然后From和to交换角色
- 重复此过程,直到To填满了后,会将所有对象移动到老年代
优点:
- 在垃圾对象多的情况下,效率高 , 并且清理后 内存无碎片
缺点:
- 在垃圾对象少的情况下不适用, ,如老年代内存
- 分配的2块内存空间,在同一时刻,只能使用一半,内存使用率低
分代算法
原理:
- 根据回收对象的特点进行选择,在jvm中年轻代适合复制算法,老年代适合标记清除或标记压缩算法
垃圾收集器以及内存分配
- 除了垃圾回收的算法,在jvm中实现了多种垃圾收集器
- 串行垃圾收集器
- 并行垃圾收集器
- CMS(并发)垃圾收集器
- G1垃圾收集器
串行垃圾收集器
原理:
- 使用单线程进行垃圾回收,回收时只有一个线程在工作,并且java应用中所有线程都要暂停,等待垃圾回收完成,这种称为STW
- 一般在交互性较强的应用中不适用,如javaweb应用中不会采用该收集器
设置程序运行
-XX:+UseSerialGC 指定年轻代和老年代都使用串行垃圾收集器
-XX:+printGcDetails 打印垃圾回收的详细信息
GC日志解读
[GC (Allocation Failure)] [DefNew: 4416k->512k 0.00453433 secs]....
[GC (Allocation Failure)]...这个表示什么原因进行垃圾回收的
[GC (Allocation Failure)] [DefNew: ...] DefNew表示用的是串行垃圾收集器
4416k->512k: 表示年轻代GC前占内存 GC后占用内存
0.00453433 secs:表示GC所用时间,单位毫秒
[Full GC (Allocation Failure)] [Tenured: 12123k->234k(1203k),0.0001 secs]...
Full GC:表示内存空间全部进行GC
并行垃圾收集器:
原理:
- 在串行上做的改进,将单线程修改为多线程,缩短垃圾回收时间
- 也会暂停应用程序
parNew垃圾收集器
原理:
- 工作在年轻代上,只是将串行的收集器改为并行
- 通过 -XX:+UseparNewGC 参数设置年轻代使用parNew回收器,老年代使用的依然是串行收集器
ParallelGC垃圾收集器
- 工作机制和ParNew一样,只是新增了两个和系统,吞吐量相关的参数,让使用更加灵活
- -XX:+UseParallelGC
- 年轻代使用ParallelGC ,老年代使用串行GC
- -XX:+UseParallelOldGC
- 年轻代使用ParallelGC,老年代使用ParallelOldGC
- -XX:+MaxGCPauseMillis
- 设置最大的垃圾收集的停顿时间,单位为毫秒
- ParallelGC为了达到设置的停顿时间,可能会调整堆大小或其他参数,如果堆大小设置较小,GC会频繁,从而影响性能
- -XX:+GCTimeRatio
- 设置垃圾回收时间占程序运行时间的百分比,公式为1/(1+n)
- 值为1~100之间,默认99 ,表示回收时间不超过!%
- -XX:+UseAaptiveSizePolicy
- 自适应GC模式,回收器将自动调整新生代,老年代等参数,达到吞吐量,堆大小,停顿时间之间的平衡
- 一般用于手动调整参数,比较困难的场景,让收集器自动进行调整.
CMS垃圾收集器:
原理:
- 全称Concurrent Mark Sweep ,是一款并发的,使用标记-清除算法的垃圾回收器,是针对老年代的垃圾回收
- 通过参数-XX:+UseConMarkSweepGC进行设置的
- 主要解决上面停顿的问题,他在GC时不需要暂停
执行步骤:
- 初始化标记:标记root,会导致stw(程序暂停)
- 并发标记:与用户线程同时运行
- 预清理:与用户线程同时运行
- 重新标记:会导致stw(程序暂停)
- 并发清除:与用户线程同时运行
- 调整堆大小 , 设置cms在清理后进行内存压缩,目的是清理内存中的碎片
- 并发重置状态等待下次cms的触发,与用户线程同时运行.
G1垃圾收集器:
概述:
- 是在jdk1.7中正式使用的全新垃圾收集器, oracle官方计划在jdk1.9时 改为默认垃圾收集器,来代替CMS
- 它旨在提供可预测的停顿时间,同时实现高吞吐量。G1垃圾回收器主要通过两种方式进行垃圾回收:年轻代回收(Young GC)和混合回收(Mixed GC)。
- G1的设计原则就是简化JVM性能调优,只需要简单三步就能完成调优
- 开启 G1垃圾收集
- 设置堆的最大内存
- 设置最大的停顿时间
- G1中提供了三种模式垃圾回收模式,Young GC,Mixed GC , Full GC ,在不同条件下触发
原理:
- 最大的区别在于,取消了年轻代,老年代的物理划分,而是将堆划分为多个区域,这些区域包含了逻辑上的年轻代,老年代
- 不用对每个代进行设置,不用考虑每个代内存是否足够
- 在G1划分区域中,年轻代的收集依然采用暂停所有线程的方式,将存活对象拷贝代老年代或者Survivor空间,G1收集器通过将对象从一个分区域 复制到另外一个分区域,完成清理工作
- 在正常的处理过程中,G1完成了堆的压缩,这样就没有CMS内存碎片问题
- 还有同一个特殊区域,Humongous区域
- 如果一个对象展示用空间超过了分区容量50%以上,G1 收集就认为这是一个巨型对象
- 然后默认介质分配到老年代,如果是短期存在的,就会对收集器造成负面影响
- 而 Humongous区,就是用来存放巨型对象,如果一个H区不行,就会寻找连续的H分区来存储,所有有时候需要启动Full GC
Young GC(对年轻代进行GC)
概述:
- 主要对Eden区进行GC, 它在Eden空间耗尽时会被触发
- 数据移动到S区(空间不足,部分进年老代) ,s区移新s区,部分进老年代
- 最终Eden区空,GC停止
- 新创建的对象首先会被放置在Eden区. G1垃圾回收器通过监控年轻代的使用情况,当判断年轻代区域已满(超过60%容量)时 , 触发Young GC。
- 在执行Young GC时,G1首先会精确地标记出Eden和Survivor区域中的存活对象
- 根据预设的最大暂停时间和其他配置参数,G1选择某些区域,将存活对象复制到一个新的Survivor区(对象的年龄加1),并清空这些区域。
怎么做(Remembered Set (已记忆集合))
在GC年轻代的对象时,如何找到年轻代中的对象的根对象
根对象可能在年轻或者年老中,如果全量扫描查找费时费力,于是G1引进了RSet的概念.其作用是跟踪指向某个堆内的对象引用
解释:
- Region :表示分区
- Rset :表示
- 设置多个分区,对于每个分区(Region) 中都有一个或多个堆
- 当为每一个Region进行初始化时,会初始化一个Rset(一个set集合),这个集合用来记录并跟踪其他Region指向该Region中的对象的引用,每个Region按照512kb划分为多个Card(卡表)
- 所以每个Rset中记录的是xxRegion的xxCard
举例:现有Region1 Region2 Region3 与对应 Rset1 Rset2 Rset3
注:每个Region中又划分为多个区域,每个区域称为Card
一个卡表表示一个对象(有数据时) , 如果存在其他对象引用了别的Region中的对象,那么被引用的Region对应的Rset中就会记录xxxRegion 引用了 xxCrad.由此在进行清理之前寻找根节点就只需要扫描Rset集合来查询引用关系.
这样垃圾回收器就能够通过引用链准确地判断哪些对象需要回收,哪些对象由于被老年代引用而需要保留。
Mixed GC(混合回收)
概述:
- 越来越多对象晋升到年老代中,避免堆耗尽,从而触发混合回收
- 混合回收除了回收年轻代,还会回收一部分年老代
- 混合回收触发条件:
- 总堆占有率达到预设阈值时触发混合回收,默认45%
- -XX:InitiatingHeapOccupancyPercent=n)这种回收会处理所有年轻代和部分老年代的对象以及大对象区.混合回收采用复制算法来完成,确保高效的内存回收
步骤:
- 全局并发标记
- 初始标记
- 标记从根节点直接可达的对象,这个阶段会执行一次年轻代GC,会产生全局停顿
- 根区域扫描
- G1 GC 在初始化标记的存活区扫描对老年代的引用,并标记被引用的对象
- 该阶段与应用程序(非STW)同时进行,并且只有完成该阶段后,才能开启下一次stw年轻代回收
- G1 GC 在初始化标记的存活区扫描对老年代的引用,并标记被引用的对象
- 并发标记
- G1 GC在整个堆中查找可访问的对象,该阶段与程序同时进行,可以被STW年轻代回收中断
- 重新标记
- 该阶段是STW回收,因为程序在运行,针对上一次的标记进行修正
- 清除垃圾
- 清点和重置标记状态,该阶段会STW ,这个阶段并不会实际上去做垃圾的收集,等待拷贝存活对象(evacuation)阶段来回收
- 初始标记
- 拷贝存活对象
- 拷贝存活对象阶段是全暂停的,该阶段把一部分Region里的活对象拷贝到另一部分Region中,从而实现垃圾回收
G1收集器相关参数:
- -XX:+UserG1GC
- 使用G1垃圾收集器
- -XX:+MaxGCPauseMillis
- 设置期望达到的最大GC停顿时指标,(JVM会尽力去做,但不保证)默认值为200毫秒
- -XX:G1HeapRegionSize=n
- 设置的G1区域的大小,值是2的幂,范围是1M到32M之间,目标是根据最小的Java堆大小划分出约2048个区域
- 默认是堆内存的1/2000
- -XX:ParallelGCThreads=n
- 设置STW工作线程数的值,将n的值设置为逻辑处理器的数量,n的值用户逻辑处理器的数量相同,最多为8
- -XX:ConcGCThreads=n
- 设置触发标记周期的JAVA堆占用率阈值,默认占用率是整个java堆的45%
- 对于G1垃圾收集器优化
- 年轻代大小
- 避免使用-Xmn选项或-XX:NewRatio等其他 来设置年轻代大小
- 如果设置了固定年轻代的大小,会覆盖暂停时间目标
- 所以使用G1 尽量避免设置年轻代大小
- 暂停时间目标不要太苛刻
- G1 GC 的吞吐量目标是90% , 暂停时间目标不要太苛刻,否则就是表示您愿意承受更多的垃圾回收开销,而这个会直接影响到吞吐量
- 年轻代大小