JVM虚拟机总结
读了周志明老师的《深入理解Java虚拟机:JVM高级特性与最佳实践》第三版,总结一下里面的知识点。一方面是知识储备更多一些,另外是也为接下来的面试准备一下。
全书分为13个章节,共5部分内容。我着重是看了jvm的内管管理、垃圾收集与内存分配策略、虚拟机故障工具和调优实战、类加载机制、Java内存模型以及线程安全锁这几个章节,一方面这几个章节里面的知识点在实际的工作里面有所适用,另外一方面是这里面的知识点基本都是面试经常面到的。
Java虚拟机把内存分为若干个不同的区域,包括程序计数器、虚拟机栈、本地方法栈、方法区和堆。方法区和堆属于所有线程共享的数据区,其他三个则是线程私有的。
程序计数器:它是当前线程所执行的字节码的行号指示器,此区域是唯一一个不会出现OutofMemoryError的区域
虚拟机栈:虚拟机栈是描述Java方法执行的线程内存模型:每个方法被执行时候,Java虚拟机都会创建一个栈帧,用户存储局部变量、操作数栈、动态链接、方法出口等信息。每个方法被调用直至执行完毕的过程,就对应的一个栈帧在虚拟机中从入栈到出栈的过程。如果线程请求的栈深度大于虚拟机所允许的深度,则会抛出StackOverflowError 异常
本地方法栈:本地方法栈和虚拟机栈作用是类似的,区别是虚拟机栈是服务Java(也就是字节码),而本地方法栈是服务native方法的
Java堆:对于Java程序来说,Java堆是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的区域,在虚拟机启动时创建,此区域的唯一目的就是存储对象。 接下来的基本都是在Java堆里面的操作,下面会在重点来说。当Java堆内存不足时候,虚拟机会抛出OutofMemoryError异常
方法区:方法区也是线程共享的,主要是存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等
了解了Java虚拟机的构成之后,接下来就是针对Java虚拟机的重点-Java堆来展开介绍了。Java里面有大量的对象,这些对象怎么在堆里面创建、回收、生命周期是怎么样的,这些都是Java堆的处理。
在Java里面,创建一个对象是通过new的方式来创建的,创建之后Java堆为其分配好内存的大小,当该对象不在使用或者怎么知道该对象不在使用后,Java回收期会对该对象进行回收呢? Java虚拟机通过GcRoot算法和引用计数算法来确定一个对象是否需要进行回收。
引用计数算法:在对象中添加一个引用计算器,每当有一个地方引用他时候,计数器加1,当引用失效时候,计数器减一。但其很难解决对象互相循环引用的问题
可达性分析算法:这个算法的思路是通过一系列成为Gc Roots 的跟对象作为起始点合集,如果某个对象到Gc Roots没有任何的引用链相连,则认为此对象不在使用。
当知道一个对象是否是出于要回收还是继续存活后,就可以对该对象进行垃圾回收了。虚拟机在对对象回收的过程中,使用了很多种垃圾回收器,随着计算机以及虚拟机的发展,垃圾回收器也纷繁多样,适用不同的场景。
垃圾收集算法目前都是基于这几种方式为基础的:
标记-清除算法(mark-sweep):算法分为标记和清除阶段,首先标记出所有需要回收的对象,在标记完成后,统一回收所有标记的对象。 该算法是最基础的收集算法,他的缺点主要有两个:第一个是执行效率不稳定,如果Java对象中含有大量的对象,而且其中大部分是需要回收的,这时会进行大量的标记和清除动作,导致标记和清除都会随着对象的数量增长执行效率的降低;第二个则是会产生内存空间碎片化问题,导致后面如果需要大对象分配时候,没有连续的内存空间而需要提前触发一次full gc
标记-复制算法:将可用内存按照容量划分为大小相同的两块,每次只使用一块,当这一块使用完之后,将还存活的对象拷贝到另一块上面,然后把已使用过的内存空间进行一次清理。如果内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但是对于少数的可回收的情况,则会很快,而且也没有空间碎片 。标记-复制算法的优缺点也很明显,优点是不会产生空间碎片,缺点则是内存的使用降低了
标记-整理算法(mark-compact):由于标记-复制算法存在内存使用率不高的问题,以及当有大量的对象都存活时候,复制算法和清除算法都有一些效率的问题,因此针对老年代的对象的死亡特征,提出来了标记-整理算法。标记整理算法的标记部分,和标记-清除算法一样,但是比标记之后不在进行清理,还是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。这样的优点很明显,首先是内存空间不在存在碎片化的问题,其次是也没有浪费内存。但是也有缺点:就是整理时候的移动,这种对象移动的操作必须全程暂停用户的应用程序才行。也就是 stop-the-world. 基于标记整理和标记清除算法的优缺点,是否移动都会存在弊端:移动则内存回收更加复杂,不移动则存在空间碎片导致内存分配更加复杂,从垃圾回收的停顿时间来看,不移动对应垃圾回收的停顿时间更短,但是从整个程序的吞吐量来说,移动对象则更划算。
基于以上几种垃圾回收的算法,出现了几个比较经典的垃圾收集器:从早期的Serial收集器,parNew/Parallel以及接下来的 cms/g1/,以及低延迟的 zgc/shenandoash收集器等。下面会介绍下每种收集器。
Serial收集器:Serial收集器是历史最悠久,最基础的收集器。这个收集器是单线程收集器。新生代使用标记-复制算法,老年代使用标记-整理算法
ParNew收集器:parNew收集器是Serial收集器的多线程版本,除了同时使用多条线程进行垃圾收集外,其他的 收集算法、stop the world、对象分配规则、回收策略等都和Serial收集器一样。
Paralel Scavenge收集器: Paralel Scavenge也是一款新生代收集器,同样是基于标记=复制算法实现,也能并行收集的多线程收集器,和ParNew收集器很相似,但是它的目标是达到一个可控制吞吐量的收集器,
吞吐量 = 运行用户代码时间/(运营用户代码时间+垃圾回收收集时间)
高吞吐量可以最高效率的利用服务器资源,尽快完成程序的运算任务。Paralel收集器会根据当前系统的允许情况,动态的调整以提供最合适的停顿时间或最大的吞吐量。这种方式称为垃圾收集的自适应的调整策略。
Serial Old 收集器:主要是Serial老年代版本,使用标记-整理算法
Parallel old收集器:是Paralel Scavenge的老年代版本,基于标记-整理算法
CMS收集器:cms 收集器是以最短回收停顿时间为目标的垃圾收集器。运作过程分为四个步骤:
初始标记:
并发标记
重新标记:
并发清除:使用并发-清除算法,会存在空间碎片化导致空间不连续
优点: 并发收集、低停顿、 缺点:由于在并发标记阶段,会占用一部分系统资源,导致总吞吐量降低,特别的处理器的核数比较低(低于4个)的时候。另外就是由于使用了标记-清除算法,会产生空间碎片导致空间不连续的问题。特别是在老年代时候,会出现明明有很大的内存空间,但是无法找到足够大的连续空间而触发一次full GC
Garbage first(G1):g1收集器是面向局部收集以及基于Region的内存布局形式。R1把整个内存区域分为了多个Region,每个region 里面 有suvive/edon区域,以及专门存储大对象的Humongous区域,在做垃圾收集时候,他是通过回收不同的region区域来进行局部垃圾回收的,这种方式保证了G1收集器在有限时间获取尽可能高的收集效率。
G1收集器也是分为四个步骤:
初始标记:
并发标记:
最终标记:
帅选标记:
g1整体上是采用标记-整理算法来实现的,但是局部的region上看又是基于标记-复制算法来的,相比cms来说,g1不会产生空间碎片且垃圾回收时间可控。但是相比cms也有一些缺点,比如G1为了垃圾收集产生的内存占用,以及程序运行时的额外负载,都要比cms要高。
低延迟收集器:shenandoan ,基于Region布局,号称收集时间不超过10ms,但是目前尚未实现
ZGC: 基于标记-整理算法,开源给了OpenJDK,zgc出现在jdk11 上。
垃圾回收的 可视化处理工具
1.JDK自带的JCONSOLE以及使用dump 来分析,实际的工作上大部分是使用arthas 来分析。这个章节没有细看