JVM内存模型与垃圾回收
1 JVM与JVM结构
1.1 JVM是什么
JVM是java virtual machine的英文首字母缩写, 也就是java虚拟机。 是java运行环境(jre)的一部分,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
java为什么是跨平台的语言? 是因为oracle官方拥有多个不同系统的jvm实现。我们只写一套代码就可以在不同的设备上运行。
1.2 JVM结构
JVM主要包含了4个部分:类加载器、运行时数据区、执行引擎和本地方法接口
类加载器将java文件编码后的class字节码文件加载到JVM中
运行时数据区存储与程序运行相关的所有数据
执行引擎负责解释与执行我们的指令
本地方法接口用来调用其他语言编写的程序
2 运行时数据区
jvm的运行时数据区主要包含5个部分:堆、栈、方法区、程序计数器和本地方法栈,
2.1 程序计数器
程序计数器是线程私有的, 会随着线程创建而创建。 程序计数器记录的是当前字节码指令的地址。如果方法是native,则程序计数器记录的内容为undefined
2.2 栈
栈也是线程私有的,虚拟机栈描述的是Java 方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直到执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程中。如果线程请求的栈深大于虚拟机所允许的尝试,将抛出StackOverflowError;如果栈扩展时无法申请到足够的内存时会抛出OutOfMemoryError。
2.3 本地方法栈
本地方法栈与栈类似,主要是区别是本地方法栈不是用来执行java方法的,而是用来执行native方法的
2.4 方法区
方法区是所有线程都共享的内存区域,它存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然jvm规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的是与堆区别开来
2.5 堆
堆是也是线程共享的,也是一般情况下最大的内存区域。因为java是面相对象的语言,我们创建的所有的对象都存放到这个堆里面。这个区域是垃圾回收主要区域, 现在java主要是采用分代收集算法来对此区域进行垃圾回收
3 垃圾回收
3.1 垃圾回收是什么
垃圾回收是jvm提供的一种用于释放堆内垃圾(垃圾就是不再被使用到的内存空间)占用的空间,防止内存泄露的手段。有效的使用可以使用的内存,对内存堆中已经死亡或者长时间没有使用的对象进行清除和回收。
3.2 判断垃圾的算法
由于引用计数法无法解决循环引用的问题,所以目前jvm是根据根搜索算法(可达性分析算法)来判断一个对象是否垃圾的。
3.3 根搜索算法
根搜索算法是把一些对象看作是程序的根, 以这些根为起点一直搜索,找出与这个根没有直接引用或者间接引用的对象, 然后这些对象就可以被判定为垃圾了。
3.4 根搜索算法中的根有哪些
根搜索算法中可以作为根(GC Roots)的对象包括:虚拟机栈(栈帧的局部变量)中引用的对象、方法区中类的静态属性或常量引用的对象、本地方法栈中JNI引用的对象
4 对象的引用类型
在java中, 引用的对象分为四种,分别是:强引用、软引用、弱引用和虚引用
4.1 强引用
强引用是比较普通的引用, 我们平常new出来的对象就是强引用,这类的对象不会被回收
4.2 软引用
软引用是还有用但并不必须的对象。只有内存不足时,才会被回收掉。可以用SoftReference来实现软引用
4.3 弱引用
弱引用是非必须的对象,比软件引用还要弱,垃圾回收时就会回收掉。可以用WeakReference来实现弱引用
4.4 虚引用
虚引用被称为幽灵引用或幻影引用,是最弱的引用。无法通过虚引用来获取对象垃圾回收时会回收掉,用PhantomReference来实现虚引用
5 垃圾回收的算法
垃圾回收的算法主要有:标记清理算法、复制算法、标记整理法、分代收集算法
5.1 标记清理算法
标记清理算法是先将需要回收的对象打上标记,然后对标记的对象进行回收。但是容易产生碎片空间,使大对象无法存放且效率低
5.2 复制算法
复制算法是将内存分为两个区域,每次只使用其中的一块区域。当一块用完了触发垃圾回收,然后将活着的对象放到另一块区域,然后清理块这块区域。下次垃圾回收时将另一块区域的对象复制到这块,然后抹掉那块,循环使用。这种算法的好处是解决的内存的碎片问题,效率也更高。但是会造成内存浪费。
5.3 标记整理算法
标记整理算法与标记清理算法类似,但是标记整理算法会在清理完之后将对象往内存的一侧移动,对象与对象之间间隔。这样就不会造成碎片空间,但是效率比较慢。
5.4 分代收集算法
分代收集算法是大多数商用虚拟机使用的算法,由于一些对于在创建完之后很快就会死掉,只有一小部分对象会长时间保留,所以产生了分代收集算法。这个算法并没有新的内容,可以看做成上面几种的组合。它根据对象的存活时间的长短,将内存分为了新生代和老年代,这样就可以针对不同的区域采取对应的算法。
6 JVM的垃圾回收
6.1 JVM堆的结构
jvm将堆空间分为新生代与老年代, 其中新生代又分为了Eden区、form survivor区和to survivor区。新生代采用复制算法,老年代采用标记清除或者标记整理法
6.2 JVM垃圾回收的思路
jvm的垃圾回归大致的思路是:新产生的对象总是在eden区创建,然后当eden区没有足够空间时会触发一次新生代的垃圾回收(Minor GC),这次回收会把eden区的垃圾直接清理,并且将存活的对象移动到from survivor区。当再次进行新生代的垃圾回收时会把eden区的存活对象和from区的对象放入to区,并且从from区到to区的对象年龄加一岁。然后to区和from区的位置交换,即保证to区一直没有对象。如果年龄大于15(XX:MaxPretenuringThreshold 默认)的对象进入老年代。 如果老年代没有足够的空间时, 则会触发堆的垃圾回收(Full GC)。这个gc通常比Minor gc慢10倍。通常情况下会有STW(stop the world)发生,不过也和选择的垃圾收集器有关。
6.3 JVM垃圾回收的特殊情况
为避免新生代发生大量的内存复制,内存过大的对象直接进入老年代。
为了能更好地适应不同程度的内存情况,判断一个对象是否进入老年代的标准也并不只是年龄和大小,还有一种情况是:如果survivor中相同年龄的对象的占用内存大小总和大于survivor空间的一半,则年龄大于等于该年龄的对象直接进入老年代。
7 STW是什么
Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。
8 空间分配担保
8.1 什么是空间分配担保
简单点说是:就是当在新生代无法分配内存的时候,把新生代的对象转移到老生代,然后把新对象放入腾空的新生代。
详细说是:在发生Minor GC之前,虚拟机会检查老年代的最大可用的连续空间是否大于新生代的所有对象的总空间,如果大于则直接进行gc。否则会查看是否设置了允许空间分配担保(-XX:HandlePromotionFailure)如果为true,则会继续检查老年代的最大可用的连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行gc,但是这次gc是有风险的,如果小于或者为false,则直接进行Full GC
8.2 为什么要进行空间担保?
是因为新生代采用复制收集算法,假如大量对象在Minor GC后仍然存活(最极端情况为内存回收后新生代中所有对象均存活),而Survivor空间是比较小的,这时就需要老年代进行分配担保,把Survivor无法容纳的对象放到老年代。老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更多空间。
9 jvm中主要的垃圾收集器
新生代:Serial、ParNew、Parallel Scavenge、G1
老年代:CMS、Serial old、 Parallel old、G1
其中常用的组合有:Serial+Serial old、ParNew+CMS、Parallel Scavenge+Parallel old、Parallel Scavenge+Serial old、G1
9.1 串行收集器Serial/Serial old
Serial/Serial old是单线程的串行收集器,在垃圾回收时,会STW。这个收集器的优点是简单,对于单cpu,由于没有多线程的交互开销,可能更高效,是默认的Client模式下的新生代收集器。可以在jvm启动的参数里使用-XX:+UserSerialGC来使用这个收集器,会使用Serial+Serial old的组合
9.2 并行收集器ParNew
ParNew是多线程的并行收集器,在垃圾回收时也会STW。这个收集器的优点是在并发能力好的cpu环境里,它停顿的时间要比串行收集器短;但对于单cpu或者并发能力较弱的cpu由于多线程的交互开销,可能比串行回收器更差.是Server模式下首先的新生代收集器,目前它只和CMS一起使用。开启的方法是使用-XX+UserConcMarkSweepGC,会使用ParNew+CMS
9.3 并行收集器Parallel Scavenge
Parallel Scavenge/Parallel old是和ParNew很类似的收集器,但更关注吞吐量,能最高效率的利用CPU,适合运行后台应用。开启的方法是使用-XX+UserParallelGC,会使用Parallel Scavenge+Parallel old
9.4 CMS收集器
CMS(Concurrent Mark and Sweep 并发标记清除)收集器分为:初始标记:只标记GC Roots能直接关联到的对象;并发标记:进行GC Roots Tracing的过程;重新标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象;并发清除:并发回收垃圾对象。这个收集器在初始标记和重新标记阶段还是会发生STW。使用标记清除算法,多线程并发收集的垃圾收集器。这个收集器优点是低停顿、并发执行。不过并发执行对cpu资源压力大,而且无法处理在处理过程中产生的垃圾,还会导致内存碎片,可能触发Full GC。要开启时使用-XX:UserConcMarkSweepGC将使用ParNew+CMS+Serial Old的组合,Serial old将作为CMS出错的后备收集器
9.5 G1收集器
G1(Garbage-First)收集器:是一款面向服务端应用的收集器,与其它收集器相比,有如下特点:G1把内存划分成多个独立的区域(Region);G1仍采用分代思想,但它们不再是物理隔离,而是一部分Region的集合,且不需要Region是连续的;G1能充分利用多cpu、多核环境硬件优势,尽量缩短STW;G1整体上采用标记整理算法,局部是通过复制算法,不会产生内存碎片;G1的停顿可预测,能明确指定在一个时间段内,消耗在垃圾收集上的时间不能超过多长时间;根据回收价值的大小,G1会在后台维护一个优先列表,根据允许的时间来回收价值最大的区域,保证有限时间内的高效收集。G1收集器是可以同时回收新生代和老年代的收集器,使用-XX:+UserG1GC开启。
同时G1收集器也有相关的命令
-XX:MaxGCPauseMillis最大GC停顿时间
-XX:InitiatingHeapOccupancyPercent堆占用了多的时候触发GC
10 GC性能指标
吞吐量=应用代码执行的时间/运行的总时间(代码执行时间+gc时间)
GC负荷=GC的时间/运行的时间
暂停时间=发生stw的时间
gc频率=gc在一个时间段发生的次数
反应速度=从对象成为垃圾到被回收的时间
11 方法区的垃圾回收
实际上垃圾回收并不只包含堆的垃圾回收,虽然方法区占用的内存空间不多,但是将不用的空间回收正是垃圾回收器的功能所在,也是实现内存利用最大化的目的。
11.1 方法区中可回收的内容
方法区中可以被回收的内容包含常量与类
11.2 常量的回收
当一个常量没有被任何对象所引用,就认为是被可回收的
11.3 类的回收
1.类的所有实例已经被回收
2.类的类加载器已被回收
3.没有任何地方引用该类,没有任何反射使用该类