垃圾指的是在运⾏程序中没有任何指针(或引⽤)指向的对象,这个对象就是需要回收的垃圾。 如果不及时对内存中的垃圾进⾏清理,那么这些垃圾对象所占⽤的内存空间⼀直保留到应⽤程序结束,被保留的空间⽆法被其他对象使⽤。可能会导致内存溢出。
对于⾼级语⾔来说,如果不进⾏垃圾回收,因为不断分配内存⽽不进⾏回收,内存早晚会被消耗完。除了释放没有⽤的对象,垃圾回收也可以清除内存⾥的碎⽚,碎⽚整理将所占⽤的堆内存移动到堆的⼀端,便于JVM将整理出内存分配给新的对象。特别是⼤的对象,可能需要⼀块连续的⼤的内存空间。
垃圾回收(Garbage Collection)作为⼀⻔实⽤⽽⼜᯿要的技术,可以说拯救了⽆数苦于内存管理的程序员。尽管很多⼈认为,GC技术⾛进⼤众的视ᰀ,多是源于Java语⾔的崛起,但是GC技术本身却相当的古⽼。早在1960年,Lisp之⽗John McCarthy已经在其论⽂中发布了GC算法,Lisp语⾔也是第⼀个实现GC的语⾔。
在 GC 最开始设计时,⼈们在思考 GC 时就需要完成三件事情:
垃圾回收与“java⾯向对象编程”⼀样是java语⾔的特性之⼀;它与“ c/c++语⾔”最⼤区别是不⽤⼿动调⽤ free() 和 delete() 释放内存。GC 主要是处理 Java堆Heap ,也就是作⽤在 Java虚拟机 ⽤于存放对象实例的内存区域,(Java堆⼜称为GC堆)。JVM能够完成内存分配和内存回收,虽然降低了开发难度,避免了像C/C++直接操作内存的危险。但也正因为太过于依赖JVM去完成内存管理,导致很多Java开发者不再关⼼内存分配,导致很多程序低效、耗内存问题。因此开发者需要主动了解GC机制,充分利⽤有限的内存的程序,才能写出更⾼效的程序。
Stop-the-World,简称STW,指的是GC事件发⽣过程中,会产⽣应⽤程序的停顿。停顿是产⽣时整个应⽤程序线程会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。Stop-the-world意味着 JVM由于要执⾏GC⽽停⽌了应⽤程序(⽤户线程、⼯作线程)的执⾏,并且这种情形会在任何⼀种GC算法中发⽣。当Stop-the-world发⽣时,除了GC所需的线程以外,所有线程都处于等待状态直到GC任务完成。
STW事件和采⽤哪款GC⽆关,所有的GC都有这个事件。哪怕是G1也不能完全避免Stop-the-world情况发⽣,只能说垃圾回收器越来越优秀,回收效率越来越⾼,尽可能缩短了暂停时间。
STW是JVM在后台⾃动发起和⾃动完成的,在⽤户不可⻅的情况下,把⽤户正常的⼯作线程全部停掉。
随着应⽤程序越来越复杂,每次GC不能保证应⽤程序的正常运⾏。⽽经常造成STW的GC跟不上实际的需求,所以才需要不断对GC进⾏优化。事实上,GC优化很多时候就是指减少Stop-the-world发⽣的时间,从⽽使系统具有⾼吞吐 、低停顿的特点。
在操作系统中,是指⼀个时间段中有个⼏个程序都处于已启动运⾏到运⾏完毕之间,且这⼏个程序都是在同⼀个处理器上运⾏。
并发并不是真正意义上的"同时进⾏",只是CPU把⼀个时间段划分成⼏个时间⽚段(时间区间),然后在这⼏个时间区间之间来回切换,由于CPU处理的速度⾮常快,只要时间间隔处理得当,即可让⽤户感觉是多个应⽤程序是同时进⾏的。
当系统有⼀个以上CPU时,当⼀个CPU执⾏⼀个进程时,另外⼀个CPU可以执⾏另⼀个进程,两个进程互不抢占CPU资源,可以同时进⾏,我们称之为并⾏(Parallel);
其实决定并⾏的因素不是CPU的数ᰁ,⽽是CPU的核⼼数ᰁ,⽐如⼀个CPU多个核可以并⾏。
并发,指的是多个事情,在同⼀时间段内同时发⽣了。
并⾏,指的是多个事情,在同⼀时间点上同时发⽣了。
并发的多个任务之间是互相抢占资源的。
并⾏的多个任务之间是不互相抢占资源的。
只有在多个CPU或者⼀个CPU多核的情况中,才会发⽣并⾏。
否则,看似同时发⽣的事情,其实都是并发执⾏的。
在默认情况下,通过System.gc()或者Runtime getRuntime().gc()的调⽤,会显示触发Full GC,同时对⽼年代和新⽣代进⾏回收,尝试释放被丢弃对象的占⽤的内存。
然⽽System.gc()调⽤附带⼀个免责声明,⽆法保证对垃圾收集器的调⽤。
JVM实现者可以通过System.gc()调⽤在决定JVM的GC⾏为。⽽⼀般情况下,垃圾回收应该是⾃动进⾏的,⽆须⼿动触发,否则就太过于麻烦了。在⼀些特殊情况下,如我们正在编写⼀个性能基准,我们可以在运⾏之间调⽤System.gc()。
public class SystemGCTest {
protected void finalize() throws Throwable{
super.finalize();
System.out.println("SystemGCTest 重写finalize方法");
}
public static void main(String[] args) {
new SystemGCTest();
//提醒jvm的垃圾回收器执行gc,但是不确定是否马上执行gc,与Runtime.getRuntime().gc();作用一致
System.gc();
//强制调用使用引用的对象Finaliza方法
System.runFinalization();
}
}
程序执⾏时并⾮在所有的地⽅都能停顿下来开始执⾏GC,只有在特定的位置才能停顿下来开始GC,这些位置称为"安全点(safepoint)"。
Safe Point的选择很᯿要,如果太少可能导致GC等待的时间太⻓,如果太频繁可能导致运⾏时的性能问题。⼤部分指令的执⾏时间都⾮常短暂,通常会根据"是否具有让程序⻓时间执⾏的特征"为标准。
如何在GC发⽣时,检查所有线程都跑到最近的安全点停顿下来呢?
SafePoint机制保证了程序执⾏时,在不太⻓的时间内就会遇到可进⼊GC的Safepoint。但是程序"不执⾏"的时候呢?例如线程处于Sleep状态或Blocked状态,这时候线程⽆法响应JVM的中断请求,"⾛"到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(SafeRegion)来解决。
安全区域是在在⼀段代码⽚段中,对象的引⽤关系不会发⽣变化,在这个区域中的任何位置开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。
实际执⾏时:
JVM在进⾏回收时,是针对不同的内存区域进⾏回收的,⼤多数的回收指的是对新⽣代的回收。
针对HotSpot VM的实现,它⾥⾯的GC其实准确分类只有两⼤种:
Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有⼈说“major GC”的时候⼀定要问清楚他想要指的是上⾯的full GC还是old GC。
约定: 新⽣代/新⽣区/年轻代 养⽼区/⽼年区/⽼年代/年⽼代 永久区/永久代
从次数上讲:
最简单的分代式GC策略,按HotSpot VM的serial GC的实现来看,触发条件是:
⼀般情况下,所有新⽣成的对象⾸先都是放在新⽣代的。新⽣代内存按照 8:1:1 的⽐例分为⼀个eden区和两个survivor(from survivor,to survivor)区,⼤部分对象在Eden区中⽣成。
在进⾏垃圾回收时,先将eden区存活对象复制到from survivor区,然后清空eden区,当这个from survivor区也满了时,则将eden区和from survivor区存活对象复制到to survivor区,然后清空eden和这个from survivor区,此时from survivor区是空的,然后交换from survivor区和to survivor区的⻆⾊(即下次垃圾回收时会扫描Eden区和to survivor区),即保持from survivor区为空,如此往复。
特别地,当to survivor区也不⾜以存放eden区和from survivor区的存活对象时,就将存活对象直接存放到⽼年代。如果⽼年代也满了,就会触发⼀次FullGC,也就是新⽣代、⽼年代都进⾏回收。注意,新⽣代发⽣的GC叫做MinorGC,MinorGC发⽣频率⽐较⾼,不⼀定等 Eden区满了才触发。
Minor GC触发⽐较频繁,⼀般回收速度也是⽐较快的。Minor GC会引发STW,暂停⽤户线
程,等待垃圾回收完毕后,⽤户线程才会恢复。
由Eden区、from survivor区向to survivor区复制时,对象⼤⼩⼤于to survivor可⽤内存,则把该对象转存到⽼年代,会先尝试触发Minor GC,如果之后空间还是不⾜,则会触发Major GC。
如果Major GC后还是不⾜ 就会OOM。
发⽣Major GC,通常伴随Minor GC,但这并不是绝对的,Parallel Scavage这种收集器就有直接进⾏Major GC的策略过程。
说明:Major GC的速度⼀般会⽐Minor GC的速度慢10倍以上。