一、前言
笔者最近在复习JVM的知识,本着记录分享的精神,整理下学习Java虚拟机垃圾回收相关知识点,由于整个垃圾回收内容比较多,我将整理成上下两篇文章去分享,上篇我会主要分享Java虚拟机的运行时数据区域划分,垃圾回收算法。下篇文章主要分享Java虚拟机的垃圾回收器以及一些虚拟机调优建议。
二、运行时数据区
Java虚拟机定义了程序在运行期间的多种数据区域,其中有些区域是在Java虚拟机创建的时候就创建了,只有在虚拟机退出后才会被销毁。根据Java虚拟机定义,我们可以数据区域做如下区分,分为:堆、Java虚拟机栈、程序计数器、方法区(元数据区、运行时常量池、本地方法栈。下面我们来详细介绍下每个区域的作用。
2.1 程序计数器
程序计数器是一块线程私有的区域,是一个较小的内存块,用来存放当前线程执行的字节码的指令地址,如果执行的是本地方法(Native),这个计数器就会为空(Undefined)。
2.2 Java虚拟机栈
Java虚拟机栈是线程私有的区域,生命周期与线程相同,它存储的是栈帧(Stack Frame),栈帧会来存储局部变量表、操作数栈、动态链接、方法出口和返回地址等信息。每一个方法从调用到执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。如果线程请求的栈深度大于虚拟机所允许的最大深度,就会抛出StackOverflowError异常;如果申请栈内存不够,也会导致抛出OutOfMemoryError异常。
Jvm参数
-Xss:栈空间大小;栈的空间大小决定了栈能创建的深度
栈结构如下:
2.3本地方法栈
本地方法栈和java方法栈非常类似,他们之前的区别主要是Java方法栈是提供给字节码服务的,本地方法栈是给本地方法(C语言实现)调用服务的。Java虚拟机并没有对本地方法栈中使用的语言、数据结构等进行强制规定,所以虚拟机可以自行实现它。Sun HotSpot虚拟机把虚拟机栈和Java方法栈进行了合二为一。本地方法栈也会和虚拟机栈一样抛出StackOverFlowError和OutOfMemoryError异常。
2.4 堆
Java堆是一个所有线程共享的区域,堆用来存储几乎所有对象的实例和数组,堆按照分代的思想进行划分,可以划分了新生代(YoungGeneration)和老年代(Old/Tenured Generation),新生代又可进一步细分为 eden、survivor space0(s0 或者 from space)和 survivor space1(s1或者to space)。我们用图来表示下堆的划分:
eden区:新建对象一般都放在该区域,除非是新建了大对象,该区域放不下就直接存放在老年代(Tenured)。 S0和S1区:该区域放置的对象至少经历了一次垃圾回收(Minor GC),如果经历了多次回收,到达指定次数还存活,那么就会被转移到老年代。
Java虚拟机规范规定堆可以是物理上不连续的空间,只需要逻辑上连续即可,我们可以通过命令(-Xmx和-Xms )来调整堆空间,如果申请的堆内存超过了堆的最大内存,将会抛出OutOfMemoryError异常。
Jvm参数
-Xmx:最大堆空间大小
-Xms:最小堆空间大小
-Xmn:新生代空间大小
2.5 方法区(元数据区)
方法区是线程共享的区域,它用于存放已经被虚拟机加载的类信息、常量池、静态变量、即时编译器编译后的代码等数据。类信息包括类的完整名称、父类的完整名称、类型修饰符(public/protected/private)和类型的直接接口类表;常量池指运行时常量池(后面有介绍);方法区又被称为非堆(Non-Heap)。
在Host Spot虚拟机的实现中,方法区也被称为永久区,是一块独立于 Java 堆的内存空间。虽然叫永久区,但是永久区中的对象同样可以被 GC 回收的(注:方法区是 JVM 的一种规范,永久区是一种具体实现,在 Java8 中,永久区已经被 Metaspace 元空间取而代之。相应的,JVM参数 PermSize 和 MaxPermSize 被 MetaSpaceSize 和 MaxMetaSpaceSize 取代
)。对永久区 GC 的回收,通常主要从两个方面分析:一是 GC 对永久区常量池的回收;二是永久区对类元数据的回收。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
2.5.1运行时常量池
运行时常量池(Run-Time Constant Pool)是方法区的一部分,它主要用来存放编译期生成的各种字面量和符号引用,既然是运行时常量池,理所应当的可以存放运行时产生的常量,比如调用String.intern()
方法产生的字符串常量就会被放入运行池常量中。
三、垃圾判定算法
3.1 引用计数算法
引用计数法的思想比较简单,每个对象都有一个引用计数器,只要对象被引用,计数器就+1,当对象不再被引用时候,计数器就减一。这种算法很高效,但是有一个致命缺点,就是有循环引用的问题。对于两个无用对象的互相引用,就会导致两个对象的计数器不为0,从而无法被判定为无用对象,无法回收内存。
3.2 可达性分析算法
由于引用计数法有互相引用的缺陷,所以Java虚拟机采用了可达性分析算法来判定垃圾对象。这个算法的思想是,以一系列称为“GC Roots”的对象作为起始点,从这些起点往下搜索,搜索所走过的路径称为引用链(Referenc Chain),当一个对象到GC Roots没有任何引用链(从GC Roots到这个对象不可达)时,就说明这个对象不可用,可以被回收。
可以作为GC Roots的对象包括:
- 虚拟机栈中引用的对象(局部变量)。
- 方法区中类静态属性引用的对象(静态变量)。
- 方法区中常量引用的对象(常量)。
- 本地方法栈中JNI(即本地方法)引用的对象。
那为什么上面四种对象就可以作为GC Roots呢?
1.虚拟机栈中当前引用的对象,因为虚拟机栈中的对象是随着线程的生命周期存活的,那么在垃圾判断的时候,当前线程还存活,也就意味着栈中持有的对象肯定是存活的,所以可以作为GC Roots,本地方法栈也是一样的道理。
2.对于方法区中的静态变量引用和常量,我的理解是使用方法区中的对象作为GC Roots并不是一定就会以里面所有的对象作为GC Roots,虽然Java虚拟机并没有规定方法区要进行回收,但是该区域在目前的JVM实现中都有回收,由于方法区也会对“废弃常量”和“无用类”进行回收,所以选择GC Roots只会选择方法区内的有效对象。"废弃常量"判断比较简单,对于“无用类”的判断,Java虚拟机只会判断动态加载的类,对于原始加载的类,虚拟机永远不会自动卸载。所以判断动态加载的类为无用类可以有以下原则:
- 该类所有的实例都已经被回收,堆中不存在该类的任何实例。
- 该类对于的类加载器已经被回收
- 该类对应的java.lang.Class对象没有在任何地方引用,无法通过反射访问该类的方法。
四、垃圾回收算法
4.1 标记-清除算法
标记-清除算法分为“标记”和“清除”两个阶段,首先需要标记出需要回收的对象,标记完成后再进行统一的垃圾回收。该算法有两个缺点:1.效率不高;2.清除后会产生大量不连续的内存碎片,内存碎片会导致分配大对象时候,无法找到足够的内存,从而提前触发一次GC.
4.2 复制算法
上面的标记清除算法效率不高,为了解决这个问题,就有了复制算法,复制算法就是把内存容量划分为大小相等的两块,每次只用其中一块,当一块内存用完后就将存活的对象复制到另外一块内存上,然后再对原内存块进行清理。这种算法的优点就是内存分配不用考虑碎片的问题,只需要移动堆顶的内存指针,按顺序分配内存即可。但是这算法的缺点就是空间利用率不高,将内存缩小为原来的一半,有一半的内存没有被真正利用起来。
虽然内存利用率不高,但是目前的虚拟机中堆中的新生代就是采用这种算法进行垃圾回收的。上面我们提到新生代分为 eden 空间、form 空间和 to空间3个部分。其中 from 和 to 空间可以视为用于复制的两块大小相同、地位相等,且可进行角色互换的空间块。from 和 to 空间也称为 survivor 空间,即幸存者空间,用于存放未被回收的对象。
在垃圾回收时,eden空间中存活的对象会被复制到未使用的survivor空间中(假设是 to),正在使用的survivor空间(假设是 from)中的年轻对象也会被复制到to空间中(大对象或者老年对象会直接进入老年代,如果to空间已满,则对象也会进入老年代)。此时eden和from空间中剩余对象就是垃圾对象,直接清空,to空间则存放此次回收后存活下来的对象。
4.3 标记-压缩算法
复制算法只适用于存活率较低的新生代中,如果存活率较高就需要进行过多的复制操作,效率将会降低。老年代的存活率比较高,所以复制算法不适用于老年代的场景,之前提到的“标记-清除”算法,如果不会产生内存碎片的话,还是可以满足老年代的,那么有没有不产生碎片的类似算法呢?答案是有,“标记-整理”算法就派上用处了。它的核心思想是:先对可回收对象进行标记,然后把所有存活的对象移动到一端,接着直接清理掉边界意外的内存区域。因为清理过后,存活对象都紧密的在一端,所以不会产生内存碎片。
八、总结
本篇文章我整理了Java虚拟机的运行区划分,每个区域的作用,同时分享了垃圾判断算法和垃圾回收算法。运行时数据区划分为:程序计数器、Java虚拟机栈、本地方法栈、堆、方法区、运行时常量池。有的文章中提到Jdk1.7及以后的版本把运行时常量从方法区移除,这里我想说明下,Java虚拟机规范还是要求在方法区分配,这只是个别虚拟机的自己实现,比如说Hot Spot虚拟机。
垃圾判定算法现在虚拟机主要使用可达性分析算法,垃圾回收算法有“标记-清除”算法、“复制”算法、“标记-整理”算法。“复制”算法比较适合存活对象较少的新生代,“标记-整理”算法比较适合老年代,整理的作用就是为了有连续的内存空间,防止内存碎片太多无法存放大对象。
九、参考
《深入理解jvm虚拟机 第二版》