JVM总结
JMM是什么,作用是什么
JMM是java的内存模型是一个抽象的概念主要是屏蔽操作系统之间的不同保证java程序在各个操作系统上都能达到一致的运行,其三个核心理念:原子性、可见性、有序性
原子性:操作的连续性,要么同时成功要么同时失败,不会受其他线程干扰。
可见性:线程对共享变量操作后要立刻刷新到主内存中,避免其余线程读取到的是旧址。
有序性:程序执行顺序和代码书写顺序一致,避免操作系统或CPU对程序执行顺序优化导致异常。
什么是JVM
1.JVM(Java Virtual Machine,Java虚拟机)是Java程序运行的核心环境,负责将Java字节码(.class文件)翻译成底层操作系统可执行的机器指令,并管理内存、线程、垃圾回收等资源。
2.核心作用是实现“一次编写,到处运行”(Write Once, Run Anywhere),屏蔽底层操作系统和硬件的差异。
JVM的主要组成部分和作用

1.JVM主要有两个系统和两个组件组成,两个系统分别是Class Loader(类加载器)、Execution engine(执行引擎),两个组件分别是Runtime data area(运行时数据区),Native Interface(本地方法接口/库)
2.Class Loader(类加载器)根据全限定类名(java.lang.Object)把类加载到运行时数据区的方法区,是将.class文件的二进制数据读取到内存中。
3.Execution engine(执行引擎)负责将字节码转换成系统指令并执行。
4.Runtime data area(运行时数据区),就是我们常说的JVM内存
5.Native Interface(本地接口库)与native libraries交互,是其它编程语言交互的接口
执行流程
编译器把java文件转换成.class文件,类加载器根据全限定类名把类加载到运行时数据区的方法区,此时方法区的数据是JVM指令,不能直接被系统使用,需要执行引擎将字节码转换成系统调用指令,系统调用的指令就是本地接口库的方法。
运行时数据区主要组成部分和作用
1.方法区:线程共享,存放编译后的.class文件、运行时常量池、常量,静态变量等。
2.堆:线程共享,存放的是对象和数组。
3.程序计数器:线程私有的,记录当前线程执行的字节码地址。
4.虚拟机栈:线程私有,每个线程会有栈,线程每调用一个方法都会生产一个栈帧,栈是先进后出。
5.本地方法栈:与虚拟机栈类型,专门为本地方法服务。
虚拟机栈的组成部分和作用

1.局部变量表:储存方法的参数和内部变量
2.操作数栈:主要用于储存计算过程中的结果(算数运行、逻辑运算的中间值),方法无计算操作数栈仍可能被使用。
3.动态链接:将符号引用(如方法名、类名)转换成直间引用(内存地址)
4.方法返回地址:记录方法执行完毕后需要返回的代码位置
两种返回方式:
正常返回(return指令):回到调用方法的下一条指令。
异常返回:通过异常处理表跳转到异常处理代码块。
5.附加信息
栈帧高度、JDK版本等等。
备注
1.类加载过程中这些符号引用会被加载到运行时常量池,但不会立即解析为直接引用。解析动作发生在类首次主动使用时(如调用方法、访问字段时),此时符号引用会被转换为具体内存地址的直接引用。
2.符号因为A->B,直接引用B->0X001,A->0X001
静态常量池、运行时常量池、字符串常量池

静态常量池
1.静态常量池是.class文件的一部分,静态常量包含字面量和符合引用,字面量是字符串、数据常量,符合引用是类/接口全限定类名、字段名称描述、方法名描述。
运行时常量池
1.当静态常量池被加载到内存后就会变成运行时常量池,也就是真正把文件内容落地到JVM中,线程共享。
字符串常量池
1.字符串常量池是位于堆中的一片空间,专门用存放字符串字面量(如“hello”),字符串常量池主要是用于解决字符串频繁创建销毁的资源消耗,通过字符串常量池对重复的字面量进行复用,字符串常量池是线程共享的。
类加载器的三大特性
全盘负责
类加载器加载一个类时,该类和所依赖的类,均有一个加载器加载。此时只是调用ClassLoader.loadClass(name)的过程,真正创建类是由双亲委派完成。
例如,系统类加载器AppClassLoader加载入口类(含有main方法的类)时,会把main方法所依赖的类及引用的类也载入,依此类推。“全盘负责”机制也可称为当前类加载器负责机制。显然,入口类所依赖的类及引用的类的当前类加载器就是入口类的类加载器。
以上步骤只是调用了ClassLoader.loadClass(name)方法,并没有真正定义类。真正加载class字节码文件生成Class对象由“双亲委派”机制完成。
双亲委派
类加载请求首先委派给父类加载器处理,仅在父类无法完成加载时(如父类路径中未找到目标类),子加载器才会尝试加载,该机制通过层级关系(应用程序类加载器(AppClassLoader) ->扩展类加载器(ExtClassLoader)->启动类加载器(Bootstrap ClassLoader) )确保核心类库优先由顶层加载器处理,避免重复加载和安全问题。双亲委派机制规避了一个类由不同的类加载器完成导致类冲突。
父类委托别名就叫双亲委派机制。 “双亲委派”机制加载Class的具体过程是: 1. ClassLoader先判断该Class是否已加载,如果已加载,则返回Class对象;如果没有则委托给父类加载器。 2. 父类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则委托给祖父类加载器。 3. 依此类推,直到始祖类加载器(引用类加载器)。 4. 始祖类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的子类加载器。 5. 始祖类加载器的子类加载器尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的孙类加载器。 6. 依此类推,直到源ClassLoader。 7. 源ClassLoader尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,源ClassLoader不会再委托其子类加载器,而是抛出异常。 “双亲委派”机制只是Java推荐的机制,并不是强制的机制。 我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就应 该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法。

缓存机制
类加载器加载一个类时,先看缓存区是否有这个类实例,如果做有则直接使用,没有则重新加载
缓存机制将会保证所有加载过的Class都将在内存中缓存,当程序中需要使用某个Class时,类加载器先从内存的缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效.对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。
备注:
全盘负责规定的是类加载的边界,双亲委派规定的加载顺序,全盘负责划定 “谁加载依赖”(边界),双亲委派规定 “按什么顺序加载”(流程)
两者共同作用,确保类加载的 安全性与一致性(如防止核心类被篡改)
对象的生命周期

1.创建阶段
1)开配内存空间
2)开始构建对象
3)实例化父类到子类的static成员变量
4)实例化父类的成员变量。
5)递归调用父类的构造函数。
6)实例化子类的成员变量。
7)调用子类的构造函数。
备注:当一个类被实例化后赋值给某个变量,这个类就有创建阶段切换到了使用阶段。
2.使用阶段
1)至少由一个强引用执行该类
3.不可见阶段
1)在虚拟机的对象根引用集合中再也找不到直接或间接引用。如线程或方法中的临时变量。
4.回收阶段
1)对象不在被任何变量引用,GC发现该对象已经不可达。
对象的垃圾回收机制

我是一个对象出生在Eden区,Eden区有很多和我很像的小兄弟,我们在Eden区玩耍了一段时间,后来人太多了我们就被安排到了Survivor区了,到了Survivor区我就开始漂了,
一会在survivor from区一会在Survivor to区,知道我15岁时,爸爸说我应该出去闯闯了,于是我就被安排到了Old区,Old区的人很多且年龄都很大。
1.为了解决内存空间的碎片
2.当只有一个Survivor区时,Eden区内存满时会进行一次yong gc,将Eden的数据放入到Survivor 区,当Eden区再次满时会把对象再次放入Survivor 区,此时两部分对象不是连续的
只能将对象强塞入Survivor区,造成内存空间的碎片,内存空间的碎片无法满足后续大对象的分配需求,从而导致内存浪费和性能下降。
3.当有两个Survivor区时,当Survivor from区经过yong gc后会把对象复制到Survivor to区,并且复制后的数据是连续的,当再有对方放入survivor区时 survivor from是空的可以直接放入。
Eden区、Survivor from 区、Survivor to区 、Old区比例如何分配

1.年轻代(包括Eden、Survivor From、Survivor To)与老年代的比例为1:2
2.年轻代中Eden区与两个Survivor区(From和To)的默认比例为8:1:1
3.8:1:1的原因Eden的对象存活率较低,因为大部分对象都是方法级别的,方法结束对象就死亡了,超过一次生命周期的对象只有2%.
1.可达性分析算法是垃圾回收机制的核心算法,主要从一组称为GC Roots的根对象开始,沿着对象引用链向下搜索,被引用到的对象标记为可达(存活),未被引用的对象判定为不可达(可回收)。与引用计数法相比,该算法能有效解决循环引用问题。

备注:
引用计数法通过维护对象的引用计数器来判断对象是否存活,当对象被引用时,计数器+1,当引用失效时,计数器-1,计数器归零时,对象可被回收。引用计数发存在循环引用问题。
哪些对象可以作为GC ROOT的根路径
1.虚拟栈的的局部变量:User user = new User() , user局部变量引用的User对象不会被回收
2.方法区的静态变量:public static User globalUser = new User(),globalUser作为静态变量引用的User对象不会被回收
3.方法区中的常量:public static final String CONST_STR = "hello",CONST_STR引用的字符串不会被回收
4.活跃的线程对象:Thread t = new Thread(...)只要线程未终止,t引用的对象(如Runnable)不会被回收
5.同步块中锁定的对象: 被锁定的对象正在被线程使用,需保证存活以避免并发问题,synchronized锁定的对象在锁释放前不会被回收
6.本地方法栈中的JNI引用:Native方法(如调用C/C++代码)中的局部变量由JVM与原生系统交互维护,需作为根节点。
什么是STW(stop the world)
1.JVM在执行GC时,会暂停所有用户线程(包括业务线程和辅助线程),仅允许GC线程运行,其他的线程都将停止工作,中断了的线程直到GC线程结束才会继续任务,若应用线程与GC线程并发运行,可能导致对象引用关系被修改引发漏标或误标
2.hotSpot虚拟机的垃圾回收器都会STW
JVM调优的本质是什么
1.增大吞吐量
2.减少stw的时间
GC运行的时候通常会产生两者不符合预期的情况(Parallel Scavenge和G1并行标记期间)
1.该回收的未被回收
在标记阶段,一个对象被标记为可达后,用户线程将对象置为不可达,在GC遍历阶段此对象标记的是可达的,因此不会被回收。
2.不该回收的被回收了
在标记阶段,一个对象被标记为不可达后,用户线程将对象建立了新的引用变为可达,,在GC遍历阶段此对象标记的是不可达的,因此会被回收
3.一般通过STW避免这两种情况
已经能够确定一个对象为垃圾之后,接下来要考虑的就是回收,怎么回收呢?得要有对应的算法,下面介绍常见的垃圾回收算法。高效 健壮 数据结构 + 算法
标记-清除(Mark-Sweep)
-
根据可达性算法找出内存中所有的存活对象,并且把它们标记出来

-
清除掉没有被标记的对象,释放出对应的内存空间

缺点
标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。 (1)标记和清除两个过程都比较耗时,效率不高 (2)会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记清除算法的衍生规则之分配(动态分区分配策略)
首次适应算法(Fisrt-fit)
首次适应算法(Fisrt-fit)就是在遍历空闲链表的时候,一旦发现有大小大于等于需要的大小之后,就立即把该块分配给对象,并立即返回。
最佳适应算法(Best-fit)
最佳适应算法(Best-fit)就是在遍历空闲链表的时候,返回刚好等于需要大小的块。
最差适应算法(Worst-fit)
最差适应算法(Worst-fit)就是在遍历空闲链表的时候,找出空闲链表中最大的分块,将其分割给申请的对象,其目的就是使得分割后分块的最大化,以便下次好分配,不过这种分配算法很容易产生很多很小的分块,这些分块也不能被使用
标记-复制(Mark-Copying) 效率很高
将内存划分为两块相等的区域,每次只使用其中一块,如下图所示:

当其中一块内存使用完了,就将还存活的对象复制到另外一块上面(将指针指向对应的内存空间),然后把已经使用过的内存空间一次清除掉。

缺点:空间利用率降低。
标记-整理(Mark-Compact) 标记压缩算法
随机整理 线性整理 滑动整理
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都有100%存活的极端情况,所以老年代一般不能直接选用这种算法。
标记整理算法会有头尾指针进行逐步判断,当头指针找到了空闲区域,尾指针找到了存活对象,会把存活对象放到空闲区域,然后头尾指针重复上述步骤,直到两指针相遇
整理完成后清除掉边缘的对象。
其实上述过程相对"复制算法"来讲,少了一个"保留区"

让所有存活的对象都向一端移动,清理掉边界意外的内存。

分代收集算法
既然上面介绍了3中垃圾收集算法,那么在堆内存中到底用哪一个呢?
Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
Serial:

1.Serial是最基础的垃圾收集器是单线程的。
2.适用于新生代
3.采用的标记复制算法
4.存在STW
Serial Old:

1.Serial Old同Serial一样也是单线程的。
2.Serial Old适用于老年代
3.采用复制整理算法
4.存在STW
备注:Serial单线程的原因是JDK1.3的时候机器都是单CPU的
ParNew:

1.是Serial收集器的多线程版本
2.适用于新生代
3.采用的标记复制算法
4.存在STW
备注:ParNew相比Serial是依赖了机器的多CPU计算
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,看上去和ParNew一样,但是Parallel Scanvenge更关注系统的吞吐量。
吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间) 比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。 若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的运算任务。-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间,
-XX:GCRatio直接设置吞吐量的大小。
Parallel Old 停顿时间 吞吐量
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法进行垃圾回收,也是更加关注系统的吞吐
CMS垃圾收集器是以"最短回收时间为目标",既然是最短回收时间就要耗时操作并行处理
1.初始标记:找到GC ROOT和其直接关联的对象 ,GC ROOT是不耗时,所有不要并发
2.并发标记:找到GC ROOT上所有存活的对象并进行标记,GC ROOT是链表因为每个都要遍历是耗时(3-5S)的所有要并发,并发标记的过程没有进行STW所有会产生该回收的未回收,不该回收的回收了。
3.重新标记:增量标记并发阶段因程序修改变动的内容,会产生stw。
4.并发清除:清除不可达对象回收空间,同时有新垃圾产生,留着下次清理称为浮动垃圾
优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量
G1
所谓Garbage-Frist,其实就是优先回收垃圾最多的Region区域

1.分区收集:G1将内存分割成大小相同的区域(Region),每个区域(Region)是1M-32M,可以设置但是要是2的N次幂,默认情况是将内存分割成2048个区域(Region),可以进行设置,这种设计使得G1能够更灵活地进行内存管理和垃圾收集。
2.优先回收垃圾最多的区域:G1会跟踪每个区域(Region)的垃圾堆积情况,优先回收垃圾最多的区域(Region),最大限定的提升垃圾回收的效率
3.可预测的停顿时间:可设置GC的停顿时间,在垃圾回收的时候不会超过这个限度。
4.解决碎片空间:通过最近区域相同转化解决内存空间碎片的问题
参数设置:
-XX:+UseG1GC 启用G1垃圾收集器
-XX:MaxGCPauseMillis 设置最大GC暂停时间的目标
-XX:+G1HeapRegionSize 每个区域的大小
缺点:
使用G1最起码是4C8G的机器
工作过程可以分为如下几步
初始标记(Initial Marking) 标记以下GC Roots能够关联的对象,并且修改TAMS的值,需要暂停用户线程
并发标记(Concurrent Marking) 从GC Roots进行可达性分析,找出存活的对象,与用户线程并发执行
最终标记(Final Marking) 修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需暂停用户线程
筛选回收(Live Data Counting and Evacuation) 对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划

JVM预调优
1.上线前预估业务量
2.内存配置是否符合并发要求,是否并发期间不触发GC
3.压测吞吐量 95% 98% GC view工具
4.Full gc 和 yong gc 的频率
5垃圾回收器的参数
6.CPU的使用率
如果有个4核8g的docker,jvm我要给堆、栈分配多少内存,需要考虑哪方面的内容
1.分配的内存不要超过容器的总内存
2.最大堆内存一般占用虚拟机的50%-75%,初始堆内存设置为最大堆内存的值,避免运行时动态调整的开销。
3.年轻代一般和老年代相比是1:2 ,如果使用G1垃圾收集器G1会自动调整
4.栈内存默认1M,一般设置256KB 、512KB,设置小一点可以减少高并发情况下内存占用,当设置栈大小为256时在变量不多的情况下可以调用1000多个方法。
亿级浏览JVM调优
https://www.cnblogs.com/sunnycc/p/19148615
在进行垃圾回收器的选择时要考虑那些?
1.从业务层面
1.响应时间:指垃圾回收导致应用线程暂停的时长,直接影响用户体验,C端或交易类项目一般要求较高,B端项目有一定容忍度
2.吞吐量:反映单位时间内业务逻辑执行占比,B端项目一般吞吐量大如基础服务系统,数据分析系统,C端一般要求不高
2.服务性能
1.内存规模:2C4G,4C8G,4C16G等等
2.并发能力:是否多核CPU
3.垃圾回收器的特性
1.parallel垃圾回收器适合高吞吐量单响应时间要求不高的应用,parallel垃圾回收器在进行标记清除时都是GC线程并发进行的,多线程并发提高了处理效率,但是parallel垃圾回收器会进行STW。
2.CMS垃圾回收器适合响应时间要求较高的应用,CMS垃圾回收器只有在初始化标记和重新标记的时候才会STW,这两个阶段耗时都比较短,并发标记和并发清除阶段都是和用户线程并发的不影响业务处理。
3.C1垃圾回收器是对CMS垃圾回收器的替代,能够更好的控制停顿时间和吞吐量,其本质是优先回收垃圾最多的区域来降低停顿时间,但是对机器性能有要求,最低是4C8G的服务器。
JVM参数都需要配置那些?
1.基础参数都有那些
最大堆内存:-Xmx5324m ( 一般占用虚拟机的50%-75%)
初始化堆内存:-Xms5324m (初始堆内存设置为最大堆内存的值,避免运行时动态调整的开销)
指定栈内存大小:-Xss256K
元空间大小:XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m (不设置的话上限是物理内存,设置的核心目的是为了平衡内存使用效率与系统稳定性)
指定垃圾回收器:-XX:+UseParallelGC -XX:+UseConcMarkSweepGC XX:+UseG1GC
指定内存异常生成的dump文件: XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/export/Logs
2.parellel垃圾回收器
-XX:+UseParallelGC
-XX:+UseParallelOldGC
线程数配置 : -XX:+ParallelGCThreads (默认值为CPU核心数,一般设置为核心线程数或 核心线程数+1)
停顿时间:-XX:MaxGCPauseMillis (优先级高于吞吐量)
吞吐量:-XX:GCTimeRatio
3.CMS垃圾回收器
指定使用CMS作为老年代回收器:-XX:+UseConcMarkSweepGC
搭配ParNew作为新生代回收器:-XX:+UseParNewGC(默认组合)
并发标记线程数:-XX:ConcGCThreads 建议为CPU核心数的1/4~1/2
4.G1垃圾回收器参数设置
XX:+UseG1GC
目标最大停顿时间:-XX:MaxGCPauseMillis (默认200ms)
单区域块大小:-XX:+G1HeapRegionSize (1-32M,默认根据堆内存自动计算,堆内存/2048)
JVM内存调优实战
背景:
1.手机充值项目月初月末流量大且金融APP首页给开量100%,主要流量峰值在中午9点,下午13点。
2.8月份零售钱包要求需要展示手机充值信息日常要求500TPS
3.JVM参数-Xmx5324m Xms5324m -Xss256K XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
现象:
1.月初的时候每台每天会触发一次FULL GC导致线程池打满,时间集中在早上中午9点,下午13点。
分析:
1.应该是零售钱包要500TPS的问题,导致系统FULL GC,但是都是查询接口对象都是朝生夕死,且没有大对象。
2.观察系统yong gc 有明显的频次增加,由于流量大了yong gc变多也是正常,CPU利用率60%。
3.直接扩容机器肯定能解决,扩容机器后日常CPU使用率就低了,为了成本考虑决定先排查问题。
4.在堆内存峰值的时候使用Jmap工具收集堆栈信息到dump文件,使用MAT工具分析是否有内存泄漏和大对象。
5.排查发现有个科技券对象内存占用较高,排内存占用的12名,观察日志发现有些科技券日志能占用3屏幕,
6.科技券多的原因是月初会有用户从各个渠道领取很多优惠券,3屏数据大概20KB还不够大对象级别所以不会直接进入老年代。
7.项目使用的是CMS垃圾回收器,在短暂的流量突增的情况下会导致Eden区的对象到Survivor,但是Survivor区也满了导致对象进入老年代,在老年代存活。
8.于是就将Eden和Survivor的比例由8:1:1改成了 6:2:2, 这样Survivor的区域就大了存活的对象就可以多一些,很多对象都是朝生夕死,这样很多对象在Survivor区就被GC了不会进入老年代,就减少了FULL GC了。
9.预发环境进行8个普通对象2个大数据对象压测到250TPS没有产生问题,于是就上线配置观察,最终问题得以解决。



浙公网安备 33010602011771号