当一个程序启动之前,它的class会被类装载器装入方法区(Permanent区),执行引擎读取方法区的字节码自适应解析,边解析边运行,然后pc寄存器指向了main函数所在位置,虚拟机开始为main函数在Java栈中预留一个栈帧(每个方法都对应一个栈帧),然后开始跑main函数,main函数里的代码被执行引擎映射成本地操作系统里相应的实现,然后调用本地方法接口,本地方法运行的时候,操纵系统会为本地方法分配本地方法栈,用来储存一些临时变量,然后运行本地方法,调用操作系统API。
执行引擎中的GC(垃圾收集器)主要作用域运行时数据区的方法区和堆。
一些概念
通用GC概念
垃圾:Garbage(名词),在系统运行过程当中所产生的一些无用的对象,这些对象占据着一定的内存空间,如果长期不被释放,可能导致OOM。
垃圾收集器:Garbage Collector(名词),负责回收垃圾对象的垃圾收集器
垃圾回收:Garbage Collect(动词),垃圾收集器工作时,对垃圾进行回收
垃圾回收算法/GC算法:不同的GC算法,它们的垃圾回收工作模式不同(比如串行、并行等)
引用计数算法(Reference Counting)
标记-清除算法(Mark-Sweep)
复制算法(Copy)
标记-整理算法(Mark-Compact)
标记-清除-整理算法(Mark-Sweep-Compact)
GC算法优点缺点存活对象移动内存碎片适用场景
引用计数实现简单不能处理循环引用
标记清除不需要额外空间两次扫描,耗时严重NY旧生代
复制没有标记和清除需要额外空间YN新生代
标记整理没有内存碎片需要移动对象的成本YN旧生代
这几种算法中存活对象没有移动的算法只有:标记-清除算法。复制算法会将存活对象移动到另一块内存区,标记整理算法会将存活对象移动到边界位置
垃圾回收线程/GC线程:垃圾收集器工作时的线程。
应用程序和GC都是一种线程,以Java的main方法为例:应用程序的线程指的是main方法的主线程,GC线程是JVM的内部线程。
在GC过程中,如果GC线程必须暂停应用程序线程(用户线程),则发生Stop the World。当然也可以允许GC线程和应用程序线程一起运行,即GC并不会暂停应用程序的线程。
串行、并行、并发:串行和并行指的是垃圾收集器工作时暂停应用程序(发生Stop the World),使用单核CPU(串行)还是多核CPU(并行)。
串行(Serial):使用单核CPU串行地进行垃圾收集
并行(Parallel):使用多CPU并行地进行垃圾收集,并行是GC线程有多个,但在运行GC线程时,用户线程是阻塞的
并发(Concurrent):垃圾收集时不会暂停应用程序线程,大部分阶段用户线程和GC线程都在运行,我们称垃圾收集器和应用程序是并发运行的。
概念Stop the World单线程/多线程
串行YGC线程是单线程
并行YGC线程是多线程
并发NGC线程和应用程序线程是多线程
在Java中有并发编程的概念,并发编程中有多线程的概念。通常并发指的是不同类型的线程可以同时运行(比如GC线程和用户线程并发地运行),而并行指的是相同类型的线程采用多线程模式运行(比如GC线程使用多个CPU并行地运行)。
GC暂停/Stop The World/STW:不管选择哪种GC算法,Stop-the-world都是不可避免的。Stop-the-world意味着从应用中停下来并进入到GC执行过程中去。一旦Stop-the-world发生,除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务。GC调优通常就是为了改善stop-the-world的时间(尽量减少STW对应用程序造成的暂停时间)。
垃圾对象:对象如果没有在使用,认为是垃圾对象,那么怎么判定有没有在使用?
一个在使用的对象(被引用的对象):程序的某个部分依然维系者一个指向该对象的指针
一个没有使用的对象(未被引用的对象):该对象不再被你程序的任何部分引用,所以被这些不再使用的对象占用的内存可以(被垃圾收集器)得到回收
具体的实现方式:
引用计数算法:对象的引用计数=0,表示没有对象引用它,可以作为垃圾对象(该方法无法处理循环引用)
根搜索算法:当前对象到根对象没有一条可达的路径,可以作为垃圾对象(JVM采用此方法)
引用计数算法和根搜索算法
引用计数
引用计数算法:每个对象都有一个引用计数器,当有对象引用它时,计数器+1;当引用失效时,计数器-1;任何时刻计数器为0时就是不可能再被使用的。
下图中左图是对象的引用关系,中图有一个引用失效,右图是清理引用计数器=0的对象后。
但是这种方式的缺点是:
引用和去引用伴随加法和减法,影响性能
对于循环引用的对象无法进行回收
下面的3个图中,最右图三个对象的循环引用的计数器都不为0,但是他们对于根对象都已经不可达了,但是无法释放。
根搜索
解决循环引用的办法是:使用根搜索算法来判定对象是否需要被回收,只要对象没有一条到根对象的可达路径,就可以被回收。
所以问题转换为:怎么定义根对象?在Java中可以作为GC Roots的对象:
虚拟机栈(左上)的栈帧的局部变量表所引用的对象
本地方法栈(右中)的JNI所引用的对象
方法区(右下)的静态变量和常量所引用的对象
示例
循环引用的程序实例,如果采用引用计数,无法被垃圾收集器回收
使用根搜索算法,则可以正常回收
Tracing GC算法
Tracing GC算法主要包括:
标记清理/标记清除:标记垃圾对象,然后清理垃圾对象
复制算法:标记垃圾对象和非垃圾对象,将非垃圾对象移动到某个空闲的内存块
标记压缩/标记整理:标记垃圾对象和非垃圾对象,将非垃圾对象移动在一起
通常还会存在标记清理和标记压缩结合起来的:标记-清理-压缩算法
结合根搜索算法定义的对象可达性,对应的垃圾收集算法如下(左图是垃圾收集前,右图是垃圾收集后):
A.标记-清理算法(Mark-Sweep Collector)
步骤:
标记阶段:根据可达性分析对不可达对象进行标记,即标记出所有需要被回收的对象
清理阶段:标记完成后统一清理这些对象,即回收被标记的对象所占用的空间
缺点:
标记和清理的效率都不算高(因为垃圾对象比较少,大部分对象都不是垃圾)
会产生大量的内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作
适用场景:基于Mark-Sweep的GC多用于老年代。
B.复制算法(Copy Collector)
适用场景:新生代GC
C.标记-压缩算法(Mark-Compact Collector)
步骤:在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存
在标记好待回收对象后,将存活的对象移至一端
然后对剩余的部分进行回收
优点:
可以解决内存碎片的问题
适用场景:基于Mark-Compact的GC多用于老年代
D.标记-清理-压缩算法(Mark-Sweep-Compact Collector)
结合使用标记清理算法(Mark-Sweep)和标记压缩算法(Mark-Compact)
并不是每次标记清理都会执行压缩,而是多次执行GC后,才会执行一次Compact
优点:
相对于标记清理和标记压缩算法,可以减少移动对象的成本(并不是说不会移动对象,只要有压缩就一定会移动对象,只不过压缩不是很频繁)
JVM GC
前面我们分析了垃圾收集器的几种算法,在Java中,因为对象创建在堆中,垃圾收集时,垃圾收集器就应该扫描堆中的对象,执行垃圾收集工作。
基于分代理论的垃圾回收
JVM的垃圾回收器基于以下两个假设:
大多数对象很快就会变得不可达,即很多对象的生存时间都很短
只有极少数情况会出现旧对象(老年代对象)持有新对象(新生代)的引用,即新生对象很少引用生存时间长的对象
问题1:到底是老年代对象引用新生代对象,还是新生代对象引用老年代对象?
问题2:引用和持有引用有什么关系,比如A引用了B,和A持有B的引用。
这两条假设被称为”弱分代假设”。为了证明此假设,在HotSpot VM中物理内存空间被划分为两部分:新生代(Young generation)和老年代(Old generation)。
新生代:大部分新创建的对象分配在新生代。因为大部分对象很快就会变得不可达,所以它们被分配在新生代,然后消失不再。当对象从新生代移除时,我们称之为”Minor GC”。
老年代:在新生代中存活的对象达到一定年龄阈值时会被复制到老年代。一般来说老年代的内存空间比新生代大,所以在老年代GC发生的频率较新生代低一些。当对象从老年代被移除时,我们称之为”Major GC”(或者Full GC)。
概念
分代:将JVM的堆内存分成多个代(generation)。