JVM 垃圾回收
在C/C++中我们创建一个对象,为其分配内存空间之后,是需要程序员手动释放内存,清理不需要的对象(垃圾)。而在java中JVM虚拟机是会有后台的GC线程帮我们自动清理的。
一、什么是垃圾
Java程序中每new一个对象,都会在堆或者栈上分配对应的内存空间。
A a = new A()
那么如何确定这个对象什么时候变为垃圾呢?
垃圾:程序中的一块内存没有被任何变量持有引用,导致这块内存无法被这个程序再次访问时,这块内存被称为垃圾。
二、怎么找到垃圾
引用计数
过去java通过引用计算,即一个对象被引用一次,引用计数加一,某个变量不再引用这个对象,计数减一,当引用计数为0时,则将这个对象视为垃圾。
存在问题:两个对象互相持有对方的引用,但是没有任何其他变量引用这两个对象,则两个对象引用计数都为1,但是这两个对象其实已经无法被程序所访问到了,所以已经是垃圾了
可达性分析算法(Root Searching)
GC定义了一些根(Roots) 从这些根对象开始搜索,能被搜索到的引用对象就不是垃圾,反之这视作垃圾。
GC Roots:虚拟机栈(栈帧中的局部变量表所引用的对象),方法区中静态属性引用的对象(即static修饰的一些变量),方法区中常量引用的对象,本地方法引用的对象(Native方法),存活的线程中的对象等
三、垃圾回收算法
标记清除算法
首先遍历内存中所有的对象,标记垃圾对象,第二次遍历时将垃圾清除。
缺点:
- 需要遍历两次
- 内存被切割的很分散,可能会造成有足够的空间,但是无法为后续的大对象分配空间
优点:
- 算法容易理解,实现简单
- 垃圾较少时,效率非常高
拷贝算法
将内存分为两部分,给对象分配空间分配在其中一部分,GC时将存活对象拷贝到另外一部分,之前一部分全部回收
缺点:
- 可使用内存空间减少
- GC时需要拷贝对象,并且调整存活对象的引用
优点:
- 不会产生内存碎片
- 只扫描一次,效率高,尤其在垃圾多时
标记整理算法
先扫描一遍内存,标记垃圾对象,回收时,先将垃圾回收,然后将存活对象向内存一端移动
缺点:
- 需要扫描两次
- 需要移动对象,并且调整存活对象的引用
优点
- 不会产生内存碎片
- 不会减少可用内存空间
并发标记中错标情况
1:并发标记过程本来标记为垃圾的对象没有被标记,产生浮动垃圾,影响较小,下一次GC时清除即可
2:并发标记过程中本该存活的对象被标记为垃圾,对象被错误的回收,产生程序错误
三色标记
白色:对象尚未被垃圾收集器访问过,可达性分析阶段刚开始所有对象都是白色,结束仍然为白色则标记为垃圾
黑色:已经访问且所有引用都被访问
灰色:本身被访问但是存在至少一个引用未被访问
对象消失出现满足条件,两条同时满足
1:存在一条或多条黑色对象到白色对象的引用
2:所有灰色对象到该白色对象的引用都被删除了
解决办法
1:增量更新:并发标记过程中,出现的新的黑色对象对白色对象的引用记录下来,标记结束之后,再次对这些记录的黑色对象再次扫描
2:原始快照:并发标记过程中,灰色对象删除了对白色对象的引用时,将其记录下来,标记结束之后,,再次对这些记录的灰色对象再次扫描
四、内存分代模型
一种内存管理模型,不同内存区域运用不同的GC算法,提供内存利用率和回收效率。内存模型如下图:
JVM将内存中堆分为新生代和老年代
默认比例是1:2,我们也可以自己设置这个比例(通过 -Xms 初始化堆的大小,通过 -Xmx 设置堆最大分配的内存大小,通过 -Xmn 设置新生代的内存大小)
新生代又分为一个eden和两个suivivor
(eden和suivivor的默认比例是 8:1:1,通过 -XX:SurvivorRatio 可以自定义此比例)。
对象存活时间较长则放在老年代,反之则放在新生代,通过对象头设置的对象年龄来确定(最大为15,因为只预留了四位),
对象被new时首先会分配在eden,经历GC还存活就会移动到suivivor区中一个,后面每次GC都存活的话,会在两个suivivor区来回移动,每次GC,对象如果存活,年龄+1。当到达一定年龄则会转移到老年区。
新生代转移到老年代的年龄根据垃圾回收器的类型而有所不同,CMS(Concurrent Mark Sweep,一种垃圾回收器) 设置的默认年龄是 6,其他的垃圾回收器默认年龄都是 15。这个年龄我们可以自己设置(通过参数-XX:MaxTenuringThreshold 配置)
新生代的 GC 被称之为 YGC(Young Garbage Collector,年轻代垃圾回收)或者 MinorGC(Minor Garbage Collector,次要垃圾回收)
一般采用拷贝算法,因为新生代中的对象每次GC都存活下来一小部分,所以每次清除时都是,预留两个suivivor中一个,将eden和另外一个suivivor存活对象移动到其中来,然后整个回收内存。
老年代的 GC 采用的是 标记清除 或者 标记整理,因为老年代的空间较大,所以老年代的 GC 并不像新生代那样频繁。
整个内存回收称之为 FGC(Full Garbage Collector,完整垃圾回收),或者 MajorGC (Major Garbage Collector,重要垃圾回收)。YGC/MinorGC 在新生代空间耗尽时触发。FGC/MajorGC 在老年代空间耗尽时触发,FGC/MajorGC 触发时,新生代和老年代会同时进行 GC。在 Java 程序中,也可以通过 System.gc() 来手动调用 FGC。
五、垃圾收集器
STW:Stop The World GC线程运行,停止所有用户线程。
垃圾收集器优化理念就是让STW时间尽可能短,或者实现与用户线程并发执行。
Serial收集器
新生代收集器
最基础,历史最悠久,单线程工作,适用于单核处理器或者核心数较少,管理的内存较少,对于运行在客户端模式下的虚拟机适用。
ParNew收集器
新生代收集器,实质上就是Serial的多线程版本
Parallel Scavenge收集器
新生代收集器,基于标记-复制算法
其他垃圾收集器关注的是尽可能缩短用户线程停顿时间,而此垃圾收集器关注的是达到一个可控制的吞吐量,即用户代码执行时间比上用户代码执行时间+GC时间
Serlal Old收集器
单线程工作,基于标记-整理算法,老年代收集器,适用于客户端模式下的虚拟机
Parallel Old
Parallel收集器的老年代版本,支持多线程收集
CMS收集器
基于标记-清除算法,适用于服务器端运行的虚拟机