垃圾回收介绍
1. 什么是垃圾回收?
程序在执行的过程中, 必然会向系统申请内存资源, 而已经没有用了的资源, 如果不回收掉, 最终就会导致内存溢出, 因此需要垃圾回收
2. C/ C++ 语言的垃圾回收
在C/ C++ 语言中,没有自动垃圾回收机制,是通过new 关键字申请内存资源,通过delete关键字释放内存资源。如果,程序员在某些位置没有写delete进行释放,那么申请的对象将一直占用内存资源,最终可能会导致内存溢出。
3. Java语言的垃圾回收
在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉的GC。
垃圾回收算法
常见的垃圾回收算法:
* 引用计数法
* 标记清除法
* 标记压缩法
* 复制算法
* 分代算法
1. 引用计数法的原理:
当new 一个对象A的时候, gc会为该对象分配一个引用计数器; 每当有一个其他的对象B持有对象A的引用的时候, 对象A的引用计数器的数值+ 1 ; 每当有一个其他的对象C放弃对对象A的引用的持有权, 对象A的引用计数器的数值- 1 ; 最终, 当执行gc的时候, gc会将所有的引用计数器数值为0 ( 即: 没有被任何对象引用) 的对象回收
1.1 引用计数法的优缺点:
优点:
- 实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0 ,就可以直接回收。
- 在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报outofmember 错误。
- 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象。
缺点:
- 每次对象被引用时,都需要去更新计数器,有一点时间开销。
- 浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。
- 无法解决循环引用问题。(最大的缺点)
2. 标记清除法的原理:
标记清除法分为2 个阶段, 第一阶段: 标记阶段, 当执行gc时, 引用中除了gc的线程, 其他所有的线程全部暂停活动, gc从一个root根节点出发, 遍历找出所有被root节点直接或间接引用了的对象, 并标记为1 , 其他没有被root根节点引用到的对象标记为0 ; 第二阶段: 清除阶段, 清除所有被标记为0 的对象, 然后重置标记为1 的对象为0 , 唤醒其他的线程
2.1 标记清除法的优缺点:
优点:
- 标记清除算法解决了引用计数算法中的循环引用的问题,没有从root节点引用的对象都会被回收。
缺点:
- 效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的。
- 通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存是不连贯的。
2.2 GC时, 停止其他线程的原因:
标记清除法执行GC时, 需要遍历操作, 如果此时出现了对象的新建或者销毁, 将会影响GC的判断; 比如当我们遍历list集合的时候, 如果同时进行增删操作, 会发生并发修改异常
3. 标记压缩法的原理:
基本同标记清除法, 当进入清除阶段时, 首先将存活的对象压缩到内存中的一端, 然后将非存活区的所有对象回收
3.1 标记压缩法的优缺点:
优点:
- 解决了标记清除法中造成的内存碎片化的问题
缺点:
- 对对象的压缩移动, 影响了性能
4. 复制算法的原理:
将内存一分为2 , 这两块内存区域完全相同, 通常称为from区和to区; GC只对From区的对象进行回收; 每次执行GC时, GC会将From区的存活对象copy到To区, 然后将From区的数据全部回收; 此时这一次的To区就成为了下一次GC的From区, 而这一次的From去就成为下一次GC的To区
4.1 复制算法的优缺点:
优点:
- 在垃圾对象多的情况下,效率较高
- 清理后,内存无碎片
缺点:
- 在垃圾对象少的情况下,不适用,如:老年代内存
- 分配的2 块内存空间,在同一个时刻,只能使用一半,内存使用率较低
5. 分代算法:
前面介绍了多种回收算法,每一种算法都有自己的优点也有缺点,谁都不能替代谁,所以根据垃圾回收对象的特点进行选择,才是明智的选择。
分代算法其实就是这样的,根据回收对象的特点进行选择,在jvm中,年轻代适合使用复制算法,老年代适合使用标记清除或标记压缩算法。
JVM垃圾回收算法
年轻代使用复制算法, 老年代使用标记清除或标记压缩算法
JVM垃圾回收算法原理:
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区, Survivor区“To”是空的; 当执行GC的时候, GC会将Eden区中存活的对象copy到SurvivorTo区; 同时, 对SurvivorFrom区的存活对象进行筛选, 当对象的年龄到达阀值, 就将这个对象放到老年代, 没达到阀值的就放入SurvivorTo区; 如果SurvivorTo区被填满, 就直接都放到老年代中, ( 可以通过- XX: MaxTenuringThreshold来设置阀值)
垃圾收集器
垃圾收集器是对垃圾收集算法的具体实现
1. CMS垃圾收集器:
CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记- 清除算法的垃圾回收器( 导致内存碎片化严重) ,该回收器是针对老年代垃圾回收的,通过参数- XX: + UseConcMarkSweepGC进行设置。
* 初始化标记: 从Old区的roots节点出发, 找出Old区的所有可达的对象, 此过程会stw
* 并发标记: 从初始化标记的可达对象中, 找出所有的存活对象; 并且, 在这段时间, 如果有新的对象进入Old区, 或者对象不再被roots节点引用, 会被标记为dirty, 此过程不会stw
* 预清理: 从被标记为dirty的对象中找出所有的roots可达的对象, 此过程依然会产生dirty对象
* 重新标记: 从Olds区的roots节点和dirty表出发, 再一次扫描出所有的存活的对象, 确保所有可达存活对象被标记
* 并发清理: 清除那些没有标记的对象并且回收空间
* 并发重置: 对标记的表和记录dirty的表做清零。并且重置CMS收集器的其他数据结构,等待下一次垃圾回收。
1.1 设置启动参数
- XX: + UseConcMarkSweepGC - XX: + PrintGCDetails - Xms16m - Xmx16m
2. G1垃圾收集器:
G1垃圾收集器分为三种模式: Young GC, Mixed GC, Full GC; G1垃圾收集器最大的特点便是取消了物理上将内存划分为年轻代, 老年代区域, 取而代之的是将内存分为了多个Region区域, 由这些Region区组成各个逻辑上的年轻代和老年代; 为了能在Young GC模式下快速找到roots, 每个Region初始化时还会初始化一个Remembered Set, 用于跟踪对象的引用, Region区被初始化后默认又会被封为多个512 kb的card, 因此Remembered Set中便记录某某region中的某某card引用了当前region区中的对象
通常情况下, 新new 出来的对象都会放到Eden区, 但是如果一个对象所占内存超过了分区容量的50 % , 就会被认为是一个大对象, 会直接分配为老年代; 但是有可能这个大对象只是一个临时对象, 因此G1垃圾收集器又有了一个Humongous区, 专门用来存储巨型对象
2.1 G1垃圾收集器的三种模式
* Young GC模式:
当逻辑上的Eden区不够用时触发, G1垃圾收集器会用复制算法将多个Eden区和Survivor From区的存活对象复制到一个新的Survivor To区, 如果Survivor To区的内存不够用, 就将部分数据直接晋升为逻辑老年区
* Mixed GC模式:
混合GC, 不是Full GC, 回收整个Young Region和部分Old Region, 默认情况下, 当Old Region的已用内存超过总内存的45 % 时, 触发Mixed GC; 主要分为两阶段, 全局并发标记( 基本同CMS垃圾收集器, 唯一区别是Mixed GC最终拷贝存活对象, 而CMS是直接清理) , 拷贝存活对象
2.2 G1收集器相关参数:
- - XX: + UseG1GC
- 使用 G1 垃圾收集器
- - XX: MaxGCPauseMillis
- 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到),默认值是 200 毫秒。
- - XX: G1HeapRegionSize= n
- 设置的 G1 区域的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。目标是根据最小的 Java 堆大小划分出约 2048 个区域。
- 默认是堆内存的1 / 2000 。
- - XX: ParallelGCThreads= n
- 设置STW工作线程数的值。将n的值设置为逻辑处理器的数量。n 的值与逻辑处理器( CPU) 的数量相同,最多为 8 。
- - XX: ConcGCThreads= n
- 设置并行标记的线程数。将 n 设置为并行垃圾回收线程数 ( ParallelGCThreads) 的 1 / 4 左右。
- - XX: InitiatingHeapOccupancyPercent= n
- 设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45 % 。
eg VM Options: XX: + UseG1GC - XX: MaxGCPauseMillis= 100 - XX: + PrintGCDetails - Xmx256m
2.3 G1收集器优化建议:
- 年轻代大小
- 避免使用 - Xmn 选项或 - XX: NewRatio 等其他相关选项显式设置年轻代大小。
- 固定年轻代的大小会覆盖暂停时间目标。
- 暂停时间目标不要太过严苛
- G1 GC 的吞吐量目标是 90 % 的应用程序时间和 10 % 的垃圾回收时间( 9 : 1 ) 。
- 评估 G1 GC 的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示您愿意承受更多的垃圾回收开销,而这会直接影响到吞吐量。
2.4 GC可视化工具:
GC Easy 可视化工具
GC Easy是一款在线的可视化工具,易用、功能强大,网站:http: / / gceasy. io/
- XX: + PrintGC 输出GC日志
- XX: + PrintGCDetails 输出GC的详细日志
- XX: + PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
- XX: + PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013 - 05 - 04 T21: 53 : 59.234 + 0800 )
- XX: + PrintHeapAtGC 在进行GC的前后打印出堆的信息
- Xloggc: . . /logs/ gc. log 日志文件的输出路径
eg: - XX: + UseG1GC - XX: MaxGCPauseMillis= 100 - Xmx256m - XX: + PrintGCDetails - XX: + PrintGCTimeStamps - XX: + PrintGCDateStamps - XX: + PrintHeapAtGC - Xloggc: F: / / test
循环引用代码
class TestA {
public TestB b;
}
class TestB {
public TestA a;
}
public class Main {
public static void main ( String[ ] args) {
A a = new A ( ) ;
B b = new B ( ) ;
a. b= b;
b. a= a;
a = null;
b = null;
}
}
说明:
A a = new A ( ) , 在堆内存中申请了一块内存空间A, 并将地址值保存到栈内存变量a中; B b = new B ( ) , 在堆内存中申请了一块内存空间B, 并将地址值保存到栈内存变量b中;
a. b= b, 将对象b的引用保存到堆内存的A区域中; b. a= a, 将对象a的引用保存到堆内存B中;
a = null, b = null; 对栈内存中的变量a, b赋值为null, 此时栈内存不再持有堆内存中A, B区域的引用; 正常来说, 那么它们的引用计数应该为0 , 但是它们的对内存中依然保持着各自的引用, 也就是引用计数器的值始终的变化为:
0 -- > 1 -- > 2 -- > 1
标记清除法原理图
标记压缩法原理图
CMS垃圾回收器原理图