JVM底层原理

Posted 最小的帆也能远航

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM底层原理相关的知识,希望对你有一定的参考价值。

1.基础

1.JVM整体架构

JVM(虚拟机):指以软件的方式模拟具有完整硬件系统功能、运行在一个完全隔离环境中的完整计算机系统 ,是物理机的软件实现。

  • 作为一种编程语言的虚拟机,实际上不只是专用于Java语言,只要生成的编译文件匹配JVM对加载编译文件格式要求,任何语言都可以由JVM编译运行。比如kotlin、scala等。
  • b.jvm有很多,不只是Hotspot,还有JRockit、J9等等

2.JVM内存结构

3.类加载过程

1.加载:在硬盘上查找并通过IO读入字节码文件
2.连接:执行验证、准备、解析步骤
3.验证:校验字节码文件的正确性
4.准备:给类的静态变量分配内存,并赋予默认值
5.解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符合引用,比如main())替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是指在程序运行期间完成的符合引用替换为直接引用
6.初始化:对类的静态变量初始化为指定的值,执行静态代码块

4.类加载器

启动类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,如JRE目标下的rt.jar,charsets.jar等
扩展类加载器:负责加载支撑JVM运行的位于JRE的lib 目录下的ext扩展目录中的jar类包
系统类加载器:负责加载ClassPath路径下的类包,主要加载我们自己写的那些类
用户自定义加载器:负责加载用户自定义路径下的类包

5.类加载机制–双亲委派机制

所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。

沙箱安全机制+避免类的重复加载

沙箱安全机制:比如自己写的String.class类不会被加载,这样可以防止核心库被随意篡改
避免类的重复加载:当父ClassLoader已经加载了该类的时候,就不需要子ClassLoader再加载一次

2.内存模型

1.虚拟机栈

Java线程执行方法的内存模型,一个线程对应一个栈,每个方法在执行的同时都会创建一个栈帧(用于存储局部变量表,操作数栈,动态链接,方法出口等信息)不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致。

2.方法区

存放常量,静态变量,类元信息

类的所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在这里定义。
简单来说,所有定义的方法的信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+运行时常量池都存在方法区中,虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是为了和Java的堆区分开
装载子系统将class文件加载到方法区,以类元形式存放

3.本地方法栈

本地方法栈也是线程私有的内存区域,与Java栈作用比较相似,不同之处在于该区域主要是保存native方法相关的数据。登记native方法,在Execution Engine执行时加载本地方法库。
Native方法是非Java语言编写的方法。

4.堆

虚拟机启动时自动分配创建,用于存放对象的实例,几乎所有对象都在堆上分配内存,当对象无法在该空间申请到内存时将抛出OutOfMemoryError异常。同时也是垃圾收集器管理的主要区域。

4.1 新生代(Young Generation)

类出生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,
结束生命。
新生代分为两部分:伊甸区(Eden space)和幸存者区(Survivor space),所有的类都是在伊甸区被new出来的。
幸存区又分为From和To区。当Eden区的空间用完是,程序又需要创建对象,JVM的垃圾回收器将Eden区进行垃圾回收(Minor GC),将Eden区中的不再被其它对象应用的对象进行销毁。然后将Eden区中剩余的对象移到From Survivor区。若From Survivor区也满了,再对该区进行垃圾回收,然后移动到To Survivor区。

4.2 老年代(Old Generation)

新生代经过多次GC仍然存货的对象移动到老年区。若老年代也满了,这时候将发生Major GC(也可以叫Full GC),
进行老年区的内存清理。若老年区执行了Full GC之后发现依然无法进行对象的保存,就会抛出
OOM(OutOfMemoryError)异常

4.3 元空间(Meta Space)

在JDK1.8之后,元空间替代了永久代,它是对JVM规范中方法区的实现,区别在于元数据区不在虚拟机当中,而是用的本地内存,永久代在虚拟机当中,永久代逻辑结构上也属于堆,但是物理上不属于。

3.JVM内存分配和回收策略

开始之前先将JVM的三种运行模式和对象逃逸分析进行简单解释
JVM的3种运行模式
a.解释模式(Interpreted Mode):只使用解释器(-Xint 强制JVM使用解释模式),执行一行JVM字节码就编译一行为机器码。
b.编译模式(Compiled Mode):只使用编译器(-Xcomp JVM使用编译模式),先将所有的JVM字节码一次编译为机器码,然后一次性执行所有机器码。
c.混合模式(Mixed Mode):(-Xmixed 设置JVM使用混合模式)依然使用解释模式执行代码,但是对于一些“热点”代码采取编译器模式执行,这些热点代码对应的机器码会被缓存起来,下次执行无需再编译。JVM一般采用混合模式执行代码。
对象逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如,作用调用参数传递到其他地方中。(开启:-XX:+DoEscapeAnalysis 关闭: -XX:-DoEscapeAnalysis )。

1.对象优先在Eden区分配

大多数情况下,对象在新生代中Eden区分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC

Minor GC和Full GC的区别

Minor GC/Young GC:指发生新生代的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
Major GC/Full GC:一般回收老年代,年轻代,方法区的垃圾,Major GC比Minor GC慢10倍以上

2.长期存活的对象将进入老年代

1.虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应该放在新生代,哪些对象应放在老年代。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
2.如果对象在Eden出生并经过一次Minor GC后仍能存活,并且能被Survivor容纳的话,将被移到Survivor空间中,并将对象年龄设为1.对象在Survivor中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁),就会被晋升到老年代。虚拟机提供了-XX:MaxTenuringThreshold来进行设置。

a.Minor GC后存活的对象Survivor区放不下
这种情况会把存活的对象部分挪到老年代,部分还可能会放在Survivor区

b.大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)
 JVM参数-XX:PretenureSIzeThreshold可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在Serial和ParNew两个收集器下有效。

例如: -XX:PretenureSizeThreshold=1000000 –XX:+UseSerialGC
为什么要这样做呢?

 为了避免大对象分配内存时的复杂操作而降低效率。

c.对象动态年龄判断

 当前放对象的Survivor区域里(其中一块区域,放对象的那块Survivor区),一批对象的总大小大于这块Survivor区域内存大小的50%,那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了。

1.例如Survivor区域里现有一批对象,年龄1+年龄2+···+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n以上的对象都放在老年代。
2.Minor GC 之后触发

d.老年代空间分配担保机制

 1.年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间。
 2.如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象),就会看一个”-X:HandlePromotionFailure”(JDK1.8默认就设置了)的参数是否设置了,如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每次minor gc后进入老年代的对象平均大小。
 3.如果上一步结果是小于或者参数没有设置,那么就会触发一次Full GC,对老年代和年轻代一起回收一次垃圾。
 4.如果回收完还是没有足够空间存放新的对象就会发生“OOM”

4.判断对象/类是否可以被回收

1.引用计数法

a.给对象添加一个引用计数器,每当有一个地方引用,计数器就加1。当引用失效,计数器就减1。任何时候计数器为0的对象就是不可能再被使用的。
b.这个方法实现简单,效率高,但是目前主流的虚拟机中没有选择这个算法来管理内存,最主要的原因是它很难解决对象之前相互循环引用的问题。

对象之间的相互引用

除了对象a和b相互引用着对方之外,这两个对象之间再无任何引用。但是它们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数器法无法通知GC回收器回收它们。

2.可达性分析算法

a.这个算法的基本思想就是通过一系列的称为”GC Roots“的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象是不可用的。
b.GC Roots根节点:类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等等

--------------------------------------判断类是否可以被回收-----------------
需要满足以下三个条件:

 1.该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
 2.加载该类的 ClassLoader 已经被回收。
 3.该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述3个条件的无用类进行回收,这里仅仅是”可以“,而并不是和对象一样不适用了就必然会被回收。

5.垃圾收集算法

1.标记-清除算法

它是最基础的收集算法,这个算法分为两个阶段,“标记”和”清除“。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

a.效率问题,标记和清除两个过程的效率都不高
b.空间问题,标记清除后会产生大量不连续的碎片

2.复制算法

为了解决效率问题,复制算法出现了。它可以把内存分为大小相同的两块,每次只使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块区,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

a.内存使用不如标记-清除算法
b.适合年轻代

3.标记-整理算法

根据老年代的特点提出的一种标记算法,标记过程和“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行回收,而是让所有存活的对象向一端移动,然后直接清理掉边界以外的内存。

4.分代收集算法

现在的商用虚拟机的垃圾收集器基本都采用"分代收集"算法,这种算法就是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
 在新生代中,每次收集都有大量对象死去,所以可以选择复制算法,只要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率时比较高的,而且没有额外的空间对它进行分配担保,就必须选择“标记-清除”或者“标记-整理”算法进行垃圾收集。

6.垃圾回收器

所有回收算法都是为实现垃圾回收器服务的,而垃圾回收器就是内存回收的具体实现。
目前HotSpot虚拟机用到的垃圾回收器如下图所示。注意只有两个回收器之间有连线才能配合使用

a.并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
b.并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序继续运行,而垃圾收集程序运行于另一个CPU上。

1.串行垃圾收集器 — Serial/Serial Old

串行收集器收集器是一个单线程收集器。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
串行的垃圾收集器有两种,Serial与Serial Old,一般两者搭配使用。新生代采用Serial,是利用复制算法;老年代使用Serial Old采用标记-整理算法。

2.并行垃圾收集器 — ParNew

ParNew收集器(-XX:+useParNewGC)
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。
新生代采用复制算法,老年代采用标记-整理算法。

它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。

3.并行垃圾收集器 — Parallel Scavenge/ Parllel Old

a.Parallel Scavenge收集器(-XX:+UseParallelGC (年轻代) –X:+UseParallelOldGC(老年代))
关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。吞吐量=CPU中用于运行用户代码的时间/(运行用户代码时间+垃圾收集时间)
b. Parllel Old:Parallel Scavenge的老年代版本。JDK 1.6开始提供的。通过-XX:+UseParallelOldGC参数使用Parallel Scavenge + Parallel Old器组合进行内存回收

4.CMS收集器

CMS收集器(-XX:+useConcMarkSweepGC(老年代))

 CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

整个过程分为四个步骤:

主要优点:并发收集,低停顿。但是它有下面几个明显的缺点:

 1.对CPU资源敏感(会和服务抢资源)
 2.无法处理浮动垃圾(在并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次GC再清理)
 3.CMS是基于“标记 -清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。当然通过参数-XX:UseCMSCompactAtFullCollection可以让JVM在执行完标记-清除后再做整理。
 4.执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清除阶段出现,一边回收,系统一边运行,也许没回收完就再次触发full GC,也就是“Concurrent Mode Failure”,此时会进入“Stop The World”,用Serial Old垃圾收集器来回收。

相关参数:

1. -XX:+UseConcMarkSweepGC:启用CMS
2. -XX:ConcGCThreads:并发的GC线程数
3. -XX:+UseCMSCompactAtFullCollection:Full GC之后做压缩整理
4. -XX:CMSFullGCsBeforeCompaction:多少次次Full GC之后压缩一次,默认是0,代表每次Full GC之后都会压缩一次
5. -XX:CMSInitiatingOccupancyFraction:当老年代使用达到该比例时会触发Full GC(默认是92,百分比)
6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阀值( -XX:CMSInitiatingOccupancyFraction 设定的值),如果不指定,JVM在第一次使用设定值,后续会自动调整。
7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor GC,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时80%都在标记阶段。


5.G1收集器

G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。它具备以下特点:

1.**并行与并发:**G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
2.**分代收集:**虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
3.**空间整合:**与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
4.**可预测的停顿:**这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。
G1分区的概念

 G1的堆区在分代的基础上,引入分区的概念。G1将堆分成了若干Region(“分区”)。(这些分区不要求是连续的内存空间)Region的大小可以通过G1HeapRegionSize参数进行设置,其必须是2的幂,范围允许为1Mb到32Mb。
 JVM的会基于堆内存的初始值和最大值的平均数计算分区的尺寸,平均的堆尺寸会分出约2000个Region。分区大小一旦设置,则启动之后不会再变化。

a.Humongous区:用来存储巨型对象(占用了Region容量的50%以上的一个对象)。
如果一个H区装不下一个巨型对象,则会通过连续的若干H分区来存储。因为巨型对象的转移会影响GC效率,所以并发标记阶段发现巨型对象不再存活时,会将其直接回收。
b.分区可以有效利用内存空间,因为收集整体是使用“标记-整理”,Region之间基于“复制”算法,GC后会将存活对象复制到可用分区(未分配的分区),所以不会产生空间碎片。

G1收集器的运作大致分为以下几个步骤:

 初始标记
 并发标记
 最终标记
 筛选回收

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
—————————————————————————————————
怎么选择垃圾收集器?

 1.优先调整堆的大小让服务器自己来选择
 2.如果内存小于100m,使用串行收集器
 3.如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
 4.如果允许停顿时间超过1秒,选择并行或者JVM自己选
 5.如果响应时间最重要,并且不能超过1秒,使用并发收集器
 6.官方推荐G1,性能高。

 1.优先调整堆的大小让服务器自己来选择
 2.如果内存小于100m,使用串行收集器
 3.如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
 4.如果允许停顿时间超过1秒,选择并行或者JVM自己选
 5.如果响应时间最重要,并且不能超过1秒,使用并发收集器
 6.官方推荐G1,性能高。

以上是关于JVM底层原理的主要内容,如果未能解决你的问题,请参考以下文章

JVM参数设置分析

[转]JVM系列三:JVM参数设置分析

JVM系列三:JVM参数设置分析

耗时几个月,终于找到了JVM停顿十几秒的原因

JVM底层原理

如何设置jvm启动参数