jvm常见垃圾回收算法及双亲委派模型
10 Feb 2021java
相对于C++
优势在于自动的垃圾回收,提供对象的构造函数后,不需要再提供析构函数(销毁对象,释放之前申请的内存),更易避免了内存泄露的问题。主要归功于虚拟机进行垃圾回收,虚拟机版本有Sun
公司的HotSopt VM
、BEA
的JRockit
、微软的JVM
及IBM
的J9 VM
。
内存区域划分
Java
虚拟机在执行程序时会把管理的内存划分为若干个不同的数据区域,这些区域有各自的用途,以及创建和销毁的时间。
1)程序计数器(Program Counter Register
)占用一块较小的内存空间,可看作是当前线程执行字节码的行号指示器(与操作系统中的PC
的概念相同,指定下一条指令的位置)。在执行分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。每个线程都有一个独立的程序计数器;
2)Java
虚拟机栈也是线程私有的,声明周期与线程相同。描述Java
方法执行的内存模型,每个方法执行会创建一个栈帧 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。递归方法调用超过最大深度时 将跑出StackOverflowError
的异常;
3)本地方法栈(Native Method Stack
)用于调用其它语言的方法(如C++
),声明周期也与线程绑定;
4)Java
堆是多个线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,也是垃圾收集器管理的主要区域。由于收集器基本采用分代收集算法,Java
堆还可以细分为:新生代和老年代(细致些有Eden
空间、From Survivor
空间、To Survivor
空间)等;
5)方法区(Method Area
)与Java
堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后代码等数据。在HotSpot
中,开发者更愿意把方法去称为“永久代”(Permanent Generation
),但本质上并不等价;
6)运行时常量池(Runtime Constant Pool
)和直接内存(Direct Memory
)这两部分,用于存储class
文件翻译出来的直接引用也存储在运行时常量池中,直接内存主要用于NIO
类;
回收算法及垃圾收集器
如何判断一个对象已死?已有引用计数算法,但其无法解决循环引用的问题。java
采用可达性分析算法,算法的基本思路 是通过一系列称为”gc roots
“的对象作为起始点,搜索所走过的路径称为引用链(reference chain
),当gc
不可达时 则证明此对象是不可用的。
“标记-清除”(Mark-Sweep
)算法,首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。问题在于,一个是效率问题 标记和清除两个过程的效率都不高,另外,还会产生大量不连续的内存碎片。在分配较大对象时,容易产生OOM
。
为了解决效率问题,一种称为复制(copying
)的算法出现了,它将可用内存按容量划分为大小相等的两个块。每次将存活的对象复制到另一个块。但是,内存利用率不高 存在50%
的内存浪费。目前商业虚拟机分为1
个80%
的Edge
区和2
个10%
的Survivor
区。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark Compact
)算法,差异在于不清理可回收的对象,而是让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
商业上目前用的是分代回收(Generational Collect
)算法,根据对象存活周期的不同将内存划分为几块。一般是把Java
堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
常见的垃圾回收器,垃圾回收器就是内存回收的具体实现,Java
虚拟机规范中对实现没有任何规定,因此,不同厂商虚拟机使用的垃圾收集器可能会有很大差别。
Serial
收集器,是发展历史最悠久的收集器,曾经(在JDK 1.3.1
之前)是虚拟机新生代收集的唯一选择。该收集器会引入”stop the world
“的问题,进行垃圾收集时必须暂停其它所有的工作线程,直到它结束。
ParNew
收集器,是Serial
收集器的多线程版本,除了使用多条线程进行垃圾收集外,却是许多运行在Server
模式下的虚拟机首选新生代收集器,只有ParNew
和Serial
能够与CMS
收集器配合工作。
Parallel Scavenge
收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。其优点在于达到一个可控制的吞吐量(Throughput
),吞吐量就是CPU
用于运行用户代码的时间与CPU
总耗时间的比值。
CMS
收集器是一种以获取最短回收停顿时间为目标的收集器,其是基于“标记-清除”算法实现的,整个过程分为:初始标记(initial mark
)、并发标记(concurrent mark
)、重新标记(remark
)、并发清除(concurrent sweep
)。耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,停顿的时间较少。
CMS
是基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生,当没有足够空间来满足大对象分配时,那不得不提前进行一个Full GC
。CMS
收集器提供了+UseCMSCompactAtFullCollection
开关参数,用于在CMS
收集器进行Full GC
时合并内存碎片。
此外,CMS
收集器对CPU
资源是非常敏感的,在并发阶段,虽不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢。
G1
收集器是当今收集器技术发展的最前沿成果之一,由Java 1.7
引入。G1
是一款面向服务端应用的垃圾收集器,HotSpot
开发团队赋予它的使命是在未来可以替换掉JDK 1.5
中发布的CMS
收集器。与其它GC
收集器相比,G1
具备以下特点:
并发与并行,G1
能充分利用多CPU
、多核环境下的硬件优势,使用多个CPU
来缩短Stop The World
的停顿时间;分代收集,分代的概念在G1
中依然得以保留。它可以采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过了多次GC
的旧对象以获取更好的收集效果;
空间整合,与CMS
的“标记-整理”算法不同,G1
从整体来看是基于“标记-整理”算法实现的收集器,从局部上来看是基于“复制”算法实现的;可预测的停顿,这个G1
相对于CMS
的另一大优势,降低停顿时间是G1
和CMS
共同的关注点,但G1
处理追求低停顿外,还能建立可预测的停顿时间。
类加载器与双亲委派模型
从Java
虚拟机的角度来讲,只存在两种不同的类加载器:一种是自动类加载器(Bootstrap ClassLoader
),这个类加载器使用C++
实现。另一种就是所有其它的类加载器,包括扩展类加载器(Extension ClassLoader
)、应用程序类加载器(Application ClassLoader
),及一些实现了ClassLoader
接口的自定义类加载器。
双亲委派模型对于保证Java
程序的稳定运作很重要,但其实现却非常简单,只需实现java.lang.ClassLoader
的loadClass()
方法就可以(同时设置class
文件的path
)。加载逻辑:先检查是否已被加载过,若没有加载则调用父加载器的loadClass
方法,若父加载器为空则默认使用启动类加载器作为父加载器。
破坏双亲委派模型,覆写loadClass()
方法实现加载class
的逻辑,而类加载器和抽象类java.lang.ClassLoader
在JDK 1.0
时代就已经存在。在JDK 1.2
之后已不提倡用户再去覆盖loadClass()
方法,而应当把自己的类加载逻辑写到findClass()
方法中。
双亲委派模型的第二次破坏是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器的基础类的统一问题。可在线程上下文类加载器(Thread Context ClassLoader
)中通过Thread
的setContextClassLoader()
进行设置。若创建线程时还未设置,它将会从父线程中继承一个,默认为应用程序类加载器,也算是一种“舞弊”的方式。第三次破坏是由于用户追求动态性的追求导致的,这里的“动态性”指的是当前一个非常“热门”的名次:代码热替换(HotSwap
)、模块热部署(Hot Deployment
)等,采用OSGI
的技术。