jvm-垃圾回收简单分析

版权声明: https://blog.csdn.net/qq_18298439/article/details/84633065

首先看看传统的垃圾回收算法

引用计数法和可达性分析法
       引用计数法:给每一个对象添加一个引用计数器,表示这个对象的引用个数,一旦引用个数为0的时候,说明该对象已经没用了,就可以回收了。缺点是需要额外的空间,额外的更新操作,且不能处理循环引用。一旦有对象互相引用你  即 A引用B,B引用A,如果使用引用计数法那这两个对象就永远不会回收造成内存泄漏。

       可达性分析法:将一系列GCRoots作为根集合,然后从这个集合出发,寻找所有可以被这个集合引用的对象,再把对象加入到这个集合里。最终没有在集合里面的对象就是要回收的对象。一般来讲,GCRoots包括以下几种

  • 已经加载的类的静态变量
  • 局部变量
  • JNI handles
  • 已启动且未停止的线程

       可达性分析法可以解决引用计数法不能解决的循环引用问题。但预想很美好,在实际处理中还是有不少问题的。譬如在多线程环环境里,在GC已经标记完成的情况下,线程将引用设置成null而造成的误报,将引用设置成未被访问的对象而造成的漏报。误报对于jvm来讲,最多损失一部分性能,对于其他处理没什么影响。而漏报则很苦逼了,因为回收了还被正在使用中的对象,那么运气好就线程崩溃,但很大的可能是直接导致jvm崩溃。

        为了解决这个问题,在传统的jvm的垃圾回收算法就引入了一种简单粗暴的方式 叫Stop-the-world。就是停止非垃圾回收线程的工作,直到垃圾回收完成。jvm是通过safepoint机制来实现Stop-the-world的功能的。即jvm收到Stop-the-world请求,会等待所有线程都到达了safepoint,才会允许Stop-the-world继续执行下去。

        safepoint的目的并不是要让非垃圾回收线程停止工作,而是让费垃圾回收线程处于一个稳定的执行状态,在这个执行状态里面,jvm的堆栈不会发生变化。使垃圾回收器能更安全的执行可达性分析法去GC。列举一下线程的几种状态  java程序通过JNI执行本地代码;解释执行字节码,执行即时编译器生成的机器码,以及线程阻塞。

        其中执行JNI本地代码需要通过JNI的API,如果这段代码不访问java对象,调用java方法,那jvm的堆栈并不会发生任何变化,所以这段本地代码就可作为同一个safepoint。即只需要在API的入口处进行safepoint检测,是否有其他线程停留在safepoint里。

        阻塞的线程处于jvm线程调度器里,所以也属于safepoint。其他几种则是运行时状态,需要虚拟机保证在可以预计的时间里进入safepoint,不然垃圾回收线程就要长时间的处于等待状态,从而变相的提高了Stop-the-world时间。

垃圾回收的方式
        使用垃圾护手算法标记完所有存活的对象后,就可以进行垃圾回收工作了。主流的基础回收算法有三种

  • 清除 :把死亡对象占据的内存标记为空闲内存,并记录在一个列表里面,当需要新建对象的时候,就从列表里寻找空闲内存,划分给新建的对象。 这种方法简单粗暴  ,但有两个厅严重的缺陷,
  1. 内存碎片会很多。因为jvm的堆中的对象必须是连续的,因此会出现总的空闲内存足够,在新分配的内存比空闲的连续内存大,导致无法分配的情况
  2. 分配效率低下。要给新对象分配内存,jvm就要遍历空闲列表。
  • 压缩:把存活的对象全都放在内存的起始位置,从而留下一片连续的内存空间。这种方式是可以解决碎片化的问题,但代价则是及其低下的性能
  • 复制:把内存分为两份,使用from和to指针来分别维护。并只用form指针来分配内存。当发生垃圾回收时,就把存活的对象复制到to指针指向的区域,并交换from和to指针的内存。有点是也可以解决内存碎片的问题,缺点是内存使用率极其低下。

       jvm将虚拟机划分为了两代,一个叫新生代,另一个叫老年代。新生代用来存储新建的对象,而当对象的年龄够大了,就会将它移动到老年代里。

       对于老年代而言,jvm猜测是大部分需要回收的都在新生代里回收了,而存活下来的对象有很大的概率可以继续存活下去。当然,当触发老年代回收时,意味着猜测错误或者堆内存耗尽,这时候就要进行full gc了。

       新生代被划分为Eden区和两个Survivor。默认情况下jvm采用的是动态分配的策略来调整Eden和Survivor区的比例。通常来讲,在你使用new指令生成对象时,就会在Eden区划分一块内存来存储对象。当然,堆对于线程来讲是共享的,因此划分内存是需要同步的。但如果真使用同步那就太影响效率了,根本跟不上现代处理器的速度。所以,jvm的解决办法是预先生气一批内存。当预先申请的内存用完了,那就再申请就完事了。这个呢就叫做Thread Local Allocation Buffer。对应参数是-XX:UseTLAB 默认是开启状态。线程申请内存后,会维护多个指针。其中重要的指针有两个。一个指向TLAB空余内存的起始位置,另一个指向TLAB的末尾。

       当Eced区的空间耗尽了,就会触发一次Minor GC,在这次GC存活下来的对象,就会被送到Survivor。新生代的Survivor有两个,可以用from和to来代表。其中  to指向的区域是空的。当发生Minor GC,Eden去和From区中存活下来的对象就会被复制到to区,然后在交换from和to的指针,以保证下次GC时,to区还是空的。

        jvm会记录Survivor区中的对象存活了几次GC,因为jvm中用来代表对象年龄的只有4位,所以一个对象被存活了15次GC后,该对象就会被一道老年代。设置次数的jvm参数是-XX:-MaxTenuringThreshold  。另外,如果单个Survivor的内存被占用了超过50%  那么年龄较大的对象也会被复制到老年代。

        Minor GC的好处一个是不用进行全表扫描,但有个问题就是老年代的对象可能会引用新生代的对象。如果按现在的逻辑就是说在标记存活对象的时候需要对老年代的进行扫描,这样一来不就也是进行了一次全表扫描?为了解决这个问题,HotSpot给的方案叫做Card Table 。Card Table 将整个堆划分成一个个大小为512字节的的card,且还会维护一张表,用来标识每张card。,这个标识位如果指向的Card Table存有指向新生代的引用,那么在Minor GC时,就需要把这张Card Table 里的对象加入GC Roots里面。当完成了所有的Card Table扫描时,就会把那些标识位全部清零。由于Minor GC后,存活对象会有复制,复制就需要更新指向这个对象的引用,因此在更新引用的时候,就可以设置引用所在Card Table的标识位,这样就能确保Card Table必定包含指向新生代的引用。

猜你喜欢

转载自blog.csdn.net/qq_18298439/article/details/84633065