一. 简介
Java中的垃圾回收机制与C++有着本质上的不同,在C++语言中我们用delete关键字主动来释放某块内存,所以程序员需要谨慎小心来管理内存分配,使用完某一块内存后手动调用delete来释放内存空间。然而内存释放不完全,换一句话说就是存在着某些永远都没有释放的内存块,很有可能因此内存泄漏,严重时可能会让系统崩溃,程序运行受阻。
但是,Java语言在垃圾回收上有一些调整,可以进行自动回收,大大减少了程序员的工作量,使得开发人员更加关注实现本身,而不用过多着眼于垃圾回收和内存释放。凡是均有利弊,这样做的后果是增加了系统的负担。
二. JVM内存结构
首先,可以看到某些结构是不需要回收的,比如JVM stack, Native Method Stack以及Program Counter Register, 因为这三个部分是线程隔离数据区,也就是说他们的生命周期与Thread(线程)是同步的,当Thread被destroy后,这三个部分随着线程而自动释放。相反Method Area和Heap区域是线程共享的,所以我们需要关注的也就是这部分内存的回收。
下面解释一下几个部分的作用:
1. Class Loader(类加载器): 读取Java源文件以及将类文件导入Data Area。
在这里很多类加载器,他们的分工也不同,等级划分如下:
- Bootstrap Class Loader: 其他类加载器的parent,它负责加载java核心库,也是唯一一个用native code写出来的。
- Extension Class Loader: 继承于Bootstrap Class Loader,用于加载扩展类。
- System Class Loader: 继承于Extension Class Loader,根据环境变量CLASSPATH,加载应用类文件。
- User-defined Class Loader: 继承于System Class Loader或者其他User-defined Class Loader。
加载类的过程是这样的:
当一个类加载器收到请求去加载类文件时,首先查看缓存,判断是否已经加载过,如果没有,给上级类加载器发出请求,要求它们来加载。如果上级类加载器失败,孩子类加载器来进行加载工作.
这里有个小trick, 子类可以查看上级类的缓存,而上级类不能查看子类缓存,这样设计的意义在于子类是不能加载一个已经被上级类加载过的类文件的。
2. Execution Engine(执行引擎): 执行Data Area中的指令。
主要功能是让机器能够识别bytecode。
3. Method Area ( 方法区):
- 加载类的信息(名字,修饰符等)
- 类的静态变量(static修饰)
- 类的常量(final修饰)
- 类的方法
4. JVM 栈: 存放基本变量以及引用变量
5. Heap:存放对象(包括数组)
三. 内部结构
1. 堆空间
堆内存中分为年轻代(Young Generation)和年老代(Old Generation)。年轻代一般是动态存储,近期刚刚new的对象存放在这里,而时间久一些的对象被移入年老代。
JVM内存垃圾过程:
1、Eden Pool中新的对象完成内存分配
2、当Eden Poll满了,再创建对象,会因为申请不到空间,触发minorGC,young(eden+1survivor)区垃圾回收。
3、minorGC时,Eden不能被回收的对象被放入到空的survivor,清空Eden Pool,另一个survivor里不能被GC回收的对象也会被放入这个survivor,始终保证一个survivor是空的。
4、当做第3步的时候,如果发现survivor满了,则这些对象被copy到old Generation,或者survivor并没有满,但是有些对象已经足够Old,也被放入Old区。
5、当Old区被放满的之后,进行fullGC
2. 其他
Method Area是永久代,一般不参与垃圾回收。Native Area存放基本变量和引用。
四. 对象分配原则
1.对象优先分配在Eden区,如果Eden区没有足够的空间时,JVM执行一次Minor GC。
2.大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
3.长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
4.动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
5.空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于,检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
五. 垃圾回收常用算法
1. 引用计数法:
给每一个对象添加一个引用计数器,当程序中增加一个该对象的引用,计数器加1,反之,减1。当计数器为0该对象将被垃圾回收机制定为目标来进行内存回收。
弊端: 无法解决循环引用问题。需要编译器配合产生特殊指令来进行引用计数操作。
2. 根搜索算法:
使得名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则该对象不可达,该对象是不可使用的,垃圾收集器将回收其所占的内存。
通常情况下,可以作为GC Root的对象:
a. JVM栈(栈帧中的本地变量表)中的引用的对象。
b.方法区中的类静态属性引用的对象。
c.方法区中的常量引用的对象。
d.本地方法栈中JNI本地方法的引用对象。
3. 标记-清除法(Mark-Sweep):
分为两个阶段,标记和清除。在标记阶段中,从根节点开始,标记所有经过的对象。最后没有被标记的对象就应该被回收。然后再清除阶段,将那些没有标记的对象清除。
弊端:存在空间碎片,回收的空间不连续。
4. 复刻算法(Copying):
从根集合扫描存活对象,复制到一个未被使用的空内存中,清除正在使用的内存块,交换两个空间,完成垃圾回收。
具体过程如下:
首先将内存空间分成两个相等的区域,然后扫描根集合,将存活对象复制到另一个内存空间中,清除正在使用的空间,进行垃圾回收。
当内存中垃圾很多时,也就是存活对象很少时,复刻算法的效率很高,而且将存活对象复制到新的内存空间中,会进行整理,所以没有碎片产生。
弊端:需要两倍内存空间。
参考资料:
JVM 垃圾回收器工作原理及使用实例介绍
Java之JVM垃圾回收 内存结构以及垃圾回收算法
Java Virtual Machine: the Essential Guide