垃圾回收针对的是Heap(堆区),因为运行时所有的对象实体都是存在这个区域的。
1、它的做法是为每个对象添加一个引用计数器,用来统计指向该对象的引用个数,当对象的引用个数为0的时候,就可以回收了。
2、引用计数法还有一个重大的漏洞,那便是无法处理循环引用对象,比方说A引用B,B引用A,但是A和B都没有其他引用,那么这个时候就回造成对象无法回收。
这个算法的实质在于将一系列 GC Roots 作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以被回收的。
那么,什么是GC roots呢?比如
1、Java 方法栈桢中的局部变量;
2、已加载类的静态变量;
3、已启动且未停止的 Java 线程。
为了避免在垃圾回收的过程中,由于还有应用线程在运行,导致内存状态的改变,而引起的错误回收。在 Java 虚拟机里,采用的是一种简单粗暴的方式,那便是 Stop-the-world,停止其他非垃垃圾回收线程的工作,直到完成垃圾回收。这也就造成了垃圾回收所谓的暂停时间(GC pause)。
Java 虚拟机中的 Stop-the-world 是通过安全点(safepoint)机制来实现的。当 Java 虚拟机收到 Stop-the-world 请求,它便会等待所有的线程都到达安全点,才允许请求 Stop-the-world 的线程进行独占的工作。
把死亡对象所占据的内存标记为空闲内存
1、造成内存碎片,内存不连续,对象内存重新分配的时候效率低下。
2、极端情况下,会造成总内存足够,但是对象无法分配内存。
清除死亡对象,把存活的对象聚集到内存区域的起始位置
1、压缩算法存在性能开销
2、对象的移动,涉及到内存地址的变化,需要更新引用地址。
把内存区域分为两等分,分别用两个指针 from 和 to 来维护,并且只是用 from 指针指向的内存区域来分配内存,当发生垃圾回收时,便把存活的对象复制到 to
指针指向的内存区域中,并且交换 from 指针和 to指针的内容。
1、能够解决内存碎片化的问题
2、堆空间的使用效率极其低下
Java 虚拟机将堆划分为新生代和老年代。其中,新生代又被划分为 Eden 区,以及两个大小相同的 Survivor 区。
通常来说,当我们调用new指令时,它会在Eden区中划出一块作为存储对象的内存。由于堆空间是线程共享的,因此直接在这里边划空间是需要进行同步的。
否则,将有可能出现两个对象共用一段内存的事故。
Java虚拟机的解决方法是TLAB技术(Thread Local Allocation Buffer,对应虚拟机参数-XX:+UseTLAB,默认开启)。
1、每个线程可以向Java虚拟机申请一段连续的内存,比如2048字节,作为线程私有的TLAB。
2、这个操作需要加锁,线程需要维护两个指针,一个指向TLAB中空余内存的起始位置,一个则指向TLAB末尾。
3、new指令,便可以直接通过指针加法(bump the pointer)来实现,即把指向空余内存位置的指针加上所请求的字节数。如果加法后空余内存指针的值仍小于或等于指向末尾的指针,则代表分配成功。否则,TLAB已经没有足够的空间来满足本次新建操作。这个时候,便需要当前线程重新申请新的TLAB。
1、当Eden区的空间耗尽,这个时候Java虚拟机便会触发一次Minor GC,来收集新生代的垃圾。存活下来的对象,则会被送到Survivor区。
2、新生代共有两个Survivor区,我们分别用from和to来指代。其中to指向的Survivior区是空的。当发生Minor GC时,Eden区和from指向的Survivor区中的存活对象会被复制到to指向的Survivor区中,然后交换from和to指针,以保证下一次Minor GC时,to指向的Survivor区还是空的。
3、Java虚拟机会记录Survivor区中的对象一共被来回复制了几次。如果一个对象被复制的次数为15(对应虚拟机参数-XX:+MaxTenuringThreshold),那么该对象将被晋升(promote)至老年代。
4、如果单个Survivor区已经被占用了50%(对应虚拟机参数-XX:TargetSurvivorRatio),那么较高复制次数的对象也会被晋升至老年代。
5、发生Minor GC时,应用了标记-复制算法,将Survivor区中的老存活对象晋升到老年代,然后将剩下的存活对象和Eden区的存活对象复制到另一个Survivor区中。理想情况下,Eden区中的对象基本都死亡了,那么需要复制的数据将非常少,因此采用这种标记-复制算法的效果极好。
6、Minor GC的另外一个好处是不用对整个堆进行垃圾回收。
但是,它却有一个问题,那就是老年代的对象可能引用新生代的对象。也就是说,在标记新生代存活对象的时候,我们需要扫描老年代中的对象。如果该老年代对象拥有对新生代对象的引用,那么这个老年代对象也会被作为GC Roots。
这样一来,又做了一次全堆扫描呢?
HotSpot给出的解决方案是一项叫做卡表(Card Table)的技术。
1、该技术将整个堆划分为一个个大小为512字节的卡,并且维护一个卡表,用来存储每张卡的一个标记。这个标识位代表对应的卡是否可能存有指向新生代对象的引用。如果可能存在,那么我们就认为这张卡是脏卡。
2、Minor GC的时候,可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到Minor GC的GC Roots里。当完成所有脏卡的扫描之后,Java虚拟机便会将所有脏卡的标识位清零。
3、Minor GC伴随着存活对象的复制,而复制需要更新指向该对象的引用。因此,在更新引用的同时,又会设置引用所在的卡的标识位。这个时候,我们可以确保脏卡中必定包含指向新生代对象的引用。
4、在Minor GC之前,我们并不能确保脏卡中包含指向新生代对象的引用。
新生代的垃圾回收器共有三个:
1、Serial,Parallel Scavenge和Parallel New。
2、这三个采用的都是标记-复制算法。
3、其中,Serial是一个单线程的,Parallel New可以看成Serial的多线程版本。Parallel Scavenge和Parallel New类似,但更加注重吞吐率。此外,Parallel Scavenge不能与CMS一起使用。
老年代的垃圾回收器也有三个:
1、Serial Old和Parallel Old,以及CMS。
2、Serial Old和Parallel Old都是标记-压缩算法。同样,前者是单线程的,而后者可以看成前者的多线程版本。
3、CMS采用的是标记-清除算法,并且是并发的。除了少数几个操作需要Stop-the-world之外,它可以在应用程序运行过程中进行垃圾回收。
原文:https://blog.51cto.com/janephp/2427547