上一篇我们介绍了JVM,还有JVM中的内存结构。
当我们了解其中的内存之后,我们可能会有一点想法,我们的对象、相关类信息是存放在Java堆、方法区之中的。那我们的程序正在不断的new 对象、不断的loading Class。那么我们的JVM为什么没炸了(OOM),即使数量不会多到炸,但是我们不用的那些对象难道一直要仍在内存中?
cpp这样的语言,程序员是对自己的对象负责的,用完之后得了结了它。那Java呢?类似于以上的问题,是依靠JVM的垃圾回收机制去处理那些废弃的对象还有类信息的。
垃圾:废弃的对象、类信息、常量
回收:如何标记垃圾、如何清扫垃圾
垃圾的定义是比较巧妙的,JVM需要完完全全的确定我们不再使用了,才将其定义为垃圾。
《深入理解Java 虚拟机》一书中对什么是垃圾有一个有趣的标题— — 对象已死?
首先说 废弃的对象:
当一个对象,对我们来说是不可见、并且不可达的。那么如何判断 有这么两种方式或方法
1> 引用计数法:判断程序是否对这个对象还持有引用(这个对象是否还有使用的可能)
这个方法的优缺点显而易见,效率高,但是很有可能出现循环引用。
2>可达性分析:在说可达性之前,先说一个概念叫做GCRoot(既JVM 垃圾回收中判断对象是否可达的起点,是否仍被使用的起始节点),GCRoot常常有这么几个点:栈中所引用的对象(既被方法中直接使用的对象)、方法区中静态元素所引用的对象。
当我们有了GCRoot之后,我们便可以从它们出发去探索我们的对象了,那些不可达的也就可以被判断为应该不会被使用。也就一定程度上可以被标记待回收。
为了好理解,还是画个图吧
image.png
然后知道如何辨别和标记处垃圾之后,剩下的便是清理工作了。
JVM中的垃圾清扫或者收集算法有这么几种:标记-清除、标记-整理、复制、分代回收
标记-清除:
见明知义,先标记再清除(容易实现,但是效率较低、容易产生大量的内存碎片),放个图更好理解吧
image.png
针对内存碎片这个事儿,JVM也算是做出了不少策略。比如下面的,标记整理、复制
标记整理:
清扫掉废弃对象、并且整理,虽然没有内存碎片了,但是需要额外的整理工作,不仅需要标记存活的对象,还需要整理所有存活对象的引用地址。
放个图吧~
image.png
复制:其实是回收时将内存分为两部分,然后将存活的对象收集起来,清除掉待会收区域的垃圾就好啦。效率比标记整理要高,但是浪费了一部分空间。
image.png
JVM是可以这么玩复制算法的,新生代划分为三块区域(Eden区、2个Survival区,这其实就是种简单的面相程序员的一种抽象而已,具体低层实现比这个要复杂的多),其中默认Eden:Survival ==8:1,这个是要根据具体的对象的存活率来定的,为啥?别急,慢慢来。
首先新产生的对象是放在Eden中的,然后GC的时候是将存活对象取出来放到一个Survival(存活区)中,然后回收完成,继续向Eden中扔对象,下一次回收的时候是回收Eden及存放存活对象的那个Survival中,然后把这次的GC幸存者放在空的Survival中,然后回收完成,继续向Eden中扔对象,下一次回收的时候是回收Eden及存放存活对象的那个Survival中,然后把这次的GC幸存者放在空的Survival中(依次循环,我就不复制文字了,其中经历好多次的老对象到达年龄后是被请到老年区的。根据存活率控制空间比值是很重要哒,省的动不动向老年代抛对象)
image.png
然后是分代收集:就是按照对象年龄(经历的回收次数)作为主要标准,对Java Heap进行分区处理,之前作为简单例子说复制的时候提到过。
然后生命周期短的对象、生命周期长的对象,所采取的应该是不一样的,因为对象的存活率差太多。比如标记整理适合老年代,标记清除新生代也可以使用。这些在Java Heap 对象回收的时候其实是配合使用的。具体JVM使用哪种算法,其实是按照垃圾回收器来定的,感觉分代回收很高级的样子,其实在G1中已经弱化分代了。感觉标记-清除那么些毛病,为啥CMS使用它。每个具体的回收算法都有自己的优点和缺点,对这些算法进行合理的优化,相互配合在对象存活率不同的区域使用才能发挥更好的效果。具体的算法或者垃圾回收器的选择,要根据现实世界问题情形及相关物理硬件条件。
然后是对于废弃常量还有类的回收(方法区的回收)
就一点何为废弃的类:
1>该类所有的实例都已经被回收
2>加载该方法的ClassLoader已经被回收
3>该类对应的Class对象已无引用,并且无法通过反射访问。
但满足以上条件时,是允许进行回收的。
并且方法区也是会有溢出风险的,而且也会有废弃产生。所以也具有回收的意义,尤其是在大量反射使用的场景。
然后提几个中间没说到的东西:
1> Stop the world,在对象进行可达性分析的时候,会出现一次骤停(停止在使用对象的线程),然后标记线程启动,对对象进行标记。(并不是一下子让所有线程停止、而是让线程在安全点自行暂停)
2> finalize( ) ,这是每本书都不推荐使用的方法。它是在回收过程中自动执行的,并且仅执行一次。不同于cpp中的析构函数,这个不是来销毁对象的,据说是用来做一些对象销毁前必要的清理工作的。存在内存泄漏的风险。而且这个会让本来在回收流程中的对象复活(仅仅需要在方法中把对象的引用传给一个GCRoot可达的地方就好)。
3>Java 引用相关(传送门~)
4> 大对象直接进入老年代
下一篇 或者 后面几篇 说具体的垃圾回收器,以G1为主,CMS也会说一说。其他那些会大体提一提(较为简单,篇幅不大),然后是关于GC日志分析的。
具体怎么调优和选择,还有我们编码相关的注意事项会在调优那一篇章进行描述~