JVM-运行时数据区
Java字节码是在jre中运行的,JVM是jre的核心组成成分承担着字节码的解释和执行工作。JVM主要包含3个部分,类加载子系统,运行时数据区,执行引擎,下面系统的介绍下JVM的运行时数据区。
运行时数据区介绍
image.png本地方法栈:记录Native(java 调用非java代码的接口)方法(Native声明的方法),Execution Engine执行时加载本地方法库
程序计数器:是一个指针,指向方法区中的方法字节码(用来存储下一条要执行的指令地址)。由Execution engine来读取下一条指令。
方法区:类所有的字段和方法的字节码,以及一些特殊方法构造函数,接口代码都定义在这个区域。(静态变量/常量/类构造方法/类接口信息/运行时常量池)
Java 虚拟机栈:Java线程执行的内存模型,一个线程对应拥有一个栈,每个方法在执行时都对应一个栈帧(用来保存局部变量表,操作数信息,动态链接,方法出口信息)
image.png
堆:虚拟机启动时创建,用于存储对象的的实例,几乎所有的对象都在堆 上分配内存。当对象无法再申请空间时就会抛出out of memory异常。同时也是垃圾回收主要管理区域。可通过xmx-xms来控制堆内存的大小
image.png
JVM垃圾回收算法与垃圾回收器
垃圾回收,是垃圾才会被回收,不是垃圾就应该保留在内存,那如何判断内存中的一个对象是不是垃圾那?就是通过下面2种算法,1:引用计数法,2:GC Root法
引用计数法
每个对象在创建后就会给该对象分配一个计数器,当该对象被引用时计数器加1,当引用取消时计数器减1。 计数器为0时则表示该对象可以被回收。这种计算方式简单也比较高效,但对于有循环引用的对象就没有办法回收了。例如:
对象A 引用了对象B的实例,同时对象B也引用了对象A的实例
A. instanceB = B;
B. instanceA = A;
当A,B 都没有再被其他对象引用时,他俩依然不能被回收,因为彼此互引。所以引用计数法就没有办法解决这个问题。
可达性分析法(GC Root)
1:可达性分析算法
通过一系列被称为”GC Root”的对象作为起始点,开始向下依此遍历,遍历走过的路径称为引用链,当某个对象到GC Root没有引用链时,就表示GC Root到这个对象不可以到达,既:该对象可以被回收。(如下图,OBJ5,6,7即为可被回收的对象)
2:可以作为GC Root 的对象:
1): 虚拟机栈(栈帧中的本地变量)中引用的对象
2): 方法区中类静态属性引用的对象
3): 方法区中常量引用的对象
4): 本地方法栈(Native方法)引用的对象
3:Finalize:
当对象没有到GC Root的引用链时,并不一定会被回收(可能有机会复活)。当对象不可达时,GC会判断该对象是否重写了finalize方法,如果没有重写直接回收。如果重写了GC会检查该对象的Finalize方法是否已经被执行过,如果没有执行过,则放入F-Queue队列中。然后GC会再启动一个低优先级的线程来执行这个队列中每个对象的Finalize方法,执行完Finalize方法后会再一次检查该对象到GC Root是否可达,如果这时存在了引用链则复活该对象,否则直接回收该对象。
image.png
方法区垃圾回收
1:回收废弃常量(没有任何地方在使用该常量),
2:没有被引用的变量,
3:无用类(1:该类的所有对象没有再 被引用,2:该类所有实例都被回收,3:加载该类的class load已被回收(不能再通过反射创建出来的类))
Heap 垃圾收集算法
垃圾收集算法主要有四类,1:标记-清理 算法(效率低,会产生内存碎片) 2:复制算法(效率高,但浪费内存)3:标记整理算法 4:分代收集(年轻代,老年代)
1:标记-清理 算法
这种算法是最基础的垃圾回收算法,因为后面的算法都是基于这个算法优化出来的。标记-清理算法主要二个步骤,
第一步就是标记,GC会先标记出可以回收的对象(标记的算法就是上面提到的GC Root方式),
第二步就是清理, 标记完成后,GC 就会统一对所有标记的对象进行清理回收。
这个算法的不足
第一:效率低,因为标记和清理回收都比较耗时
第二:在清理的时候会把所有标记的都回收了,这些对象占用的内存大小各不相同,位置 也是随机分配,就会导致出现很多内存碎片,当我们需要分配大对象时,就没有足够的连续内存就会导致又一次的垃圾回收动作。
从图中可以看出,当清理完成后,最大连续内存块却只有3个,但是可用的确有很多。如果这时我们需要一个连续4个大小的内存块就会再一次触发垃圾回收动作。
2:复制算法:
因为标记-清理算法会产生大量内存碎片,所以出现了复制算法来解决这个缺陷。复制算法是将内存分为2个相等的部分,每次只使用其中的一部分。当内存不够用时就将活着的对象统一copy到另一块内存里,在对剩下的对象统一清理。
image.png
image.png
但是这样的算法会让内存的利用率变低,因为有1/2的内存都处于备用状态。
后面发现在java堆的年轻代中,对象基本都是“朝生夕死”的,所以将内存分为了3部分,Eden和2个survivor区(比例:8:1:1),每次使用Eden和一个survivor区,当内存回收的时候就把Eden和survivor中任然存活的对象copy到另外一个survivor中再将剩下的对象进行统一清理。这样的设计就可以更充分的利用内存空间(只有10%的内存处于备用状态)
image.png
3:标记-整理算法
标记整理算法是在标记清理的基础上衍生出来同时该算法也解决了标记 清理算法带来的内存碎片问题。标记整理算法主要包括三步,第一步就是标记:标记出所有需要回收的对象,第二步:将任然存活的对象全部移动内存的某一端, 第三步:清理掉这一端边界外的所有内存。
image.png
4:分代收集算法:
分代收集算法并不是一种新的回收思路,而是依据堆内存年轻代和老年代的各自特点(年轻代:大部分对象都是朝生夕死,每次回收都只有小部分对象可以保留下来。老年代:大部分的对象存活率都比较高,每次回收可能都只有少量的对象是GC Root 不可达的。)
所以依据他们各自的特点,对于年轻代就提出了使用复制算法,因为只需要付出一点对象复制的成本就可以了。对于老年代就采用标记整理算法,只需要花费少量时间来移动存活对象的存储位置即可
垃圾收集器
引用计数法和GC Root可达法是判断一个对象是否可回收的方法,回收算法是回收无用对象的方法,那垃圾收集器就是回收算法的产品。
垃圾收集器总共有6种,1:serial (1: 收集时候会暂停所有线程,2:简单高效,单线程,3:年轻代是复制算法,老年代是标记整理算法) ,2:parNew (多线程), 3:parallel Scavenge(1: 年轻代是复制算法,2:吞吐量优先,3:自适应算法) , 4:Serial old (老年代标记整理算法) , 5: CMS (1:标记清理算法,2:并发收集-低停顿) ,6:G1(1: 并发和并行,2:分代收集,3:标记整理算法,4:将堆分为多个大小相等的region)
1:serial
Serial是java中最基础也是最早的一代收集器,年轻代使用的复制算法,因为年轻代的对象存活率比较低。老年代使用的是标记整理算法,防止内存碎片的产生。
Serial的优点是简单高效,因为Serial是单线程回收垃圾可以省去多线程切换的时间开销。
Serial的缺点 就是他是单线程的,一方面就是在做垃圾回收时只有一个线程来回收和清理不可达的那些对象。另一方面就是他在做垃圾回收时会暂停掉所有的用户线程,等待垃圾清理结束再恢复这些线程的让其正常工作。当然这样的用户体验效果是差的。
2:ParNew
ParNew是在Serial收集器的基础上改造成了多线程(回收策略,算法,参数设置和Serial都是一样)。年轻代使用的也是复制算法,老年代也是用的标记整理算法。同时ParNew在回收内存的时候也会把所有的用户线程暂停掉,等待GC结束后再恢复。
在单核cpu 上,Serial就比较占优势,因为充分利用cpu,而且省去了多线程之间切换花费的时间
在多核cpu上,ParNew就比较占优势,多个线程可以并行执行回收任务。可以减少执行时间(缩短用户线程的暂停时间),缩短用户线程停顿时间,增强用户体验感,所以ParNew更适合于与用户交互的程序(Client端)
3: parallel Scavenge
Parallel Scavenge是一款新生代垃圾回收器,它也是多线程并行回收的同时采用的也是复制算法。从表面上看它和parNew收集器差不多。但是他俩的目标是不一样的。ParNew的目标是尽量缩短用户线程的停顿时间,而Parallel Scavenge的目标是提高吞吐量(又被称为“吞吐量收集器”)。提高吞吐量更高效的利用cpu所以Parallel Scavenge更适合于后台程序(Server端)。
吞吐率 = cpu 运行用户代码时间 /(= cpu 运行用户代码时间 + 垃圾回收时间)
Parallel Scavenge 会根据系统的运行情况收集性能监控信息,动态设置Eden和survivor的比例大小以及进入老年代的年龄。这样我们就不用手动设置这些参数只需要设置好堆的大小就可以了。(需要我们手动打开Parallel Scavenge自适应策略机制的开关)
4:Serial old
Serial old就是年轻代收集器Serial 的老年代版本,Serial old是老年代的收集器,同样采用的是标记整理算法,单线程回收垃圾并且在回收垃圾时会暂停掉所有的用户线程。
5:Parallel old
Parallel old是年轻代收集器Parallel scavenge的老年代版本,Parallel old是老年代的收集器,采用的标记整理算法,同样是多线程并行处理垃圾回收注重的吞吐量和cpu的利用率。
Parallel old 与Parallel scavenge的组合使得注重的吞吐量和cpu的利用率这种思想在多多核cpu上得到了充分的发挥。 在 注重的吞吐量和cpu的利用率的资源场合中更具有了优势。
6 : CMS
CMS 是一款以降低用户线程暂停时间为目标的并发垃圾收集器。实现的是标记清理算法,重视的是服务的响应速度,从而提高用户体验。主要包括4个阶段,分别是初始标记,并发标记,重新标记,并发清理。
在初始标记阶段,只是标记直连的首个元素,所以速度会很快,重新标记也只是修正并发标记后标记了发生引用变化的对象,所以速度也比较快。唯一耗时的操作就并发标记和标记清理算法,但是这2个阶段都是采用的并发方式,用户线程停顿的时间也很短,基本用户感知不到。CMS是一个并发收集,低停顿的优秀垃圾收集器。
缺点:
当然CMS也是有缺点的,
Cpu的占用率
CMS默认启动的回收线程数是(CPU数量+3)/4
当我们的cpu趋于无穷大时,那(CPU数量+3)就趋于cpu数量,那 cpu的利用率 ((cpu+3)/4cpu) 就趋于 1/4 那并发收集的线程就只占用了cpu的25%,但当cpu为一个的时候,那cpu的利用率((cpu+3)/4 cpu)就趋于3/4,这样就导致一个cpu基本都去做垃圾回收了,那用户线程的停顿时间就变大了。
浮动垃圾
在CMS并发标记阶段没能标记到新产生的垃圾被称为“浮动垃圾”
CMS在并发标记阶段是和用户线程一起工作的,可能会由于线程交替的顺序问题导致一些新产生的垃圾没能被标记到,等到重新标记阶段只是处理上一阶段标记了的数据,这样就导致那些新产生的垃圾留在了内存中,只能等待下一次的垃圾清理。
内存碎片
CMS使用的垃圾回收算法是标记清理算法。只会标记出现在存活的对象,然后清理掉可回收的对象,不会做内存的整理。
promotion failed & concurrent mode failure
promotion failed
在minor GC后有对象要复制到Survivor Space,发现survivor放不下,又准备移动到老年代中,而此时老年代没有足够连续的内存空间来存在这些对象(多数情况是因为老年代有足够的空间,但没有足够连续的空间导致的)。从而抛出的promotion failed
concurrent mode failure
CMS在并发回收内存阶段产生的,gc线程正在回收的时候,有用户线程要向老年代存入对象,发现此时老年代内存不不足,而引发的concurrent mode failure
解决promotion failed & concurrent mode failure的方法
1:调节老年代空间,
2:使用标记整理算法清楚内存碎片
7:G1
G1和CMS一样也是以提高吞吐量和降低用户线程等待时间为目标的垃圾收集器。G1是一个分步分代的并行垃圾收集器,同样在全局标记阶段时候采用的并发方式以此降低用户线程的停顿时间。
1:G1的堆内存布局
G1和其他的收集器一样,依然保留着年轻代,老年代的说法,但是在实际的产品中却又很大的区别,因为G1中的年轻代和老年代不在是连续的内存布局并通过物理进行隔离,他是将整个堆都切割成大小相等的内存块(region),一些region被标记为Eden区,一些被标记为survivor区,一些被标记为old区(这些区可能都不是连续的)
image.png
当G1在回收的时候就回去判断每个region的回收价值如:回收这个region可以释放的空间大小,回收这个region话费的时间值来判断每个region的回收优先级从而使得G1在回收垃圾的时候可以用最短的时间来达到最大的回收效率。
G1收集器的运作
年轻代回收:
年轻代的回收比较简单,就是复制算法,将存活的对象复制到Survivor或者晋升到老年代中,将可回收对象直接擦除即可。
老年代回收:
老年代的回收比较复制但流程和CMS有些类似包括4步:
image.png
第一步:初始标记,主要是标记出老年代中被GC Root直连的对象和老年 代中被年轻代中对象直接引用的对象。需要停顿线程,但是时间很短。
第二步:并发标记, 这一步主要是对初始标记的对象进行可达性分析,找出堆中所有存活的对象。这个阶段花费的时间比较长,所以采用的是和用户线程并发执行,以减少用户线程停顿时间 。因为是并发执行,所以有一些已经标记的记录会发生变化,这时候JVM会把这些发生了变化的记录保存在Set logs线程中。
第三步:最终标记,因为并发标记是和用户线程并发执行的由线程交替问题导致并发标记阶段标记的记录发生变动,所以这一步就对这些发生了变化的对象进行修正.完成对老年代所有存活对象的标记任务。最终标记阶段就是将Set Logs数据合并到Set(每一个region都有一个remembered Set,里面存放的该region的所有引用信息)集合中,这一步会停顿用户线程但可以并行执行。
第四步:筛选回收,对各个region的回收价值和成本进行排序,再依据用户期望的GC时间来制定回收计划,可能只回收 一部分region区域,时间是可以通过参数控制。
JVM的常见参数解析
序号 | 参数 | 功能 |
---|---|---|
1 | UseSerialGC | 虚拟机运行在client模式下的默认值,打开后使用Serial + Serial Old组合收集器 |
2 | UserParNewGC | 打开后使用ParNew + Serial Old 组合收集器 |
3 | UserConcMarkSweepGC | 打开后使用ParNew + CMS + Serial Old组合收集器, |
4 | UseParallelOldGC | 虚拟机运行在Server模式下的默认值,打开后使用Parallel Scavenge + Serial Old组合收集器 |
5 | SurvivorRatio | 年轻代Eden : survivor = 8:1 |
6 | PretenureSizethreshold | 直接晋升到老年代的对象大小,大于这个值得对象直接进入老年代1 |
7 | MaxTenuringThreshold | 晋升到老年代的对象年龄,每坚持过一次Minor GC年龄加11当超过这个值后就将该对象晋升到老年代中 |
8 | ParallelGCThreads | 设置并行GC是运行内存的线程数 |
9 | GCTimeRatio | GC时间占总时间的比例,仅在Parallel Scavenge收集器下有效 |
10 | MaxGCPuseMillis | GC的最大停顿时间,仅在Parallel Scavenge收集器下有效 |
11 | CMSInitiatingOccupancyFraction | 设置CMS在老年代空间被使用多少后触发垃圾回收,仅在CMS收集器下有效 |
12 | UseCMSCompactAtFullCollection | 设置CMS在完成垃圾回收后是否要进行一次内存碎片整理,仅在CMS收集器下有效 |
13 | UseFullGCcsBeforeCompaction | 设置CMS在进行若干次垃圾回收后再启动一次内存碎片整理,仅在CMS收集器下有效 |
查看JVM 使用的垃圾回收器命令:
java -XX:+PrintCommandLineFlags -version
image.png