jvm-jvm类加载机制
2022-02-22 17:43:52 1 举报
AI智能生成
登录查看完整内容
为你推荐
查看更多
jvm-jvm类加载机制
作者其他创作
大纲/内容
java com.tuling.jvm.Math
windows系统下java.exe调用底层的jvm.dll文件创建Java虚拟机(C++实现)
创建一个引导类加载器实例(C++实现)
sun.misc.Launcher.getLauncher()
launcher.getClassLoader()
获取运行类自己的加载器ClassLoader,是APPClassLoader的实例
classLoader.loadClass("com.tuling.jvm.Math");
调用loadClass加载要运行的类Math
Main.main()
C++发起调用
加载完成时候JVM会执行Math类的main方法入口
java程序运行结束
jvm销毁
C++调用Java代码创建JVM启动器实例sun.misc.Launcher该类由引导类加载器负责加载创建其它类加载器
① 运行一个Main方法的流程
在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
加载
校验字节码文件的正确性
验证
给类的静态变量分配内存,并赋予默认值
准备
将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用
解析
对类的静态变量初始化为指定的值,执行静态代码块
初始化
使用
卸载
② classLoad.loadClass("xxx")解析
引导类加载器
扩展类加载器
应用程序类加载器
自定义加载器
类型
在类AppClassLoader的loadClass方法中实现
调用父类的loadClass
判断类是否是自己加载过的,如果不是,就交给父类加载器去loadClass
都会调用c = findClass(name); 在加载器的类路径中查找并加载该类
源码
沙箱安全机制
避免类的重复加载
设计的目的
自定义自己的类加载器
重写loadClass方法,不指派给父加载器
SharedClassLoader:每个项目都能使用
WebAppClassLoader:各个应用有自己的类加载器,实现相互隔离
实现热加载
JasperLoader:加载JSP文件编译出来的.class文件
实例:Tomcat中的类加载器
打破双亲委派机制
双亲委派机制
全盘负责委托机制
继承java.lang.ClassLoad类
重写findClass方法
自定义类加载器
③ 类加载器
1.类加载机制
存放native修饰的方法:C语言的实现
本地方法栈
线程
如果变量值指向的时一个对象,那么这个变量存放的就是这个对象在堆里面的地址
局部变量表
操作数在进行操作时,临时存放的一块空间
操作数栈
动态链接
调用方法时的位置信息,以便方法结束之后能继续执行以后的代码
方法出口
栈帧:一个方法对应一个栈帧内存空间
javap -c xxx.class
查看jvm自己的汇编指令
对代码进行反汇编
windows命令行输入:jvisualvm
打开Java VisualVM
栈
new出来的对象
默认占堆的空间比例:2/3
老年代
Eden区 默认占比8/10
s0 默认占比1/10
Survivor区
年轻代
结构
堆
每个线程独有的,记录代码执行的位置
每执行一行代码,字节码执行引擎会修改程序计数器的值
程序计数器
常量
如果静态变量指向对象,那么这个变量存放的就是这个对象在堆里面的地址
里面有常量池:Constant pool
javap -v xxx.class
静态变量
方法名
方法返回值
...一切跟Class有关的信息
类信息
方法区(元空间)
内存结构
-Xms
-Xmx
新生代:-Xmn
-XX:Metaspace
-XX:MaxMetaspace
方法区
线程数太多会触发 StackOverflowError
-Xss 每个栈的大小 默认是1m
‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M
设置格式
jvm调优的核心思想:减少GC的次数,主要是Full GC的次数。
jvm参数设置
假设亿级流量电商(每日点击上亿次),假定每个用户点击20-30次
说明日活用户500万,付费转化率10%
日均50万订单,正常在3-4小时产生,每秒几十单
大促时,集中在前几分钟产生,每秒1000多单
假设订单系统有3台:4核8G。每一台承接300多单
假设每个对象1kb,每秒300kb对象产生
考虑其他对象(库存,优惠券,积分等),放大20倍,每秒产生300*20kb对象
同时还有其他操作,比如订单查询,再放大10倍,每秒产生300*20*10kb=60m对象
这60M对象1秒后都变成垃圾对象
此时老年代2G,Eden800m,S0:100m,S1:100m
Eden区满了之后,发生minor gc,会有60m对象进入survivor区
此时会触发对象动态年龄机制,此时60m会进入老年代
如果设置参数-Xms3072M -Xmx3072M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M
所以初步设置为:‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M
案例:日均百万级订单交易系统如何设置JVM参数
2.jvm内存模型(JDK8)
类加载检查
如果没有加载,会加载类
这里的指针是:分配内存和未分配内存的临界点标记
针对内存是规整的
指针碰撞(默认)
针对内存是不规整的
jvm会委会一张空闲内存的表
空闲列表
划分内存方法
CAS
开启:-XX:+UseTLAB
关闭:-XX:-UseTLAB
指定TLAB大小:-XX:TLABSize
如果放不下,会走CAS方式
本地线程分配缓冲(TLAB)(默认)
解决并发问题方法(对象内存争抢问题)
分配内存
可以在TLAB分配时进行
将分配到的内存空间初始化为零值
HashCode
GC分代年龄
锁状态标志
线程持有的锁
偏向线程
偏向时间
存储对象自身的运行时数据
开启压缩 占4字节
没有开启压缩 占8字节
指向类元数据的指针
Klass Point类型指针
设置对象头 32位占4字节,64位占8字节
属性赋值
执行构造方法
执行<init> 方法
查看对象大小:jol-core包
64位操作系统支持指针压缩
启用:XX:+UseCompressedOops(默认开启)
禁止指针压缩:XX:UseCompressedOops
在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力
为了减少64位平台下内存的消耗,启用指针压缩功能
在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G)
堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内存不要大于32G为好
设计目的
对象指针压缩
对象创建流程
分析对象动态作用域
JDK7 之后默认开启
开启参数: -XX:+DoEscapeAnalysis
关闭参数: -XX:-DoEscapeAnalysis
开启参数:-XX:+EliminateAllocations
标量和聚合量
标量替换
逃逸分析
不会被外部访问,随方法结束而销毁
对象不能太大,不然一个栈帧放不下
栈上分配
Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
Major GC/Full GC:一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上。
开启打印GC参数:-XX:+PrintGC -XX:+PrintGCDetail
Minor GC和Full GC
-XX:+UseAdaptiveSizePolicy(默认开启)
让8:1:1自动变化
-XX:-UseAdaptiveSizePolicy
不让8:1:1自动变化
Eden与Survivor区默认8:1:1
Eden区分配
只在 Serial 和ParNew两个收集器下有效。
-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC
-XX:PretenureSizeThreshold设置大对象的大小
大对象直接进入老年代
CMS收集器默认6岁
默认分代年龄15会进入老年代
参数 -XX:MaxTenuringThreshold
长期存活的对象进入老年代
-XX:TargetSurvivorRatio
这个50%可以通过参数设置
放对象的S区,有一批对象的总大小大于S区的50%
大于等于这批对象年龄最大值的对象会直接进入老年代
每一次minor gc之后触发
判断时机
希望可能是长期存活的对象,提前进入老年代
对象动态年龄判断
老年代空间分配机制
无法解决对象直接的相互引用问题
引用计数法
线程栈的本地变量
本地方法栈的变量
GC Roots根节点
强引用
软引用
弱引用
虚引用
常用引用类型
对象第一次被标记为垃圾对象时,如果覆盖了finaliz()方法,暂时不会被回收,反之,立即回收
第二次标记,会执行finalize()方法
只会执行一次
finalize()方法
可达性分析算法
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
判断标准(需同时满足)
无用的类:方法区主要回收的类
对象内存回收
对象的内存分配
栈上分配依赖于逃逸分析和标量替换
3.JVM对象创建和内存分配机制
4.JVM字节码深度分析 // TODO
分代收集理论
内存分为两块,每次只用一块
复制算法(年轻代)
标记整理算法(老年代)
1. 效率问题 (如果需要标记的对象太多,效率不高)
2. 空间问题(标记清除后会产生大量不连续的碎片)
标记清除算法(老年代)
垃圾收集算法
Serial
ParNew
Parallel
G1
CMS
Serial Old
在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用
另一种用途是作为CMS收集器的后备方案
Parallel Old
按照年代区分
开启参数: -XX:+UseSerialGC -XX:+UseSerialOldGC
新生代采用复制算法,老年代采用标记-整理算法。
单线程
Serial收集器
Serial收集器的多线程版本
JDK8 默认的收集器
吞吐量高
Parallel Scavenge收集器
开启参数:-XX:+UseParNewGC
跟Parallel收集器很类似
区别主要在于它可以和CMS收集器配合使用
ParNew收集器
开启参数: -XX:+UseConcMarkSweepGC(old)
Stop the world
暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。
初始标记
和用户线程一起运行
并发标记
使用三色标记的增量更新算法
重新标记
并发清理
重置本次GC过程中的标记数据。
并发重置
过程
并发收集
低停顿
优点
对CPU资源敏感(会和服务抢资源);
无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了);
它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理
执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrentmode failure",此时会进入stop the world,用serial old垃圾收集器来回收
缺点
1. -XX:+UseConcMarkSweepGC:启用cms
2. -XX:ConcGCThreads:并发的GC线程数
3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
核心参数
‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=82 ‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M ‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC3 ‐XX:CMSInitiatingOccupancyFraction=92 ‐XX:+UseCMSCompactAtFullCollection ‐XX:CMSFullGCsBeforeCompaction=0
亿级流量电商系统如何优化JVM参数设置(ParNew+CMS)
CMS收集器
开启参数:-XX:+UseG1GC
JDK 1.9默认使用 G1
安全点
安全区域
G1将Java堆分成大小相等的独立区域,叫region
region大小等于堆大小除以2048,可以用-XX:G1HeapRegionSize手动指定
默认一开始年轻代对对内存占比是5%,可以用-XX:G1NewSizePercent调整
之后会不断给年轻代增加更多的Region,上限是60%,可以用-XX:G1MaxNewSizePercent调整上限
Eden和survivor区配比保持8:1:1不变
Region可能之前是年轻代,进行垃圾回收之后,可能会变成老年代
专门分配大对象,超过Region50%的叫大对象
一个大对象可能会横跨多个Region区
Full GC会回收年轻代,老年代和Humongous区的垃圾
Humongous区
Region
采用复制算法
暂停所有其他线程,记录下gc roots直接能引用的对象,速度很快
同CMS并发标记
STW
同CMS的重新标记
最终标记
对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿STW时间(可以用JVM参数-XX:MaxGCPauseMillis指定)来制定回收计划
G1收集器在后台维护了一个优先队列,每次根据所允许的收集时间,优先选择回收价值最大的Region
筛选回收(STW)
收集过程
YoungGC
MixedGC
Full GC
G1垃圾收集分类
使用G1收集器
-XX:+UseG1GC
指定GC工作的线程数量
-XX:ParallelGCThreads
指定分区大小
-XX:G1HeapRegionSize
目标暂停时间(默认200ms)
-XX:MaxGCPauseMillis
新生代内存初始空间(默认5%,值配置整数,默认就是百分比)
-XX:G1NewSizePercent
新生代内存最大空间
-XX:G1MaxNewSizePercent
参数设置
50%以上的堆被存活对象占用
对象分配和晋升的速度变化非常大
垃圾回收时间特别长,超过1s
8GB以上的堆内存
停顿时间是500ms以内
使用场景
G1收集器(Garbage-First)
实验性质,几百G内存 使用
ZGC收集器(JDK11加入)
垃圾收集器
黑色
灰色
白色
多标-浮动垃圾
黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。
增量更新
原始快照STAB (Snapshot At The Beginning)
CMS:写屏障 + 增量更新
G1,Shenandoah:写屏障 + SATB
ZGC:读屏障
对漏标的处理方案
漏标-读写屏障
可以用于记录跨代/区引用的变化
写屏障实现SATB
写屏障实现增量更新
写屏障
可以用于支持移动对象的并发执行等
读屏障
卡表的维护
记忆集与卡表
认识几个概念
三色标记
5.垃圾收集
启动一个web应用程序,用jps查看其进程,比如进程为15300
num 序号
instances:实例数量
bytes:占用空间大小
class name:类名称
jmap -histo 15300 > ./log.txt
查看堆信息:jmap -heap 15300
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./ (路径)
设置内存溢出自动导出(可能会导不出来)
点击文件,装入,选择dump文件
可以用jvisualvm命令工具导入该dump文件分析
堆内存dump
Jmap
线程名
Thread-1
线程优先级
prio
线程id
tid
线程对应的本地线程标识nid
nid
线程状态
java.lang.Thread.State: BLOCKED
可以查看线程状态
还可以用jvisualvm自动检测死锁
查找死锁
使用命令top -p <pid> ,显示你的java进程的内存情况,pid是你的java进程号,比如19663
按H,获取每个线程的内存情况
找到内存和cpu占用最高的线程tid,比如19664
转为十六进制得到 0x4cd0,此为线程id的十六进制表示
执行 jstack 19663|grep -A 10 4cd0,得到线程堆栈信息中 4cd0 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙高的调用方法
查看对应的堆栈信息找出可能存在问题的代码
找出占用cpu最高的线程堆栈信息
jstack 15300
自动查找死锁
-Dcom.sun.management.jmxremote.port 为远程机器的JMX端口
-Djava.rmi.server.hostname 为远程机器IP
java ‐Dcom.sun.management.jmxremote.port=8888 ‐Djava.rmi.server.hostname=192.168.50.60 ‐Dcom.sun.management.jmxremote.ssl=false ‐Dcom.sun.management.jmxremote.authenticate=false ‐jar microservice‐eureka‐server.jar
普通jar程序JMX端口设置
在catalina.sh文件里的最后一个JAVA_OPTS的赋值语句下一行增加如下配置行
JAVA_OPTS="$JAVA_OPTS ‐Dcom.sun.management.jmxremote.port=8888 ‐Djava.rmi.server.hostname=192.168.50.60 ‐Dcom.sun.management.jmxremote.ssl=false ‐Dcom.sun.management.jmxremote.authenticate=false"
tomcat的JMX配置
远程连接
jvisualvm
jinfo -flags 15300
查看jvm的参数
jinfo -sysprops 15300
查看java系统参数
Jinfo
S0C:第一个幸存区的大小,单位KB
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小(元空间)
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间,单位s
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间,单位s
GCT:垃圾回收消耗总时间,单位s
jstat -gc pid
垃圾回收统计
NGCMN:新生代最小容量
NGCMX:新生代最大容量
NGC:当前新生代容量
S0C:第一个幸存区大小
OGCMN:老年代最小容量
OGCMX:老年代最大容量
OGC:当前老年代大小
OC:当前老年代大小
MCMN:最小元数据容量
MCMX:最大元数据容量
MC:当前元数据空间大小
CCSMN:最小压缩类空间大小
CCSMX:最大压缩类空间大小
CCSC:当前压缩类空间大小
YGC:年轻代gc次数
FGC:老年代GC次数
jstat -gccapacity pid
堆内存统计
S0C:第一个幸存区的大小
TT:对象在新生代存活的次数
MTT:对象在新生代存活的最大次数
DSS:期望的幸存区大小
YGCT:年轻代垃圾回收消耗时间
jstat -gcnew pid
新生代垃圾回收统计
S0CMX:最大幸存1区大小
S0C:当前幸存1区大小
S1CMX:最大幸存2区大小
S1C:当前幸存2区大小
ECMX:最大伊甸园区大小
EC:当前伊甸园区大小
FGC:老年代回收次数
jstat -gcnewcapacity pid
新生代内存统计
MC:方法区大小
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
jstat -gcold pid
老年代垃圾回收统计
jstat -gcoldcapacity pid
老年代内存统计
jstat -gcmetacapacity pid
元数据空间统计
Jstat
年轻代对象增长的速率
Young GC的触发频率和每次耗时
每次Young GC后有多少对象存活和进入老年代
Full GC的触发频率和每次耗时
JVM运行情况预估
内存泄露到底是怎么回事
6.调优实战
https://alibaba.github.io/arthas
官方文档
wget https://alibaba.github.io/arthas/arthas‐boot.jar
用java -jar运行即可,可以识别机器上所有Java进程
选择进程序号1,进入进程信息操作
输入dashboard可以查看整个进程的运行情况,线程、内存、GC、运行环境信息:
输入thread可以查看线程详细情况
输入 thread加上线程ID 可以查看线程堆栈
输入 thread -b 可以查看线程死锁
输入 jad加类的全名 可以反编译,这样可以方便我们查看线上代码是否是正确的版本
使用 ognl 命令可以查看线上系统变量的值,甚至可以修改变量的值
7.Arthas详解
Tomcat则直接加在 JAVA_OPTS变量里。
‐Xloggc:./gc‐%t.log ‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps ‐XX:+PrintGCTimeStamps ‐XX:+PrintGCCause‐XX:+UseGCLogFileRotation ‐XX:NumberOfGCLogFiles=10 ‐XX:GCLogFileSize=100M
打印GC日志方法,在JVM参数里增加参数, %t 代表时间
‐Xloggc:d:/gc‐cms‐%t.log ‐Xms50M ‐Xmx50M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps‐XX:+PrintGCTimeStamps ‐XX:+PrintGCCause ‐XX:+UseGCLogFileRotation ‐XX:NumberOfGCLogFiles=10 ‐XX:GCLogFileSize=100M‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC
CMS 打印GC日志
‐Xloggc:d:/gc‐g1‐%t.log ‐Xms50M ‐Xmx50M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps‐XX:+PrintGCTimeStamps ‐XX:+PrintGCCause ‐XX:+UseGCLogFileRotation ‐XX:NumberOfGCLogFiles=10 ‐XX:GCLogFileSize=100M‐XX:+UseG1GC
G1打印GC日志
分析日志的网站
gceasy( https://gceasy.io )
8.GC日志详解
生成可读的JVM字节码指令文件
字面量就是指由字母、数字等构成的字符串或者数值常量
字面量
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
符号引用
Constant pool 就是class常量池信息
javap -v Math.class
字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能
为字符串开辟一个字符串常量池,类似于缓存区
创建字符串常量时,首先查询字符串常量池是否存在该字符串
存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
设计思想
String s = "zhuge"; // s指向常量池中的引用
直接赋值字符串
String s1 = new String("zhuge"); // s1指向内存中的对象引用
new String();
intern方法
字符串操作(JDK1.7及以上)
Jdk1.6及之前
有永久代,但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到堆里
Jdk1.7
无永久代,运行时常量池在元空间,字符串常量池里依然在堆里
Jdk1.8及之后
主要区分字符创 在字符串常量池 还是作为对象分配在Heap上
字符串常量池位置
字符串常量池
整数大于127时 不参考对象池概念
对象池概念 类似于 字符串常量池概念
Double
Float
浮点数类型没有实现对象池技术
八种基本类型的包装类和对象池
9.Class常量池与运行时常量池
jvm剖析
jvm
0 条评论
回复 删除
下一页