目录
1. 什么是GC
2. GC主要针对区域
3. Java如何判断对象是否存活
4. GC垃圾回收机制
4.1 标记-清除法
4.2 标记-整理法
4.3 复制算法
4.4 分代收集算法
5. 内存分配策略及垃圾回收过程
5.1 为什么新生代要有survivor区?
5.2 为什么要有两个survivor区?
5.3 拓展
6. 总结
1. 什么是GC
GC全名Garbage Collection,垃圾收集,垃圾回收,这是Java区别于C/C++的一点,有自动管理内存和垃圾清扫的机制,简单地说,该机制会对JVM中的内存进行标记,确定哪块内存需要被回收,按照一定的规则,自动回收,防止JVM内存出现溢出和泄露,所以Java不需要我们管理内存,正常情况下,内存也不会溢出。
2. GC主要针对区域
垃圾回收主要针对的区域是堆内存、方法区(JDK1.7及以前),JDK1.8方法区取消,改为元数据区。
程序计数器、虚拟机栈、本地方法栈,这几块内存空间,GC是不考虑的,因为它们与线程共生死,线程结束,这里的内存也将自动被释放。
3. Java如何判断对象是否存活
判断对象是否存活有两种算法:引用计数法、可达性分析算法
引用计数法: 给对象添加引用计数器,当一个地方引用,则计数器加1,反之减1,当计数器的值为0,则该对象为可回收对象
可达性分析算法: 通过一系列称为GC Roots的对象作为起点开始向下搜索,经过的路径叫做引用链,如果一个对象到GC Roots没有任何引用链相连的话,该对象就为可回收对象。引用链说白了就是一个对象中引用另外一个对象。
在可达性分析算法中,判断对象真正死亡,需要进行二次标记,在第一次发现对象不可达后,会对对象进行一次标记,此时一次标记的对象会被放在等待回收的集合中,如果该对象所属的类重写了finalize()方法,且该方法没有被执行过,那么该对象会被取出放入专门的队列,顺序去执行finalize方法,如果被救活,则移除被回收内存,如果没被救活,则等待二次标记,被标记2此以后,就等待回收。如果第一次不可达标记后没能调用finalize()方法,那么该对象也直接被二次标记等待回收,finalize()方法可以理解为拯救对象的方法,在垃圾回收过程中,只会被执行一次。
GC Roots的对象可以是:
虚拟机栈(栈帧中的本地变量表)中引用的对象(不理解的需要深度学习一下JVM内存结构)
方法区/元数据区中类的静态变量引用的对象
类的静态常量引用的对象
本地方法栈中本地方法引用的对象
4. GC垃圾回收机制
4.1 标记-清除法
该回收机制即为表面意思,先标记,然后把标记的内存回收,即释放。
该回收算法容易产生很多内存碎片,导致大的对象没有足够的连续内存,会触发GC。举例,该网格图为JVM堆内存,每块网格为一块内存,假设白色的格是通过标记清除法释放的内存
这么多被释放的且不连续的内存,称之为内存碎片,若此时我想创建一个需要三格连续内存的大对象,则依旧内存不够,从而再次触发GC,下次可能还会出现这种情况。
4.2 标记-整理法
标记整理法,与标记清除法一样,只不过是多了一个整理的步骤,在标记清除完之后,会将内存进行归整,使内存碎片能够连续,避免了标记清除回收算法的弊端
4.3 复制算法
复制算法,需要将内存分为两部分,每次只用一部分,例如两块内存为A、B,当A内存满了以后,将存活的对象规整地复制到B中,然后将A中全部清除,这种算法效率比较低。
4.4 分代收集算法
根据对象存活周期的不同,将内存划分为几个区域(年轻代,老年代,这个结构是不是特别熟悉呢?不熟悉也没关系,哈哈哈),然后根据每个年代的特点,采用最适合的收集算法,例如年轻代,对象死亡率比较高,需要回收的内存比较多,所以可以用复制算法,老年代,对象死亡率比较低,如果采用复制算法,需要复制的存活对象比较多,效率会比较慢,况且,也没有那么多额外的空间进行复制,一般都采用标记清除法或者标记整理法。
5. 内存分配策略及垃圾回收过程
Hot Spot JVM堆内存被分为新生代和老年代,老年代又被分成三部分,一个Eden区,两个Survivor区,新创建的对象一般都在Eden区,(拓展小知识: Eden翻译过来是伊甸园,是传说中亚当夏娃造人的地方,所以新创建的对象来这里,哈哈哈哈),触发新生代GC一般叫做Minor GC或Young GC,触发老年代GC一般叫做Full GC,下面为图示(图是别人的,很久之前存下来的了,忘记是从哪篇博客看的了)
偷偷告诉你,对象也是有年龄的,新生对象在新生代每熬过一次Minor GC,年龄就会涨一岁,当对象到达一定岁数后(默认15岁),便会被移到老年代。
那么接下来有两个问题:为什么新生代中要有survivor区?为什么新生代要有两个survivor区?
5.1 为什么新生代要有survivor区?
为了防止频繁触发Full GC。Eden是用来放新生对象的,必然会很快存满,如果没有survivor,那么意味着每触发一次Minor GC,eden区的存活对象将被移入老年代,这样老年代会很快存满,然后就会触发Full GC,要知道,老年代的空间要比新生代空间大得多的,GC也会慢得多。
5.2 为什么要有两个survivor区?
用于复制算法的。最开始,新生对象会存在与Eden和Survivor From区,To为空,然后进行GC时,会将Eden中存活的对象复制到To区,From区的年纪未到的存活对象也会被复制到To区,如果到了年纪就会移入老年代,然后将Eden与From全部清除,如此反复。
5.3 拓展
在一些特定的时间点,Java虚拟机会进行一次彻底的垃圾回收(Full GC)。彻底回收时,垃圾收集器会对所有分配的堆内内存进行完整的扫描,在扫描期间,绝大部分正在运行的java线程都会被暂时停止。这意味着:这样一次垃圾收集对Java应用造成的影响,跟堆内内存所存储的数据的多少是成正比的,过大的堆内内存会影响Java应用的性能。感兴趣的朋友可以了解一下堆外内存,在这里我简单说一下:
为了防止堆内内存过大,导致在触发FullGC时程序卡顿以及操作系统对堆内内存的不可知性出现了堆外内存,堆外内存意味着把一些对象的实例分配在Java虚拟机堆内内存以外的电脑的直接物理内存区域。如此一来,对于大内存有良好的伸缩性,对与FullGC导致的停顿可以有所改善,并且在进程间可以共享,减少虚拟机间的复制。
6. 总结
以上为本人学习以来对GC的理解,目前深度也就这样了,学习这些东西,虽然我们可能并不能做JVM调优方面的工作,但是在生产环境出现一些OOM的情况是很常见的,如果我们对JVM都不了解,垃圾回收也不清楚,那么我们在面对一些OOM的情况时就会更加一头雾水。关于JVM内存分配的一些参数,例如-Xmx、-Xmn、-Xms、-XXSurvivorRatio。