深入理解java虚拟机阅读笔记二

时间:2021-11-24 20:56:01

三 垃圾收集器和内存分配策略

3.1 垃圾收集(Garbage Collection,GC)和内存动态分配最早于1960年的MIT开发的Lisp语言中使用

需要解决的三个问题:

  • 回收哪些内存
  • 什么时候回收
  • 怎么回收

程序计数器,虚拟机栈,本地方法栈3个区域随线程生死,栈中的每个栈帧分配多少内存已经在类结构确定的时候已知,因此这几个区域的内存分配和回收都具确定性

而java堆和方法区则不同,同一个接口的不同实现类的内存可能不一样,我们只有在运行期才能知道创建哪些对象,当然这些对象的内存空间分配和回收也是动态的,垃圾收集器关注的是这部分内存

尽管目前内存动态分配和垃圾回收已经非常成熟在技术方面,但是因为需要排除各种内存溢出,内存泄漏问题,同时系统需要更高并发量而垃圾收集是提高这个并发量手段时候,我们需要对这个自动化的内存分配和内存回收进行监控调节优化

3.2 对象是否存活的判断算法

堆中存放着java中所有的对象实例,垃圾收集器回收堆中对象的时候,必须判断对象是否还存活,即对象是否可达。

判断对象是否存活的算法有以下两种

  • 引用计数器算法:道理很简单,给对象里面添加一个引用计数器,对象被引用一次,计数器+1,引用失效一次,计数器-1,当计数器为0的时候说明对象是不可能被使用的,著名的Python语言就使用的该算法。但是很可惜,java虚拟机并没有使用该算法管理内存,最主要原因:难以解决对象之间相互引用所造成的问题
  • 可达性分析算法 reachability analysis 通过一系列GC Roots的对象作为起始点,然后向下搜索,搜索走过的路径叫做引用链,当对象和GC root's没有任何引用链条相连的话,该对象即不可用,尽管下图中对象5,6,7互相关联,但是和根节点GC Roots没有连接,即不可达,所有5,6,7节点是判定可回收对象

java中,可作为GC Roots的对象有

  • 虚拟机栈引用的对象
  • 方法区中类静态成员引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

对象的引用分4种

  • 强引用:类似Object o = new Object();只要强引用存在,垃圾收集器永远不回收被引用对象
  • 软引用:描述还有用但是非必须的对象,如果系统将要内存溢出,则在第二次垃圾回收的时候回收这些对象,当回收以后还没足够内存,就抛出内存溢出exception
  • 弱引用:比软引用强度更低,只要收集器开始工作,在第二次垃圾回收之前就会将被弱引用关联的对象回收
  • 虚引用:最弱的引用关系,一个对象的生存时间和是否具有虚引用无关,即虚引用不存在了,对象仍然可能存在,当然,也无法通过虚引用取得一个对象实例,为对象设置虚引用关联的唯一目的就是该对象在被回收的时候可以收到一个系统通知

3.3 可达性分析算法种不可达的对象是否一定死亡?

不一定,当发现某个对象不可达的时候,会对他进行第一次标记,看是否执行finalize方法,当对象没有覆盖finalize方法或者finalize方法已经被vm调用,则没必要执行

如果执行,那么他会被加入F-queue队列,然后启动一个后台线程去执行他,不保证会执行完,加入队列以后过一会将会进行第二次标记,如果对象在finalize中成功拯救了自己,即将this赋值给某个成员变量,则第二次标记的时候将会把他移出即将回收的集合,如果没有逃脱,那么这个对象就真的被回收了,代码如下

public class JVMt {

	static JVMt jvmt = null;
	
	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		System.out.println("enter finalize method");
		JVMt.jvmt = this;
	}

	public static void main(String[] args) throws InterruptedException {
		jvmt = new JVMt();
		
		// 让它不可达
		jvmt = null;
		
		// 启动GC
		System.gc();
		
		// finalize优先级低,需要等待时间让他执行
		Thread.sleep(500);
		
		if(jvmt!=null) {
			System.out.println("I am stil alive");
		}else {
			System.out.println("I am dead");
		}
		
		// 让它不可达
		jvmt = null;
		
		// 启动GC
		System.gc();
		
		// finalize优先级低,需要等待时间让他执行
		Thread.sleep(500);
				
		if(jvmt!=null) {
			System.out.println("I am stil alive");
		}else {
			System.out.println("I am dead");
		}
	}
	
}

结果:

enter finalize method
I am stil alive
I am dead

3.4 GC对于方法区的回收

主要两部分

  • 废弃常量的回收:比如abc,常量池中没有任何一个String对象是abc

  • 类的回收:需要满足3个条件

    1. java堆中该类的实例已经被完全回收完

    2. 该类的类加载器已经被回收

    3. 该类的 Class对象没有引用,无法在任何地方通过反射访问该类

3.5 垃圾收集算法

  • 标记清理算法Mark-sweep:最基本的算法,标记需要回收的对象,然后标记完成后对这些对象进行统一回收,有两个缺点:

    1. 效率不高:标记的过程和清理的过程效率都不高

    2. 容易产生大量不连续的内存碎片,如果碎片太多,以后如果要分配较大对象的时候,使得无法找到足够大的连续内存而提前触发下一次GC

  • 复制算法Copying:目前商用虚拟机对于新生代收集最流行算法,原理是将堆中可用内存按照容量大小平均的1:1划分为2部分,每次只使用1部分,使用的这一部分中对象可分为3个组成块(可以回收,存活对象,空闲内存),当这一部分中的所有内存用完以后,即可以回收+存活对象所占内存=区间内存------------>就将这块还存活的对象复制到另一部分中,然后堆这部分中的可以回收进行GC

    优点:高效:只需移动指针,不需考虑内存碎片化等复杂情况

    确定:浪费内存,每次只是用一部分内存

    HotSpot虚拟机使用的GC算法就是这个,将堆分为3部分,Eden,from survivor,to survivor,三者的比例是8:1:1,其中Eden和from survivor主要用于存放对象,回收的时候就将这两块的对象一次性复制到to survivor内存中,如果to survivor空间容量不足以存放上一次新生代收集的存活对象的时候,这些对象直接通过分配担保机制进入老年代

  • 标记整理算法:考虑到复制算法,如果存活的对象数量庞大,在内存满以后进行复制效率无疑下降太多,同时如果不想浪费剩下的50%内存,就需要额外空间分配担保,以避免存活对象100%的极端情况,因此在老年代一般不用这种

    老年代选用标记整理算法,和标记清理算法很类似,就是所有对象标记完成以后先不清理,而是先将存活对象移动到边界整理,然后直接清理边界以外的内存

  • 分代收集算法Generational Collection:当前商业虚拟机最流行算法,根据对象的存活 周期不同分为新生代和老年代,新生代每次都有大量对象死亡,少量存活,采用复制算法,老年代因为对象存活率高,没有额外空间使用标记整理/标记清除算法