JVM性能调优理论学习记录

2 篇文章 0 订阅

JVM内存结构

JVM内存结构

堆
从持久度 ——> 元空间,在原先的持久代时经常会遇到OOM

虚拟机栈

虚拟机栈

虚拟机栈管理Java方法

本地方法栈

本地方法栈管理的是native方法(C代码)

程序计数器

程序计数器:用来记录各个线程字节码的地址,像分支、循环、跳转、异常、线程恢复等等操作,都需要依赖程序计数器。由于Java是一个多线程的语言,当执行的线程数量超过CPU核心的时候,线程之间就会根据时间片去争抢资源。

方法区

方法区
静态常量池(也叫class文件常量池)主要存放:
字面量:如文本字符串、final修饰的常量
符号引用:例如类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
运行时常量池:
当类加载到内存后,JVM就会将静态常量池中的内容放到运行时常量池;运行时常量池存储的主要是编译期间生成的字面量、符号引用等等。
字符串常量池:
也可以理解成运行时常量池分出来的一部分,类加载到内存时,字符串,会到字符串常量池。

类加载过程

类加载过程

加载

1.读取二进制流
2.转为方法区数据结构,并放到方法区
3.在Java堆中产生java.long.Class对象

链接-验证
①验证class文件是不是符合规范

文件格式验证:
1.是否是以0xCAFEBABE开头
2.版本号是否合理

②元数据验证

1.是否有父类
2.是否继承了final类(final类不能被继承,如果继承了就有问题)
3.非抽象类实现了所有抽象方法

③字节码验证

1.运行检查
2.栈数据类型和操作码操作参数吻合(比如栈空间只有2个字节,但其实却需要大于2字节,此时就认为这个字节是有问题的)
3.跳转指令指向合理位置

④符号引用验证

1.常量池描述类是否存在
2.访问的方法或字段是否存在且有足够的权限

可以使用-Xverify:none关闭验证。 配置IDE的虚拟机配置即可

连接-准备

为类的静态变量分配内存,初始化为系统的初始值。
final static修饰的变量:直接赋值为用户定义的值,比如private finale static int temp = 123,直接赋值123。
对于static变量,初始值依旧是0,比如:static int temp = 123,该阶段值依旧是0。

连接-解析

符号引用转换成直接引用

初始化 - 1

执行 < clinit > 方法,clinit方法由编译器自动收集类里面的所有静态变量的赋值动作及静态语句块合并而成,也叫类构造器方法。
初始化的顺序和源文件中的顺序一致
子类的< clinit > 被调用前,会先调用父类的< clinit >
JVM会保证clinit 方法的线程安全性

初始化 - 2

初始化时,如果实例化一个新对象,会调用< init > 方法对实例bianl进行初始化,并执行对应的构造方法内的代码。

编译器优化

字节码如何运行

①解释执行:由解释器一行一行翻译执行
1.优势在于没有编译的等待时间
2.性能相对差一些

②编译执行:把字节码编译成机机器码,直接执行机器码
1.运行效率会高很多,一般认为比解释执行快一个数量级
2.带来了额外的开销(内存,CPU开销)

一般情况下由解释器解释执行
当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会认为这些代码是“ 热点代码 ”。为了提高热点代码的执行效率,会即时编译器(JIT),把这些热点代码编译成与本地平台相关的机器代码,并进行各层次的优化

HotSpot的即时编译器-C1

1.一个简单快速的编译器
2.主要关注局部性的优化
3.适用于执行时间较短或对启动性能有要求的程序。如GUI应用对界面启动速度就有一定要求。
4.也被称为Client Compiler

HotSpot的即时编译器-C2

1.是为长期运行的服务器端应用程序做性能调优的编译器
2.适用于执行时间较长或对峰值性能有要求的程序
3.也被称为Server Compiler

分层编译-1

自JDK7之后引入分层编译
级别:
0:解释执行
1:简单C1编译:会用C1编译器进行一些简单的优化,不开启Profiling
2:受限的C1编译:仅执行带 方法调用次数 以及 循环回边执行次数 Profiling的C1编译
3:完全C1编译:会执行带有所有Profiling的C1代码
4:C2编译:使用C2编译器进行优化,该级别会启用一些编译耗时较长的优化,一些情况下会根据性能监控信息进行一些非常激进的性能优化

级别越高,启动应用越慢,优化的开销越高,峰值性能也越高。

如何找到 热点代码

1.基于采样的热点探测
周期检查各个现场的栈顶,如果发现某些方法老是出现在栈顶,说明这是一个热点方法。

2.基于计数器的热点探测
为每个方法(甚至是每个代码块)建立一个计数器,统计执行的次数,如果超过一定阈值,就认为它是热点方法。(HotSpot用的就是该方法)

HotSpot内置计数器-1

方法调用计数器
用于统计方法被调用的次数,在不开启分层编译的情况下,在C1编译器下的默认阈值是1500次,在C2模式下是10000次。(可以使用-XX:CompilerThreshold=X指定阈值)

HotSpot内置计数器-2

回边计数器
1.用于统计一个方法内循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令被称为 “回边”。在不开启分层编译下,C1编译器下默认阈值是13995,C2默认为10700。(可以使用-XX:OnStackReplacePercentage=X指定阈值)
2.建立回边计数器是为了触发OSR(On StackReplacement)编译,这是一种在运行时,替换正在运行函数的栈帧的技术。

HotSpot内置计数器-3

当开启分层编译时,JVM会根据当前待编译的方法以及编译线程数来动态调整阈值。(XX:CompilerThreshold=X,-XX:OnStackReplacePercentage=X都会失效)

方法调用计数器的执行流程

方法调用计数器的执行流程
如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让他提交给即时编译器编译,那这个方法的调用器就会被减少一半,这个过程称为方法调用计数器热度的衰减,而这段时间就称为此方法统计的半衰周期。进行热度衰减的动作是在虚拟机信息垃圾收集时顺便进行的,可以使用虚拟机参数(-XX:UseCounterDecay)来关闭热度衰减,让方法计数器统计方法调用 绝对次数,这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。另外,可以使用(-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒)

编译器优化-方法内联

内联条件-1

方法体足够小
热点方法:如果方法体小于325字节会尝试内联,可以用(-XX:FreqInlineSize修改大小)
非热点方法:如果方法体小于35字节,会尝试内联,可以用(-XX:MaxInlineSize修改大小)

内联条件-2

被调用的方法运行时的实现可以被唯一确定
static方法、private方法以及final方法,JIT可以唯一确定具体的实现代码。
public的实例方法,指向的实现可能是自身、父类、子类的代码,当且仅当JIT能够唯一确定方法的具体实现时,才有可能完成内联。

方法内联注意点

1.尽可能让方法体小
2.尽量使用final、private、static关键字修饰方法,避免因为多态,需要对方法进行额外的检查
3.在某些场景下,可以通过JVM参数修改阈值,从而让更多方法内联

内联可能带来的问题

1.CodeCache溢出,导致JVM退化成解释执行模式

逃逸分析

分析变量能否逃出他的作用域
1.全局变量赋值逃逸
2.方法返回值逃逸
3.实例引用逃逸
4.线程逃逸

逃逸状态标记-1

全局级别逃逸:一个对象可能从方法或者当前线程中逃逸,如:
1.对象作为方法的返回值返回
2.对象作为静态字段(static field)或者成员变量(field)
3.如果重写了某个类的finalize()方法,那么这个类的对象都会被标记为全局逃逸状态并且一定会放在堆内存中

逃逸状态标记-2

参数级别逃逸:对象被作为参数传递给一个方法,但是在这个方法之外无法访问/对其他线程不可见
无逃逸状态:一个对象不会逃逸

标量替换

标量:不能被进一步分解的量,如:
1.基础数据类型
2.对象引用

通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是创建它的成员变量来代替

聚合量:可以被进一步分解的量

栈上分配

通过逃逸分析,能够确认对象不会被外部访问,就在栈上分配对象

垃圾回收

在对内存要求苛刻的场景:想办法提高对象的回收效率,多回收掉一些对象,腾出更多没存
在CPU使用率高的情况下:降低高并发时垃圾回收的频率,让CPU更多地去执行业务代码而不是垃圾回收

JVM的垃圾回收发生在 方法区 ,虚拟机栈、本地方法栈、程序计数器是线程隔离的

可达性分析

以根对象(GC Roots)作为起点向下搜索,走过的路径被称为引用链(Reference Chain),如果某个对象到根对象没有引用链相连时,就认为这个对象是不可达的,可以回收

根对象(GC Roots)包括哪些

1.虚拟机栈(栈帧中的本地变量表)中引用的对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI(即Native方法)引用的对象

引用-1-强引用

四种引用学习记录

Object obj = new Object()
只要强引用存在,永远不会被回收

引用-2-软引用

SoftReference< String > sr = new SoftReference<>(“hello”)
是用来描述一些有用但非必需的对象
软引用关联的对象,只有在内存不足时才回收

引用-3-弱引用

WeakReference< String > sr = new WeakReference<>(“hello”)
用来描述非必须对象
无论内存是否充足,都被会回收

引用-4-虚引用

ReferenceQueue< String > queue = new ReferenceQueue<>()
PhantomReference< String > pr = new PhantomReference<>(“hello”, queue)
不影响对象的生命周期,如果一个对象只有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动,必须和引用队列(ReferenceQueue)配合使用。当垃圾回收器准备回收一个对象的时候,如果他想它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

可达性算法注意点

一个对象即时不可达,也不一定会被回收

finalize()的建议

1.避免使用finalize()方法,操作不当可能导致的问题
2.finalize()优先级低,何时会被调用无法确定,因为什么时候发生GC不确定
3.建议使用try…catch…finally来代替finalize()

垃圾回收算法

标记-清除

1.标记需要回收的对象
2.清理掉要回收的对象
标记-清除

标记-整理

1.标记需要回收的对象
2.把所有存活的对象压缩到内存的一端
3.清理边界之外的所有空间
标记-整理

复制

1.把内存分成两块,每次只使用一块
2.将正在使用的内存中的存活对象复制到未使用的内存中去,然后清除掉正在使用的内存中的所有对象
3.交换两个内存的角色,等待下次回收
复制

三种算法对比
算法优点缺点
标记-清除实现简单存在内存碎片、分配内存速度受影响
标记-整理无碎片化整理存在内存开销
复制性能好,无碎片化内存利用率低

综合垃圾回收算法

分代收集算法

把内存分成多个区域,不同区域使用不同的回收算法回收对象

回收类型

1.新生代回收(Minor GC | Young GC)
2.老年代回收(Major GC)
3.整理整个堆(Full GC)
4. Major GC ≈ Full GC

对象分配过程


对象创建的时候会先放到 伊甸园(并不是所有新建的对象),当伊甸园满了之后就会触发垃圾回收,这个回收过程是 把 伊甸园存活的对象 拷贝到 存活区中的 From survivor 或者 To survivor。如果这次拷贝到From survivor,再下一次就会拷贝到To survivor,周而复始(使用了 复制算法 )。
每当一次垃圾回收之后有对象存活的话,他的年龄就会+1,当对象的年龄达到阈值(默认15)的时候,就会晋升到老年代,老年代中的对象一般是存活率比较高的。
老年代中一般使用 标记-清除 ,或者 标记-整理 算法进行回收。

新建的对象不一定分配到伊甸园,比如:
1.对象大于 -XX:PretenureSizeThreshold(默认0),就会直接分配到老年代
2.新生代空间不够

对象不一定要达到年龄才进入老年代:
动态年龄:如果Survivor空间中所有相同年龄对象大小的总和大于Survivor空间的一半,那么年龄大于等于该年龄的对象就可以直接进入老年代。

触发垃圾回收的条件-新生代(Major GC)

伊甸园空间不足

触发垃圾回收的条件-老年代(Full GC)

1.老年代空间不足(①空间真的不足②内存碎片导致没有连续的内存去分配对象)
2.元空间不足
3.要晋升到老年代的对象所占用的空间大于老年代的剩余空间
4.显示调用了System.gc(-XX:DisableExplicitGC参数可以忽略该调用)

优点

1.更有效的清除不再需要的对象
2.提升了垃圾回收的效率

调优原则

1.合理设置Survivor区域的大小,避免内存浪费
2.让GC尽量发生在新生代,尽量减少Full GC的发生

增量算法

每次只收集一小片区域的内存空间的垃圾

垃圾收集器

垃圾收集器

术语-Stop The World

简称STW,也叫全局停顿,Java代码停止运行,native代码继续运行,但不能与JVM进行交互。
原因:多半由于垃圾回收导致;也可能由Dump线程、死锁检查、Dump堆等导致的。
危害:服务停止,没有响应;主从切换、危害生产环境。

术语-并行收集VS并发收集

并行收集:指多个垃圾收集线程并行工作,但是收集的过程中,用户线程(业务线程)还是处于等待状态。
并发收集:指用户线程与垃圾收集线程同时工作

术语-吞吐量

CPU用于运行用户代码的时间与CPU总消耗时间的比值

新生代收集器1-Serial收集器

1.最基本的、发展历史最悠久的收集器
2.复制算法
执行过程

特点

1.单线程
2.简单、高效
3.收集过程全程Stop The World

适用场景

1.客户端程序,应用以-client模式运行时,默认使用的是Serial。如:

java -client -jar xxx.jar

2.单核机器

新生代收集器2-ParNew收集器

Serial收集器的多线程版本,除使用了多线程以外,其他和Serial收集器一样,包括:JVM参数、Stop The World的表现、垃圾收集算法是一样的。
执行过程

特点

1.多线程
2.可以使用-XX:ParallelGCThreads设置垃圾收集的线程数(一般设置CPU的核心数)

适用场景

主要用来和CMS收集器配合使用

新生代收集器3-Parallel Scavenge收集器

1.吞吐量优先收集器
2.复制算法
3.并行的多线程收集器,这一点和ParNew类似
执行过程

特点

1.可以达到一个可控制的吞吐量
-XX:MaxGCPauseMillis:控制最大的垃圾收集停顿时间(尽力)
-XX:TCTimeRatio:设置吞吐量的大小,取值0-100,系统花费不超过1/(1+n)的时间用于垃圾手机

2.自适应GC策略:可用-XX:UseAdptiveSizePolicy打开
打开自适应策略之后,无需手动设置新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)等参数。
虚拟机会自动根据系统的运行状况收集性能监控信息,动态的调整这些参数,从而达到最优的停顿时间以及最高的吞吐量。

适用场景

注重吞吐量的场景

老年代收集器1-Serial Old收集器

Serial收集器的老年代版本
标记-整理算法
执行过程

适用场景

1.可以和Serial、ParNew、Parallel Scavenge这三个新生代收集器配合使用
2.CMS收集器出现故障的时候,会用Serial Old作为备用

老年代收集器2-Parallel Old收集器

Parallel Scavenge收集器的老年代版本
标记-整理算法
执行过程

特点

只能和Parallel Scavenge配合使用

适用场景

关注吞吐量的场景

老年代收集器3-CMS收集器

CMS:Concurrent Mark Sweep
并发收集器
标记-清除算法

执行过程

执行过程-1

初始标记(initial mark):
1.标记GC Roots能直接关联到的对象
2.Stop The World

执行过程-2

并发标记(concurrent mark):
1.找出所有GC Roots能关联到的对象
2.并发执行,无Stop The World

执行过程-3(可能执行,可能不执行)

并发预清理(concurrent-preclean):
1.重新标记那些在并发标记阶段,引用被更新的对象,从而减少后面重新标记阶段的工作量
2.并发执行,无Stop The World
3.可以使用-XX:CMSPrecleaningEnabled关闭并发预清理阶段,默认打开

执行过程-4(可能执行,可能不执行)

并发可中止的预清理阶段(concurrent-abortable-preclean):
作用:允许我们能够控制预清理阶段的结束时机。比如扫描多长时间(CMSMaxAbortablePrecleanTime,默认5秒)或者Eden区使用占比达到一定阈值(CMSScheduleRemarkEdenPenetration,默认50%)就结束本阶段
1.和并发预清理做的事情是一样的,并发执行,无Stop The World
2.当Eden的使用量大于CMSScheduleRemarkEdenSizeThreshold的阈值(默认2M)时,才会执行该阶段

执行过程-5

重新标记(remark)
1.修正并发标记期间,因为用户程序继续运行,导致标记发生变动的那些对象的标记
2.一般来说,重新标记花费的时间会比初始标记阶段长一些,但比并发标记的时间短
3.存在Stop The World

执行过程-6

并发清除(concurrent sweep)
1.基于标记结果,清除掉要清楚前面标记出来的垃圾
2.并发执行,无Stop The World

执行过程-7

并发重置(concurrent reset)
1.清理本次CMS GS的上下文信息,为下一次GC做准备

优点

1.Stop The World的时间比较短
2.大多数过程并发执行

缺点

1.CPU资源敏感,并发阶段可能导致应用吞吐量降低
2.无法处理浮动垃圾(因为是并发执行,业务线程在处理的时候也会产生新的垃圾,这部分的垃圾就叫浮动垃圾,CMS是无法清理的,需要到下一次)
3.不能等到老年代满了才开始收集:
①预留的内存不够 -> Concurrent Mode Failre -> Serial Old作为后备
②可以使用CMSInitiatingOccupancyFraction设置老年代占比达到多少就触发垃圾收集,默认68%
4.内存碎片:由于使用 标记-清除算法导致的
①UseCMSCompactAtFullCollection:在完成Full GC后是否要进行内存碎片整理,默认开始
②CMSFullGCsBeforeCompaction:进行几次Full GC后就进行一次内存碎片整理,默认0

适用场景

1.希望系统停顿时间比较短,响应速度快的场景,比如各种服务器应用程序

G1收集器(即可新生代,也可老年代)

Garbge First
面向服务端应用的垃圾收集器
内存布局
每个区域叫做Region:
1.可以通过参数-XX:G1HeapRegionSize指定Region大小
2.取值范围1-32MB,应为2的N次幂

垃圾收集机制
Young GC

1.所有Eden Region都满了的时候,就会触发
2.伊甸园里面的对象会转移到Survivor Region里面去
3.原先Survivor Region的对象转移到新的Survivor Region中去,或者将晋升到Old Region
4.空闲的Region将放入到空闲列表中,等待下次被使用

Mixed GC

1.老年代大小占整个堆的百分比达到一定阈值(可用-XX:InitiatingHeapOccupancyPercent指定,默认45%),就触发
2.会回收所有Young Region,同时回收 部分 Old Region
执行过程

执行过程1-初始标记

初始标记,标记GC Roots能直接关联到的对象,和CMS类似
存在Stop The World

执行过程2-并发标记

同CMS的并发标记
并发执行,无Stop The World

执行过程3-最终标记

修正在并发标记期间引起的变动
存在Stop The World

执行过程4-筛选回收

对各个Region的回收价值和成本进行排序
根据用户所期望的停顿时间(MaxGCPauseMillis)来制定回收计划,并选择一些Region回收

回收过程:
1.选择一系列Region构成一个回收集
2.把决定回收的Region中的存活对象复制到空的Region中
3.删除掉需要回收的Region -> 无内存碎片
4.存在Stop The World

Full GC

1.复制对象内存不够,或者无法分配足够的没存(比如巨型对象没有足够的连续分区分配)时,会触发Full GC
2.Full GC模式下,使用Serial Old模式
3. G1优化原则: 尽量减少Full GC的发生

减少Full GC

1.增加预留内存(增大-XX:G1ReservePercent,默认为堆的10%)
2.更早地回收垃圾(减少-XX:InitiatingHeapOccupancyPercent,老年代达到该值就触发Mixed GC,默认45%)
3.增加并发阶段使用的线程数(增大-XX:ConcGCThreads)

特点

1.可以作用在整个堆
2.可控的停顿(MaxGCPauseMillis = 200)
3.无内存碎片

适用场景

1.占用内存比较大的应用(6G以上)
2.替换CMS垃圾收集器

JVM参数描述博客

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值