JVM学习(四)-垃圾回收器和内存分配
1. 判断对象存活
回收内存首先需要判断,那些内存需要回收。即需要判断那些对象还存活着,则这些是不需要被回收的。
(1) 引用计数法
原理:对象中添加一个引用计数器。被引用则累计。则计数器中数值大于0,则代表仍然被引用,不能被回收。
缺点:不能解决循环引用的情况。
(2) 可达性分析法
原理:从一些称为GCRoots的对象结点开始,向下开始搜索,根据是否能到达来判断对象的存活。其中,从GCRoots对象到目标对象之间的路径称为引用链。
GCRoots的对象包含以下几种:JAVA虚拟栈中的局部变量表中的引用对象、本地方法栈中引用的对象、方法区中静态属性引用的对象、方法区中,常量引用的对象。
2. 垃圾回收算法
(1) 标记-清除法
原理:首先先标记需要回收的对象,再标记完成后统一回收所有标记的对象。
缺点:效率【标记和回收的效率都不高】、空间【只是将需要回收的对象回收,没有将空间系统的整理,所以,会有很多的内存碎片,使用起来不好使用】
(2) 标记-复制法
原理:将内存分为两部分S1和S2,每次使用其中的一部分,比如S1。等S1空间满了的时候,将其中未被标记出来的【存活的对象】复制到另外一个内存空间S2中,回收整个S1。
缺点:内存使用只有原来的一半。如果存活率较高的情况下,这种算法效率会降低。
(3) 标记-整理法
原理:首先标记需要被删除的对象。然后将所有存活的对象向一侧移动。完成后清理掉端边以外的内存。
(4) 分代收集法
根据内存存活周期的不同,划分成不同的几块。JAVA堆一般分为新生代和老年代。根据各自年代的特点,选择适合的收集算法。比如:新生代存活率低,因此,新生代选择复制算法。老年代存活率较高,没有额外空间对其进行担保,必行采用“标记-清除”算法或者“标记-清理”算法进行回收。
① 新生代的回收算法
1) 新生代内存按照8:1:1的比例分为一个eden区和两个survivor (survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
2) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。
3) 若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
② 老年代的回收算大
1) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
2) 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
③ 永久代的回收算法
3. 垃圾回收器
Hotspot 所包含的垃圾回收器。
垃圾回收器从线程运行情况分类三种
① 串行回收:serial回收器,单线程回收。全程stw(Stop The World)。
② 并行回收:名称以parallel开头的回收器【parallel scavenge回收器、parallel old 回收器】,多线程回收。全程(Stop The World)。
③ 并发回收:CMS和G1,多线程分阶段回收。只有某阶段会(Stop The World)。
(1) Serial收集器(复制算法)
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。
(2) Serial Old收集器(标记-整理算法)
老年代单线程收集器,Serial收集器的老年代版本。
(3) ParNew收集器(停止-复制算法)
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
(4) Parallel Scavenge收集器(停止-复制算法)
并行收集器,追求高吞吐量,高效利用CPU。
(5) Parallel Old收集器(停止-复制算法)
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。
(6) CMS(Concurrent Mark Sweep)收集器(标记-清理算法)
特点:
A. 只会回收老年代和永久代,不会回收年轻代。
B. CMS是一种预处理垃圾回收器,它不能等到old内存用尽时回收。需要在内存用尽前完成回收。否则会导致并发回收失败。所以,CMS垃圾回收器在开始工作有一个触发的阈值,默认为老年代或者永久代的92%。
CMS基于“标记-清除”算法实现的。是最常用的垃圾回收器。CMS垃圾回收器的工作原理有7个步骤
1.初始化标记,会导致stw
2.并发标记,与用户线程同时运行。
3.预清理-与用户线程同时运行。
4.可被终止的预清理,与用户线程同时运行
5.重新标记,会导致swt
6.并发清除,与用户线程同时运行。
7.并发重置状态等待下一次CMS触发,与用户线程同时运行。
CMS运行流程如下:
4. GC的种类和触发
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。
(1) Scavenge GC
一般情况,在创建新对象的时候,对Eden申请空间失败的时候,会触发Scavenge GC。清除Eden中非存活对象,将存活对象移动到S1中。然后整理两个S区。这种GC对老年代不影响。对Eden GC的频率比较高,因此,需要效率比较高的算法【复制算法】。
(2) Full GC
对整个堆进行整理。包含新生代,老年代。所以比Scavenge GC消耗时间长。所以,需要尽可能少的较少FULL GC的次数。
触发场景:老年代被写满(Tenured)、持久代被写满(Perm)、System.GC被显示调用。
5. 内存分配和回收策略
对象分配的时候,需要选择一片未被使用的空间。同时两个线程需要创建对象。因此,可能产生并发冲突问题。解决的方案有两个。其一:分配空间的时候,保证同一时刻只有一个线程在申请空间。其二:每个线程在堆中获得一块区域。用来创建线程自己的对象。Thread Local Allocation Buffer,简称TLAB。
(1) 对象优先在Eden中分配
(2) 大对象直接进入老年代
设置参数,申请空间大于设定的阈值,则直接创建在老年代中。
(3) 长期存活的对象进入老年代
根据设置晋升老年代的年龄阈值,对象超过设置的阈值,则移动到老年代。
(4) 动态对象年龄判断
根据比例选择年龄阈值。相同年龄的对象大小总和大于等于整体S的一半,则当前年龄为阈值。大于或者等于这个年龄的对象可以移动到老年代,而不需要和年龄阈值比较。
(5) 空间分配担保
新生代无法分配内存的时候,把新生代对象转移到老年代中,然后把新对象放入腾空的新生代中。
如果老年代不能做担保【有连续的空间且大于S区中所有对象的总和--S可以全部移动过去】担保失败会引起Full GC。需要尽量避免频发Full GC。所以,需要了解空间分配担保。