对象访问定位方式
Jvm通过栈上的reference来操作堆上的对象,主要有两种方式:句柄和直接指针。
句柄
Java堆中划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
直接指针
reference中存储的就是对象地址。
使用句柄的优点是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改;使用直接指针访问方式的优点是访问速度快,它节省了一次指针定位的时间开销。
由于对象的访问在Java中非常频繁,因此这类开销积少成多也会消耗较多的资源。对Sun HotSpot而言,使用的是直接指针的方式。
四种引用
强引用:如Object obj = new Object() ,就属于强引用。在任何情况下,只有有强引用关联(与根可达)还在,垃圾回收器就永远不会回收掉被引用的对象。
软引用:SoftReference,一些有用但是并非必需。用软引用关联的对象,系统将要发生内存溢出(OuyOfMemory)之前,这些对象会被回收。如果这次回收后还是没有足够的空间,才会抛出内存溢出。
软引用使用场景。例如,一个程序用来处理用户提供的图片。如果将所有图片读入内存,这样虽然可以很快的打开图片,但内存空间使用巨大,一些使用较少的图片浪费内存空间,需要手动从内存中移除。如果每次打开图片都从磁盘文件中读取到内存再显示出来,虽然内存占用较少,但一些经常使用的图片每次打开都要访问磁盘,代价巨大。这个时候就可以用软引用构建缓存。
弱引用:WeakReference,一些有用(程度比软引用更低)但是并非必需,用弱引用关联的对象,只能生存到下一次垃圾回收之前,GC发生时,不管内存够不够,都会被回收。
虚引用:PhantomReference、幽灵引用,最弱(随时会被回收掉)。垃圾回收的时候收到一个通知,就是为了监控垃圾回收器是否正常工作。
对象存活判断
判断对象是否存活状态,通常有引用计数和根可达两种算法。经过判断为非存活状态,即是我们所说的“垃圾”。
引用计数法
在对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1,当引用失效时,计数器减1。目前C++的智能指针和Python采用这种方法。Jvm虚拟机没有使用,因为存在对象相互引用的情况,这个时候需要引入额外的机制来处理,影响效率。
根可达算法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。这个搜索的过程称为“根可达分析”。
常用来作为GC Roots的对象包括下面几种:1、虚拟机栈(栈帧中的本地变量表)中引用的对象;2、方法区中类静态属性引用的对象;3、方法区中常量引用的对象;4、本地方法栈中JNI(即一般说的Native方法)引用的对象;5、JVM的内部引用(class对象、异常对象NullPointException、OutofMemoryError,系统类加载器);6、所有被同步锁(synchronized关键)持有的对象;7、JVM内部的JMXBean、JVMTI中注册的回调、本地代码缓存等;8、JVM实现中的“临时性”对象,跨代引用的对象(在使用分代模型回收只回收部分代时)等。
垃圾回收算法
当发生GC时,经过“GC Roots”算法判断为不可达的对象,就会发生对象回收的动作。JVM对象回收主要有复制算法、标记清除、标记整理算法。
复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。进行垃圾回收时,将所有存活的对象复制到另一块区域。然后对该区域进行整体清除。
优点是实现简单,运行高效,并且不存在内存碎片;缺点是内存的利用率只有一半。
标记-清除算法(Mark-Sweep)
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
特点是:1、会存在内存碎片。导致在程序运行中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发GC;2、执行效率不稳定:需要清理的对象越多,则效率越低。
标记-整理算法(Mark-Compact)
首先标记出所有需要回收的对象,在标记完成后,将存活对象,整理到连续区域。然后将非存活对象清除。
优点:回收后堆空间规整,没有碎片;
缺点:效率偏低,涉及到对象移动、引用更新(因为通常使用直接引用方式)、需要暂停用户线程。
分代垃圾回收机制
等待被回收对象可能处于堆区的不同区域(Eden区、Survival区、Tenured区),JVM垃圾回收机制根据各个区域的特点,分别采用不同的回收算法,称为分代垃圾回收。
分代垃圾收集思想:现代分代垃圾回收机制设计,源于两个重要的统计依据:1、绝大部分(98%以上)对象都是分配之后很快就被回收;2、一个对象经过多次垃圾回收,则越难回收。因而对不同的分区采用不同的垃圾回收算法。
堆区分为新生代区和老年代(Tenured)区,新生代区包含Eden区和Survival区,Survival区又分为From区和To区。新生代区域与老年代区域大小比例为1:2。新生代区中大小比例,Eden:From:To 为 8:1:1。
新生代区采用复制算法。对象优先分配到Eden区域,经过一次GC(复制算法),存活的对象将放入Survive(From或To区),非存活对象直接清除。
对象在Survive区,从From区和To区之间来回执行复制回收算法。如,当前对象都在From区,经过GC,存活的对象移动到To区并将GC分代年龄+1,非存活对象直接清除。当对象分代年龄到达临界值(Hotsopt为15),将其移动到老年代区域。
在老年代执行标记清除或标记整理算法。不同垃圾收集器所有不同,CMS使用的既是标记清除算法。
垃圾收集器
根据垃圾收集器工作在新生代、老年代,分为新生代和老年代垃圾收集器;
垃圾收集器工作中是否多线性执行垃圾回收,分为单线程和多线程垃圾收集器;
在垃圾收集过程中是否与用户线程并行执行,分为并行和非并行垃圾收集器。 Parallel Scavenge、Parallel Old、CMS和G1,都是并行的垃圾收集器。
当前主要的垃圾收集器及对比如下:
CMS垃圾收集器
CMS是工作在老年代的垃圾收集器,采用标记清除算法。作为第一个能够与用户线程并行执行的垃圾收集器,具有划时代的意义。之前的垃圾收集器,在指向垃圾清除的时候,需要暂停用户线程,即常说的“STD(stop the world)”现象。
CMS与用户线程并行的原理是将垃圾收集划分成几个阶段。耗费时间短的初始标记和重新标记阶段,暂停所有用户线程;而耗时长的并发标记和并发清理阶段与用户线程并行执行。
初始标记阶段,值标记GC Roots能直接关联到的对象,耗时短;并发标记阶段,进行GC Roots深入追踪与标记,此过程耗时长,用户线程并行进行;重新标记阶段,标记在并发标记期间,用户程序执行而导致标记变动的部分,耗时短,暂停用户线程;并发清除,对标记的垃圾进行清理,消耗时间时间长,与用户线程并行执行。
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾被称为“浮动垃圾”。