前言
垃圾回收
1.垃圾回收器
2.垃圾回收算法
内存区域划分
内存模型
特别是内存可见性
总结
java虚拟机主要包含以上几个主题。这篇文章讲的是垃圾回收这一块。
回收哪一个对象?//怎么判断一个对象已经死了
两种方法
1.引用计数
有引用就加1。
没有引用就是0,表示可以回收。
2.root可达
有一个root指针执向对象引用链。
如果对象有引用链或者互相引用,但是没有root指针引用,那么照样会回收。
root可达引用链,要解决的问题,就是引用计数这种解决方案的引用可能是互相引用,比如两个对象使用完毕现在都要被回收,但是由于互相引用了所以计数永远不能为0,导致都不能被回收。
什么叫互相引用?
就是两个对象的数据,互相引用/指向了对方。具体见下面的示意图。
引用计数算法
1.对象引用和对象
2.对象引用和对象、对象的数据的引用和对象
对象引用示意图

public class ReferenceCountingGc {
Object data = null;
public static void main(String[] args) {
ReferenceCountingGc ref1 = new ReferenceCountingGc(); //对象计数器加1
ReferenceCountingGc ref2 = new ReferenceCountingGc();
ref1.data = ref2;
ref2.data = ref1; //对象计数器加1,现在的值是2
objA = null; //光是置空null第一层引用,不能释放对象,因为减1之后的值是1。如果想要释放,还需要把内层的引用也置为null,比如ref1.data=null。
objB = null;
}
}
root可达性分析
待补充
root是什么东西?
代码举例子
什么时候回收?//回收时机
空间不够就回收。这是本质原因,不然空间够还回收干嘛呢。
怎么回收?//回收算法
标记-删除标记对象
标记哪个对象?要被回收的对象。
缺点
删除之后,得到的内存空间是不连续的。
标记-复制存活对象到另外一个区
复制哪个对象?把未标记对象(幸存/存活对象)复制到另外一个区,然后一次性释放所有对象,主要的好处是没有内存碎片。
优缺点
优点
没有内存碎片,内存连续。
缺点
1.空间
额外多要一个区的空间,而且这个区只是用来专门用于回收垃圾,平时不回收的时候,是不能存储对象的。
2.速度
复制存活对象需要耗费资源。
最佳实践
目前JVM的新生代区,都是使用这种算法。为什么?下文都有详细的解释。
标记-整理存活对象到一端
解决空间一倍的问题。既可以得到连续空间,又不需要额外的一个区。
优点
不需要额外的空间,但是依然可以得到连续的内存。
缺点
但是仍然需要复制存活对象到一端,复制需要耗费资源。
最佳实践
目前JVM的老对象区,都是使用这种算法。为什么?下文都有详细的解释。
垃圾回收器
对垃圾回收算法的具体实现!
按是否并发执行划分
1.单线程清理-serial
内存回收的同一时间不能执行业务线程。且回收线程只有一个。
应用场景?适合客户端程序,因为都是点击用户界面且只有一个用户,所以偶尔有一点点停顿没有关系。但是WEB服务器程序就不能有停顿了,因为你一停顿,所有的请求都卡在那里积累起来了。
2.多线程清理-parallel
和单线程一样,垃圾回收线程执行回收对象时,必须暂停用户线程。
3.垃圾回收线程和用户线程同时运行-ConcurrentMarkSweep
可以一边垃圾回收,一边执行业务。
按新生代和老年代划分
就是上面的三种,再乘以二,就是六种垃圾回收器。
Object.finalize()方法
作用 Object.finalize()方法,如果自定义类覆盖了这个方法,那么在垃圾回收器回收该对象之前,会执行这个收尾方法。
分代垃圾回收器
内存划分
1.新对象
2.老对象
新对象还可以细分为
1.eden区
2.survive-from区
3.survive-to区
新对象,为什么要细分为三个?
因为这里用到了标记-复制算法。
标记-复制算法,就是两个区,一个放对象区,一个空闲区,先在放对象区标记要回收的对象,等到要回收的时候(即内存不够的时候),把存活对象(非标记对象)全部复制到空闲区,然后,一次性统一删除对象区的标记对象。删除的结果,就是以前的对象区变成了空闲区,以前的空闲区变成了对象区,也就是说,每次垃圾回收之后,两个区的角色要更换。
分代-新对象区之所以细分为三个区,道理同上,只不过稍微改进了一点点,就是多了一个区,具体执行的过程如下:
1.新对象放到eden区
2.第一次垃圾回收
复制存活对象到survive-to区;然后删除eden区;survive-from区暂时没有任何改变;交换survive-from和survive-to的角色。
最后的结果是,1.eden区重新全部空闲 2.survive-from变为survice-to 3.survive-to变为survive-from
3.第二次垃圾回收 与第一次垃圾回收的区别在哪里?唯一的区别是,现在的survive-from区有数据了,就是第一次垃圾回收之后的存活对象。所以,从第二次垃圾回收开始,是把eden和survive-from这两个区的存活对象复制到survive-to区,然后清理的也是eden和survive-from这两个区。最后,再次交换survive-from和survive-to的角色。
4.后面的垃圾回收和第二次一样。
既然分为新对象区和老对象区,那么怎么判断是否是老对象,什么时候把老对象放到老对象区?
1.看年龄大小
每次垃圾回收之后,存活对象的年龄加1,加到15(jvm默认值,可以配置)仍然是存活对象,那么就认为这个对象是老对象,然后就会放到老对象区。
2.看空间大小
默认把新对象放到eden区,eden区不够的话,会发生新对象区的垃圾回收(即minor GC)。由上文知道,minor GC是把eden区的存活对象复制到survive-to区,然后清理eden区。所以,如果清理之后,eden区仍然空间不够怎么办?就直接把对象放到老对象区。老对象区不够怎么办?会发生老对象区的垃圾回收(即Major/Full GC)。
存储对象的时候,大概是这么一个流程。
空间大小
1.新对象区的空间
比较小
2.老对象区的空间
比较大 //因为放的数据都是新对象区放不下的数据/大数据(比如,大字符串对象,大数组对象),还有很多的年龄大的对象。所以必须空间很大,才能放那么多的数据。另外,由于空间大,才能做到很久才做一次Full GC。为什么要很久做一次Full GC,因为Full GC其实就是老对象区GC,由于空间大 数据多,所以GC一次需要花费很长时间,这样会很严重的影响用户程序/线程的运行,会有停顿感觉,所以不能经常做Full GC。
而Minor GC不一样,由于空间小 数据少,所以GC速度快,也可以频繁GC。事实上,jvm确实也是自动的频繁执行Minor区,不然新对象区的空间一会儿就满了。反过来说,不管是Minor GC还是Full GC的时机也是因为空间快满了,才会做GC的。
总结一句话就是,垃圾回收是因为内存满了:
1.如果新对象区满
回收新对象区(Minor GC)。
2.如果老对象区满
回收老对象区(Full GC)。
为什么要分代?分代的好处?
3.4 分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。
一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点(不同的年代,有不同的特点)选择合适的垃圾收集算法。具体特点如下:
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法(整理存活对象到一端虽然也需要复制大量的对象,但是不需要额外的内存区,如果是内存不够的情况下,这种算法尤其重要)进行垃圾收集。
延伸面试问题: HotSpot 为什么要分为新生代和老年代?
根据上面的对分代收集算法的介绍回答。
四种引用类型的区别
1.强引用
没有被roots对象引用才回收
2.软引用
内存不足才会回收
3.弱引用
无论内存是否充足,都会被回收
4.虚引用
就是没有引用
总结
就像引用强度,逐渐减弱。
除了第一个强引用,是指new 对象这种引用,其他三种都是有专门的引用类来表示它。
参考
www.jianshu.com/p/8f5fa8288…
垃圾回收器-G1(Garbage First)
名字由来,优先回收效率最高的内存区域。
应用场景
和并发执行垃圾回收器一样,G1也是并发执行。
而且G1更加适合多cpu/内核、大容量内存的服务器机器。
G1是jdk7 hotspot才出来的。
引用-roots引用
什么是roots?
首先,roots是一个对象集合。那么具体是哪些对象呢?
比如
1.方法的局部变量和输入参数
2.还在运行的线程对象
所有正在运行的线程的栈上的引用变量。所有的全局变量。所有ClassLoader。。。。。。。。。and so on 请看下面。。实在是懒得翻译了。。。请点击链接 Help - Eclipse Platform 或者查看以下内容A garbage collection root is an object that is accessible from outside the heap. The following reasons make an object a GC root:
作者:Accelerator
链接:https://www.zhihu.com/question/50381439/answer/120846441
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1.System Class
----------Class loaded by bootstrap/system class loader. For example, everything from the rt.jar like java.util.* .
2.JNI Local
----------Local variable in native code, such as user defined JNI code or JVM internal code.
3.JNI Global
----------Global variable in native code, such as user defined JNI code or JVM internal code.
4.Thread Block
----------Object referred to from a currently active thread block.
Thread
----------A started, but not stopped, thread.
5.Busy Monitor
----------Everything that has called wait() or notify() or that is synchronized. For example, by calling synchronized(Object) or by entering a synchronized method. Static method means class, non-static method means object.
6.Java Local
----------Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread.
7.Native Stack
----------In or out parameters in native code, such as user defined JNI code or JVM internal code. This is often the case as many methods have native parts and the objects handled as method parameters become GC roots. For example, parameters used for file/network I/O methods or reflection.
7.Finalizable
----------An object which is in a queue awaiting its finalizer to be run.
8.Unfinalized
----------An object which has a finalize method, but has not been finalized and is not yet on the finalizer queue.
9.Unreachable
----------An object which is unreachable from any other root, but has been marked as a root by MAT to retain objects which otherwise would not be included in the analysis.
10.Java Stack Frame
----------A Java stack frame, holding local variables. Only generated when the dump is parsed with the preference set to treat Java stack frames as objects.
11.Unknown
----------An object of unknown root type. Some dumps, such as IBM Portable Heap Dump files, do not have root information. For these dumps the MAT parser marks objects which are have no inbound references or are unreachable from any other root as roots of this type. This ensures that MAT retains all the objects in the dump.
作者:Accelerator 链接:www.zhihu.com/question/50… 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
root引用到底又是怎么回事?
比如,方法的局部变量/输入数据引用了对象。
参考
www.zhihu.com/question/20… //陈树义的脉络讲的很清楚,为什么要学jvm,一步一步的大概原因讲清楚了
rednaxelafx.iteye.com/blog/362738 //R大,但是文章看不懂了。逻辑性、给菜鸟看这两方面确实不行。大家都说他牛逼,可是他写的文字,我一个都看不懂
怎么把垃圾回收算法和内存区域对应起来?
比如说,现在垃圾回收算法有三种。
那么,新生代和老年代分别使用的是哪一种算法?具体又是如何做的?
新对象区
新生代是标记-复制算法。
1.一般的标记-复制算法是两个区
一个区存放新的对象,然后要垃圾回收的时候把非标记对象复制到另外一个区,回收第一个区的标记对象。//注意:第一个区要标记的是要删除回收的对象,复制到第二个区的是非标记对象!
2.新生代基于标记-复制算法改进了一下,变成了三个区 其实就是把另外一个区变成了两个区,即survive1区和survive2区。但是这两个区的本质应该叫做survive_from区和survive_to区。为什么这么说?因为to区是用来存放非标记对象的,即回收的时候是把非标记对象复制到to区。第一次回收是把eden区非标记对象复制到to区,并且from区和to区的角色需要变换一下,回收的对象是eden区的标记对象;第二次是把eden区和from区的非标记对象复制到to区,回收的对象是eden区和from区的标记对象;后面的和第二次一样。//注意:survive这个名字也非常好,本质就是非标记对象,即把非标记对象复制到幸存区,即幸存区的对象是非标记对象——这些对象幸存了下来,没有被回收掉。
老对象区
老年代呢?使用的是标记-整理算法。为什么呢?一般来说,老对象区的内存比较多,不是更应该使用标记-复制算法吗?不是。原因是标记-复制算法是首先得有两个区,这是第一个原因;第二个原因是老年代对象的数据都是大数据,或者是年龄比较大的数据,总之是存活时间比较长的数据,一时半会都不会被回收,因为它们都有被引用,所以这些大量的大数据或老数据需要占用大量的内存空间,尽管老对象区的空间比较大,但是要存放的数据更多更大,所以本质上的问题永远都是内存不够用的,无论老年代内存相对新生代内存会大一点,但是仍然是不够用的。综上所述,所以采用标记-整理算法。