Java垃圾回收机制

1.垃圾回收(Garbage Collection)
释放垃圾占用的空间,防止内存泄露
垃圾回收的原身:
1960 年,基于 MIT 的 Lisp 首先提出了垃圾回收的概念。
2. 如何定义垃圾
2.1引用计数算法
在对象头中分配一块空间保存该对象被引用的次数
如果该对象被其他对象引用,则引用次数加一,删除该对象的引用,则引用次数减一,当引用次数为0时,该对象需被回收。

String s = new String("testGC");
s = null;

引用计数算法将垃圾回收分摊到整个应用系统中,直到对堆中所有对象处理完毕。
该方法存在缺陷,由于两个对象互相引用,它们的引用次数不可能为0,也就无法回收。

public class ReferenceCounting{
		public Object instance;
		public ReferenceCounting(String name){
			this.name = name;
		}
	}	
	public static void test(){
		ReferenceCounting a = new ReferenceCounting("a");
		ReferenceCounting b = new ReferenceCounting("b");
			
		a.instance = b;
		b.instance  =a;
			
		a = nul;
		b = nll;
	}

2.2 可达性分析算法
2.2.1通过GCRoot作为起点,从这些起点向下搜索
当某个对象到GCRoot没有任何链相连时,则该对象不可用。
(GCRoot是由堆外指向堆内的引用)
GCRoot
2.2.2哪些内容属于GCRoot
Java内存区域
在 Java 语言中,可作为 GC Root 的对象包括以下 4 种:

 - 虚拟机栈(栈帧中的本地变量表)中引用的对象
 - 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
 - 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象

(1)虚拟机栈(栈帧中的本地变量表)中引用的对象

public class StackLocalParameter{
	public StackLocalParameter(String name){};
}

public static void testGC(){
	StackLocalParameter s = new StackLocalParameter("s");
	s = null;
}
此时的s即为GC Root,当被置空时,StackLocalParameter对象也就断掉了与GC Root的引用链,s将被回收。

(2)方法区中类静态属性引用的对象

public class MethdAreaStaticProperty{
	public static MethdAreaStaticProperty s;
	public MethdAreaStaticProperty(String name){};
}
public static void test(){
	MethdAreaStaticProperty m = new MethdAreaStaticProperty("property");
	m.s = new MethdAreaStaticProperty("paramater");
	
	m = null;
}
m 为 GC Root,m置为 null,经过 GC 后,m所指向的 property 对象由于无法与 GC Root 建立关系被回收;
而 s 作为类的静态属性,也属于 GC Root,parameter 对象依然与 GC root 建立着连接,所以此时 parameter 对象并不会被回收。

(3)方法区中常量引用的对象

public class MethodAreaFinalProperty{
	public static final MethodAreaFinalProperty s = new MethodAreaFinalProperty("name");
	public MethodAreaFinalProperty(String name){}
}

public static void testGC(){
	MethodAreaFinalProperty m = new MethodAreaFinalProperty("m");
	m = null;
}
s 即为方法区中的常量引用,也为 GC Root,m置为 null 后,final 对象也不会因没有与 GC Root 建立联系而被回收。

(4)本地方法栈中引用的对象
2.3Stop-the-world以及安全点

在 Java 虚拟机里,传统的垃圾回收算法采用的是一种简单粗暴的方式,那便是 Stop-the-world,停止其他非垃圾回收线程的工作,直到完成垃圾回收。这也就造成了垃圾回收所谓的暂停时间(GC pause)。

Java 虚拟机中的 Stop-the-world 是通过安全点(safepoint)机制来实现的。当 Java 虚拟机收到 Stop-the-world 请求,它便会等待所有的线程都到达安全点,才会停止所有线程,并允许请求Stop-the-world的那个线程进行独占的工作。

当然也并非蛮横的强制停止,毕竟多线程情况下,啥事都可能发生。安全点的初始目的并不是让其他线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java 虚拟机的堆栈不会发生变化。
3. 如何回收垃圾
常见的垃圾收集算法:
3.1标记清除算法
把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表(free list)之中。当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,并划分给新建的对象。

缺陷:(1)内存中会出现较多的内存碎片,不利于空间整合与有效利用;
      (2)分配效率低

标记清除方式
3.2复制算法
把内存区域分为两等分,分别用两个指针 from 和 to 来维护,并且只是用 from 指针指向的内存区域来分配内存。当发生垃圾回收时,便把存活的对象复制到 to 指针指向的内存区域中,并且交换 from 指针和 to 指针的内容。
复制方式

优点:能够解决内存碎片化的问题
缺点:堆空间的使用效率极其低下(毕竟分成两半,一次只使用一半)

3.3标记整理算法
标记过程仍然与标记清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。
标记整理方式

优点:能够解决内存碎片化的问题
缺点:压缩算法的性能开销大

3.4分析收集算法
融合上诉三种基本算法思想,针对不同情况使用不同方法处理。
对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用适当的收集算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或者标记-整理算法来进行回收。
Java 堆主要分为 2 个区域,年轻代与老年代,其中年轻代又分 Eden 区和 Survivor 区,其中 Survivor 区又分 From 和 To 2 个区。
内存模型(1)Eden区
有将近 98% 的对象是朝生夕死,所以针对这一现状,大多数情况下,对象会在新生代 Eden 区中进行分配。

当 Eden 区没有足够空间进行分配时,虚拟机会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。

通过 Minor GC 之后,Eden 会被清空,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。
(2)Survivor 区
Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,类似于我们交通灯中的黄灯。

Survivor 又分为 2 个区:
From 区
To 区

每次执行 Minor GC,会将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(如果 To 区不够,则直接进入 Old 区)。

①为啥需要?

不就是新生代到老年代么,直接 Eden 到 Old 不好了吗,为啥要这么复杂。

想想如果没有 Survivor 区,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代,老年代很快就会被填满。

而有很多对象虽然一次 Minor GC 没有消灭,但其实也并不会蹦跶多久,或许第二次,第三次就需要被清除。这时候移入老年区,很明显不是一个明智的决定。

所以,Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。

Survivor 的预筛选保证,只有经历 16 次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。

②为啥需要俩?

设置两个 Survivor 区最大的好处就是解决内存碎片化。我们先假设一下,Survivor 如果只有一个区域会怎样。

Minor GC 执行后,Eden 区被清空了,存活的对象放到了 Survivor 区,而之前 Survivor 区中的对象,可能也有一些是需要被清除的。

问题来了,这时候我们怎么清除它们?在这种场景下,我们只能标记清除,而我们知道标记清除最大的问题就是内存碎片,在新生代这种经常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。

因为 Survivor 有2个区域,所以每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。

第二次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。

这种机制最大的好处就是,整个过程中,永远有一个 Survivor space 是空的,另一个非空的 Survivor space 是无碎片的。

那么,Survivor 为什么不分更多块呢?比方说分成三个、四个、五个?显然,如果 Survivor 区再细分下去,每一块的空间就会比较小,容易导致 Survivor 区满,两块 Survivor 区可能是经过权衡之后的最佳方案。
(3)Old 区
老年代占据着 2/3 的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。

内存越大,STW 的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记-整理算法。

除了上述所说,在内存担保机制下,无法安置的对象会直接进到老年代,以下几种情况也会进入老年代。

①大对象

大对象指需要大量连续内存空间的对象,这部分对象不管是不是“朝生夕死”,都会直接进到老年代。

这样做主要是为了避免在 Eden 区及 2 个 Survivor 区之间发生大量的内存复制。当你的系统有非常多“朝生夕死”的大对象时,得注意了。

②长期存活对象

虚拟机给每个对象定义了一个对象年龄(Age)计数器。正常情况下对象会不断的在 Survivor 的 From 区与 To 区之间移动,对象在 Survivor 区中每经历一次 Minor GC,年龄就增加1岁。

当年龄增加到 15 岁时,这时候就会被转移到老年代。当然,这里的 15,JVM 也支持进行特殊设置。

③动态对象年龄

虚拟机并不重视要求对象年龄必须到 15 岁,才会放入老年区,如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进去老年区,无需等你“成年”。

这其实有点类似于负载均衡,轮询是负载均衡的一种,保证每台机器都分得同样的请求。

看似很均衡,但每台机的硬件不同,健康状况不同,我们还可以基于每台机接受的请求数,或每台机的响应时间等,来调整我们的负载均衡算法。
回收机制

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值