JAVA JVM
2021-07-20 20:38:50 2 举报
AI智能生成
JVM详细结构讲解
作者其他创作
大纲/内容
Java程序最初都是通过解释器进行解释执行的,当虚拟机发现某个方法或某个代码块运行特别频繁时,就会把这些代码认定为“热点代码”,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成本地机器码,并以各种手段尽可能地进行代码优化,运行时完成这个任务的就是即时编译器
背景
1、需要收集的数据比较少2、编译、优化方面都做得比较浅3、编译生成的代码执行效率没 C2 高4、编译时耗费的 CPU 资源没 C2 高
C1编译器 -client(客户端编译器)
1、需要收集的数据比较多(此为触发条件)2、优化得更彻底(分析后做优化)3、编译时耗费 CPU 资源4、执行效率更高
C2编译器 -server(服务端编译器)
种类
即时编译器just in time
由于即时编译器编译本地代码需要占用程序运行时间,编译出优化程度越高的代码,所花时间也越长
为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机在 jdk 6 实现了分层编译
解释器、客户端编译器和服务端编译器会同时工作,热点代码可能会被多次编译,用客户端编译器获取更高的编译速度,用服务端编译器来获取更好的编译质量
原理
应用:系统启动初期的字节码优化采用C1,热机后采用C2编译器
冷机:刚启动
热机:运行了一段时间
热机切冷机故障
问题:并发性能快到瓶颈时,扩充了几台机器,然后把流量切过去,结果发现冷机崩了
原因:热机已经触发了C2编译器,生成了更优的字节码,承受的并发能力比冷机大很多
解决:先切部分流量过去,等触发C2编译器后,再把大流量切过去
阿里案例
分层编译技术
客户端模式下是1500次
服务端模式下是10000次
多次执行的代码块
这个阈值可以通过虚拟机参数-XX:CompileThreshold 设定
多次执行的方法
热点代码
基于采样的热点探测
基于计数器的热点探测
热点探测方式
热度衰减
触发条件
因为JIT需要申请一块可读可写可执行的内存区域,而Mac不允许申请可执行的内存
Mac为什么不支持JIT?
方法内联
新对象仅创建线程可见,外部线程不可见,否则就是发生了逃逸
逃逸分析是一种确定指针动态范围的方法
概念
Java的逃逸分析只发生在JIT的即时编译中,因为收集到足够的运行数据JVM可以更好的判断对象是否发生了逃逸
Java的逃逸分析是方法级别的,因为JIT的即时编译是方法级别
发生时机
对象被赋值给堆中对象的字段(全局变量)或类的静态变量
对象被传进了不确定的代码中去运行
JVM判断新创建的对象发生逃逸的依据
满足其一则判定为逃逸
栈上分配:如果某个对象在子程序中被分配,并且指向该对象的指针永远不会逃逸,该对象就可以分配在栈上,而不是在堆上。在有垃圾收集的语言中,这种优化可以降低垃圾收集器运行的频率
同步锁消除:如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步
分离对象或标量替换:如果某个对象的访问方式不要求该对象是一个连续的内存结构,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中
优化手段
降低垃圾回收频率,提升性能
先判断该对象是否发生逃逸,如果没有发生就在栈上分配
JVM参数设置:-XX:PretenureSizeThreshold
否则在堆上分配,再判断是否为大对象,大对象直接进入老年代
确保多线程分配对象的线程安全
新生代默认开启线程本地分配缓存-TLAB(占Eden区的1%)
如果TLAB放不下,又不是大对象,或者禁用了TLAB,则会使用CAS乐观锁在Eden区进行内存分配
基于逃逸分析的对象分配
外框
逃逸分析
公共子表达式消除
数组边界检查消除
优化
JIT
-l:输出主类的全名,如果进程执行的是 JAR 包,则输出 JAR 路径
未被显示指定的可通过 jinfo -flag 查看
-v:查看虚拟机启动时显式指定的JVM参数列表
jps:显示Java进程状况
显示JVM详细参数,末尾显示JVM默认启动参数
jinfo PID
jinfo -flag MaxMetaspaceSize 18348
jinfo -flag <name> PID
查看JVM参数
jinfo -flag [+|-]<name> PID
布尔类型的JVM参数
jinfo -flag <name>=<value> PID
数字/字符串类型的JVM参数
调整JVM参数
jinfo:实时查看和调整JVM配置参数
-class:监视类加载、卸载数量、总空间以及类装载所耗费的时间
-gc:监视内存中各分代区域的使用情况
-gcutil:监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
每250毫秒查询一次进程2764垃圾收集状况,一共查询20次
jstat -gc 2764 250 20
查看2764进程各分区占比以及Minor GC和Full GC发生的次数和耗时
jstat -gcutil 2764
使用示例
-gcnew:监视新生代的垃圾收集情况
-gcold:监视老年代的垃圾收集情况
jstat:监视虚拟机各种运行状态
扩展:其他方式生成堆转储快照文件
用于查看堆中对象的统计信息 & 生成堆转储快照文件
测试环境压测时导出 dump 文件进行分析,生产环境利用高可用的备份服务器进行操作
JVM停顿时自动生成 dump 文件
设置参数:-XX:+HeapDumpOnOutOfMemoryError
阿里巴巴JVM调优工具:ArtThas
在线定位
应用
在线查看运行的程序中有多少类,每个类产生了多少个对象,共占用多少内存
jmap -histo PID|head 20:列出前20个类的对象个数及占用空间
jmap -histo:live PID:显示堆中对象统计信息,包括类、实例数量、合计容量
jmap -histo PID
生成Java堆转储快照文件,便于下载下来进行离线分析(借助分析工具)
MAT
JProfiler:好用但收费
jhat:堆转储分析工具(jdk自带)
jvisualvm:文件->装入hprof文件
常见分析工具
jmap -dump
jmap:Java内存映像工具
用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者 javacore文件)
线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等
查看线程堆栈
jstack -l PID
jstack PID | grep tid -A100
getAllStackTraces()
扩展
jstack:Java堆栈跟踪工具
常用工具/命令
是一款基于JMX(Java Manage-ment Extensions)的可视化监视、管理工具
启动JConsole后,会自动搜索本机运行的所有JVM进程,相当于可视化的jps命令
“内存”页签的作用相当于可视化的jstat命令,用于监视被收集器管理的虚拟机内存的变化趋势
“线程”页签的功能相当于可视化的jstack命令,用于分析线程停顿的原因,比如死循环,锁等待
JConsole
jvisualvm是功能最强大的运行监视和故障处理程序之一,曾经是Oracle官方主力发展的虚拟机故障处理工具
它除了常规的运行监视、故障处理外,还将提供其他方面的能力,譬如性能分析(Profiling)
优势:对应用程序实际性能的影响也较小,使得它可以直接应用在生产环境中
jps、jinfo、jstat、jstack、jmap、jhat等多种功能于一体
在Profiler页签中,VisualVM提供了程序运行期间方法级的处理器执行时间分析以及内存分析(生产不用)
BTrace动态日志跟踪插件
Java VisualVM
下载解压,启动:java -jar arthas-boot.jar,输入编号选择进程并回车即可看到启动日志
dashboard:展示当前进程的信息,按ctrl+c可以中断执行
显示所有线程信息以及对应的CPU占用情况
thread tid:可以查看指定线程在做什么
thread -b:检测死锁
thread
显示该类对应的类加载器和location
查看代码版本
查看动态代理对象
反编译
jad 类的全路径名
使线上修改的代码立即生效(慎用)
原理:classloader重新加载
redifine /root/.../xxx.class
分析方法的运行时间与运行细节
方法调用追踪
trace 全限定类名 方法名
常用命令
Java Mission Control:可持续在线的监控工具,可用于生产环境
JMC
是一款基于服务性代理(Serviceability Agent,SA)实现的进程外调试工具
JHSDB
GCEasy
GCViewer
GC日志分析工具
可视化故障处理工具
性能监控
此时也可挂上阿里的Arthas查看占用高的线程
先用 top 显示进程列表,找到占用率最高的 PID
如果是业务线程,检查死循环或者大量计算的代码
如果 FGC 的回收正常,说明此时只是压力大
如果 FGC 每次回收很少,说明有内存泄漏
如果是收集线程,检查是否频繁 FGC
再使用 top -Hp pid 找到占用最高的线程 tid
printf '%x\' tid 将线程ID转换为16进制 (小写)
最后使用 jstack pid | grep tid -A60
CPU 占用过高
每隔2秒打印一次CPU信息,共打印3次
cs:表示上下文切换次数(context switch)
vmstat 2 3
针对 pid 进行上下文切换监控
cswch/s:每秒自愿上下文切换(进程无法获取到可供执行的资源从而自愿发生上下文切换)
nvcswch/s:每秒非自愿上下文切换(进程时间片用完、被高优先级进程抢走、系统中断等发生的非自愿的场景)
pidstat -w pid
频繁上下文切换
CPU
代码举例
原因:线程请求栈的深度超过当前Java虚拟机栈的最大深度(默认大小是1M,1024KB)
解决:检测是否有无穷递归、死循环,如果不是bug,可以通过调整虚拟机栈的大小来解决(-Xss2m)
栈内存溢出StackOverflowError
原因:堆内存被用光,或者内存泄漏堆积导致
修复内存泄露,或使用 -Xmx 增加堆内存大小
对象太多(Java heap space)
使用 -XX:-UseGCOverheadLimit 取消 GC 开销限制
GC回收超时(GC overhead limit exceeded)
打印的堆栈跟踪信息,使用操作系统本地工具进行诊断
本地内存不足(Direct buffer memory)
减少虚拟机栈的大小或减少堆内存占用比例
线程太多(unable to create new native thread)
使用-XX: MaxMetaSpaceSize 增加 metaspace 大小
元空间内存不足(Metaspace)
OOM
堆内存溢出OutOfMemory
内存溢出
程序在申请内存后,无法释放已申请的内存空间
每秒打印一次GC详细信息(C:容量|U:已使用)(E:Eden|O:Old|M:MetaSpace)
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
jstat -gc pid 1000
查看最近3次GC空间占比
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
jstat -gcutil 1000 3
使用 jmap 生成 堆转储 快照文件 jmap -histo:live pid
通过 MAT 查看 Histogram 图即可找出是哪块代码
导致频繁 GC
内存泄漏
左上角显示Threads: 28 total,下面显示详细的线程信息
pstree -p 8379 | wc -l
ls /proc/8379/task | wc -l
ps hH p 8379 | wc -l
线程数
top -H -p pid
查看线程信息
内存
故障定位
上线前:根据需求(支撑多少QPS)进行JVM规划和预估调优(几台机器,多大内存,堆内存划分)
上线初期:根据日志优化JVM运行环境(解决慢、卡顿问题)
上线后期:解决JVM运行过程中出现的各种问题(OOM,逃逸分析,频繁 full gc)
调优阶段
-XX:MaxGCPauseMillis:停顿时间(建议)
GC会尝试各种手段达到这个时间,比如减小年轻代
垃圾收集器做垃圾回收中断应用执行的时间
停顿时间
-XX:GCTimeRatio = n(默认值99)即 1% 时间用于垃圾收集
例如将 n=19 则垃圾收集时间为1/(1+19) 即 5% 时间用于垃圾收集
GC时间占用程序运行时间的百分比:1/(1+n)
GC时间比例
吞吐量=应用程序时间/(应用程序时间+垃圾收集时间) 即 1-1/(1+n)
吞吐量
调优指标
通过 java 命令查看
java -version
java -help
常用参数
标准参数,-开头
通过 java -X more 查看
-Xms200m -Xmx200m:最小堆和最大堆
-Xmn60m:新生代
-Xss1m:栈空间大小
防止内存震荡(否则JVM会将堆内存进行扩容和缩容)
扩展和回缩需要大量的计算,影响程序的执行效率
扩展:为什么最小堆和最大堆设置一样大?
-X开头
通过 java -XX:+PrintFlagsFinal -version | less 命令查看
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintHeapAtGC
-XX开头
非标准参数
调优参数
JVM堆栈大小设置?
JVM 调优,让其几乎不发生 Full GC?
日均百万交易系统
调优案例
互斥、请求和保持、不可剥夺、环路等待
四个必要条件
破坏四个必要条件中任意一个都可以
指定加锁顺序、设置超时时间
保证系统动态分配资源后不进入不安全状态
银行家算法
预防死锁
Jstack和Jconsole
死锁检测工具
手写死锁(伪代码)
哲学家用筷子吃饭,每人抢到一根
举例
线程死锁
JVM 调优
先用 ps aux 看看 stat 栏,如果是Z,那就是zombie僵尸进程
ps -ef | grep 僵尸进程ID,可以找到父进程ID,kill 父进程即可
线上进程 kill 不掉怎么办?
先用 df -h 看看磁盘使用情况
检查项目部署的tomcat或者nginx的日志,按天分割,定期删除(crontab)
如果不是日志问题,那就 find / -size+100M | xargs ls -lh,找出大于100M的文件
如果不是大文件导致的,那就 du -h > fs_du.log 查看各目录占用磁盘大小,是否有大量小文件
磁盘空间快满了怎么办?
this指针是何时赋值的?
动态数据类型
数组长度的计算方式与指针压缩的关系
谈谈你对Java数组的理解
类加载器把 class 文件加载到内存生成 class content
Class 对象是 InstanceMirrorKlass 类的实例
对象头中的 Klass Point 指向 InstanceKlass
把 class content 解析成 InstanceKlass 放在方法区同时生成一份镜像类 InstanceMirrorKlass 存在堆区
class文件在内存中是如何存储的?
栈帧中的动态链接中存储的是什么?还有其他思路吗?
oop 最大支持的堆大小是多少?如何扩容?
占用16字节(B)jdk6 以后默认开启 指针压缩(-XX:+/-UseCompressedOops)
开启:MarkWord(8B)+KlassPoint(4B)+数组长度0+实例数据0+对齐填充4B的0
关闭:MarkWord(8B)+KlassPoint(8B)+数组长度0+实例数据0+对齐填充0
图示
Object obj = new Object(); 这个对象占多少字节?
开启是32字节:MarkWord(8B)+KlassPoint(4B)+数组长度4B+实例数据12B+对齐填充4B
关闭是40字节:MarkWord(8B)+KlassPoint(8B)+数组长度4B+头部填充4B+实例数据12B+对齐填充4B
HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说任何对象的大小都必须是8字节的整数倍
一般情况下,对象头中的MarkWord占8字节,KlassPoint开启指针压缩占4字节(默认开启),如果对象为数组,则数组长度占4字节,实例数据根据数据类型做具体判断,如果实例数据是引用类型:开启指针压缩占4字节,不开启占8字节,对齐填充补满8的倍数
依据
字节问题
创建1个String对象,先在字符串常量池中创建\"sliver\"对象,然后将该对象的引用赋值给str
String str = \"silver\";在JVM中创建了几个String对象?
常量池中如果有\"cool\"则创建1个String对象,如果没有,则创建2个String对象
String str2 = new String(\"cool\"); 呢?
对象个数
常见问题
加载:将二进制 class 文件加载到内存
验证:确保类加载的正确性(文件格式、元数据、字节码、符号引用等)
准备:给类的静态变量分配内存,并赋予默认值
解析:把符号引用转化为直接引用
连接
初始化:为静态变量赋予正确的初始值,也就是实际值,准备阶段赋予的是默认的初始值
使用、卸载
1.1 加载过程
1、当虚拟机遇到一条 new 指令时,先去 常量池 检查对应的类是否被加载过,如果没有,则先进行类的加载
2、在类加载检查通过后,为新对象分配内存(内存分配方式有两种:指针碰撞和空闲列表)
3、然后将分配到的内存进行初始化操作,初始化为零值
4、接着做一些必要的对象设置,比如 设置对象头 里的哈希码、元数据、分代年龄、是否偏向锁等
5、最后执行 init() 方法
过程
如果Java堆的内存是规整的,即所有用过的内存放在一边,空闲的内存放在另一边。分配内存时将位于中间的指针向空闲的内存移动一段与对象大小相等的距离即可
指针碰撞
如果Java堆的内存不是规整的,虚拟机必须维护一个列表来记录哪些内存是可用的,分配内存时从列表中找到一块足够大的空间划分给新创建的对象,并更新列表上的记录
空闲列表
内存分配方式
虚拟机采用 CAS+失败重试,保证更新的原子性
CAS同步处理
本地线程分配缓冲(TLAB)
分配内存时还要 考虑并发 问题
对象的创建
哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
占8个字节
Mark Word
对象所属类的元信息在方法区的内存地址
不开启指针压缩:占8个字节
开启指针压缩:占4个字节
类型指针
若对象是一个Java数组,对象头中还必须有一块记录数组长度的数据
注意数组类型在关闭指针压缩时,会涉及到头部填充
占4个字节
数组长度
对象头(Header)
对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容
实例数据(Instance Data)
虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐
对齐填充(Padding)
对象内存结构
Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址(通过栈上的reference数据来操作堆上的具体对象,reference是一个指向对象的引用)
使用句柄
reference中直接存储对象地址,少一次间接访问的开销
直接指针
使用句柄访问的最大好处就是reference中存放的是稳定的句柄地址,在对象被移动时只会改变句柄中实例数据的指针,而reference本身不需要被修改
使用直接指针访问的最大好处就是速度快,节省了一次指针定位的时间开销HotSpot主要使用这种方式进行对象访问(有例外,考虑Shenandoah收集器)
两种方式各有优劣
对象的访问定位
对象探秘
简介
优先委托父类加载器加载,父类加载器都不能加载时才自己加载
安全,防止核心类被随意修改,比如自己写的String类不会被加载,或者自己写了 java.lang.test 类也不会被加载
避免重复加载,父类加载器已经加载过,则子类不需要再次加载
作用及优势
沙箱安全机制
防止类冲突,做资源隔离(不同的类加载器加载相同的class文件是不同的类)
使用用户自定义类加载器就可以打破
如何打破双亲委派?
Tomcat、JDBC、部分热部署框架都打破了双亲委派
打破双亲委派举例?
为什么要打破双亲委派?
类的完整类名必须一致。包括包名
加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
java中表示两个class对象是否同一个类存在两个必要条件
jvm 必须知道一类型是由启动加载器加载的还是由用户类加载器加载的
如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中
当解析一个类型到另一个类型的引用的时候,jvm需要保证这两个类型的类加载器是相同的
类加载器的引用
其他注意
1.3 双亲委派机制
1. 类加载机制(按需加载)
启动类加载器:Bootstrap Classloader JVM自带的
加载的API路径
加载 JAVA_HOME/jre/lib/ext 下的jar文件
扩展类加载器:Extension ClassLoader
加载 classpath 下的文件
系统类加载器:Application ClassLoader(应用类加载器)
最常用
在jdk1.2之前,会去继承ClassLoader类并重写loadClass()方法,之后不再建议去重写loadClass()方法,建议把自定义的类加载逻辑写在 findClass() 方法中。
如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass() 方法及其取字节码流的方式,使自定义类加载器更加简洁。
自定义类加载器实现步骤
负责加载用户自定义路径下的类包
自定义类加载器:必须继承 ClassLoader
获取集中加载类的方式
确定一个类的唯一性
将类的字节码文件从JVM外部加载到内存中
提供隔离特性,为中间件开发者提供便利(例如Tomcat)
作用
1.2 类加载器
java虚拟机定义了若干中程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机推出而销毁(红色区)。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁(灰色区)。
每个线程:独立包括程序计数器、栈、本地栈
线程共享:堆、堆外内存(永久代或元空间、代码缓存)
灰色的为单独线程私有的,红色的为多个线程共享的。
当前线程所执行的字节码的行号指示器
线程私有
实现流程控制 和 记录执行位置
运行时数据区中唯一不会出现OOM的区域,没有垃圾回收
如果线程执行的Java方法,则计数器记录正在执行的虚拟机字节码的指令的地址
如果正在执行的本地方法,这个计数器值则应为空。(undefined)
程序计数器(PC寄存器)
描述Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧。虚拟机栈线程私有(一个线程拥有一个栈),生命周期和线程一致
每个方法从调用到执行完成,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
java方法有两中返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
栈运行原理
基本数据类型
对象引用
returnAddress类型
存储方法参数和定义在方法体内的局部变量
在使用前,必须进行显示赋值!否则,编译不通过
局部变量表
给变量赋值的操作数做加减运算的临时空间
被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条执行的字节码指令
把15放到操作数栈
PC寄存器移动2,15存到局部变量表
把8放到操作数栈
PC寄存器移动5,8存到局部变量表
从局部变量表取出索引为1的放到操作数栈
从局部变量表取出索引为2的放到操作数栈
8和15出栈,由执行引擎解析iadd字节码指令为机器指令被CPU运算,结果放到栈内
存放到局部变量表,return结束
byte short char boolean int 都以int型来保存。局部变量表中非静态的方法默认带有this变量占据0位
代码演示
寄存器:指令更少,执行速度块
由于栈指令不停的入栈出栈,频繁地执行内存读写,影响速度,所以HotSpot JVM 的设计者提出了栈顶缓存技术。将栈顶元素全部缓存在物理CPU的寄存器中,以此降低堆内存的读写次数,提升执行引擎的执行效率
栈顶缓存技术
操作数栈
代码示例
编译字节码后看methodB()第9行指令对应#7符号位
看Constant pool #7的符号位对应#8和#31,常量池作用就是把公共变量或同类型可以有相同的符号位,否则每个方法都有会占用内存Constant pool核心就是提供一些符号和常量,便于指令的识别
#31对应#19和#13 也就是方法methodA:()V,#19其实对应的就是methodA(),#13对应的是void,#32对应的是类信息,告诉我在那个类下
把符号引用转换为直接引用
动态链接
静态链接
引用
invokevirtual 调用所有虚方法,被final修饰的除外
invokeinterface 调用接口,被final修饰的除外
调用指令
子级调用或重写上级方法需要找到上级的方法才能执行,回影响性能,所以把方法都引入到一张表中,用颜色或其他来代替是子类方法,还是调用父类方法会更快。
虚方法表
虚方法
invokestatic 调用静态方法,解析阶段确定唯一方法版本
invokespecial 调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本
非虚方法
静态类型:java是一种静态类型语言,需要准确生命一个变量的类型
动态类型:是根据值的类型判断出变量的类型
动态指令 invokedynamic
方法的调用(额外讲解)
字节码异常表:告诉你从指令4-8有正常处理就到16.如果有异常发生就会到11行
存放调用该方法的PC寄存器的值
方法退出(方法返回地址)
栈帧的结构
虚拟机栈不需要垃圾回收器来回收,随着方法的执行,线程结束后正常回收即可
StackOverflowError
OutOfMemoryError
栈空间可扩展,扩展时无法申请到足够内存,会抛出内存溢出错误
虚拟机栈会有两种情况
我们可以使用参数 -Xss2m 选线来设置现成的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
设置占内存大小
StackOverflowError:当栈帧深度不足的时候,通过设置 -Xss可以设置栈大小
OutOfMemoryError:在动态扩容的时候发现内存不足的时候
举例栈溢出的情况?
不能保证,例如递归,原本可能是5000次,但调大了无非可能涨到7000次还是会溢出
调整栈大小,就能保证不出现溢出嘛?
不是,栈越大,能开的线程就越少
分配的栈内存越大越好吗?
程序计数器:不存在Error 也不存在GC
虚拟机栈:会存在Error,不存在GC 直接出栈
本地方法栈:无法调用的是C相关的方法,存在Error,不存在GC
堆空间 Heap:存在Error,存在GC
方法去:时间比较长的数据,比如类数据本身,存在Error,存在GC
不会的
垃圾回收是否会涉及到虚拟机栈?
方法中定义的局部变量是否线程安全的?
相关面试题
虚拟机栈
本地方法栈与虚拟机栈类似,只不过虚拟机栈是为Java方法提供服务,本地方法栈是为native(c/c++)方法服务
本地方法栈
jinfo -flag UseTLAB pid 查看JVM是否启动该参数
线程共享,但会为线程划分私有缓冲区(TLAB)
实战
该对象在线程的栈中
当一个对象在方法中被定义,对象旨在方法内部使用,则认为没有发生逃逸
sb 引用被返回到供其他方法使用
当一个对象在方法中被定义后,他被外部方法所引用,则认为发生逃逸。例如作为参数传递到其他地方中
栈上分配
代码中hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。
同步省略
point 对象就是 聚合量
经过标量替换后可以看到,Point 这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个聚合量了。那么标量替换有什么好处呢?就是可以大大的减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。标量替换为栈上分配提供了很好的基础。
分离对象或标量替换
逃逸分析代码优化
数组 和 对象 可能永远不会存储在栈上(除了:逃逸分析),因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
占内存最大的一块。Java堆可以处于物理上不连续的内存空间,只要逻辑上连续即可
-Xms 用于表示堆区的起始内存,等价于 -XX:InitialHeapSize,默认情况大小=物理电脑内存大小 / 64
-Mxx 用于表示堆区的最大内存,等价于 -XX:MaxHeapSize,默认情况大小=物理电脑内存大小 / 4
jstat -gc id
jps 获取进程 id
-XX:+printGCDetails
查看内存分配情况
当打印出计算新生代和老年代的时候分配内存的使用后,实际上新生代的幸存者区只需要算一个,因为对象只可能在一个里面呆着
一旦堆区中的内存大小超过 -Xmx 所指定的最大内存时,将会抛出OutOfMemoryError异常
堆空间大小设置
通常会将 -Xms 和 -Xmx 两个参数配置相同,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分割计算堆区的大小,从而提升性能。
Eden : Survivor (from | to)
Eden : s0 : s1 = 8:1:1
新生代 young sapce(1/3)
老年代 old space(2/3)
可以修改 -XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
配置新生代与老年代在堆结构的占比
-Xmn 可以设置新生代的空间大小,大破比例分配
如果 -Xmn 和 -XX:NewRation 都配置了 则以-Xmn为准
配置新生代
默认是 8:1:1
但实际是 6:1:1,要想 8:1:1,需要显示的使用-XX:SurvivorRatio=8
配置Survivor
当Eden满了的时候,s0和s1参与的是被动回收如果s0或s1其中一个被占满,可能会直接到old区当然Eden里的对象也有可能会直接到old区
阈值可以通过 -XX:MaxTenuringThreshold=<N> 进行设置,默认15
对象回收过程
新生代收集(Minor GC/Young GC),只是新生代的垃圾收集
目前只有CMS GC 会有单独收集老年代的行为
注意,很多时候Major GC和Full GC混淆使用,需要具体分辨是老年代会后还是整堆回收
老年代收集(Major GC/Old GC)只是老年代的垃圾收集
混合收集(Mixed GC)收集整个新生代以及部分老年代的垃圾收集,目前只有G1 GC会有这种行为
部分收集不是完整收集整个Java堆的垃圾收集,其中又分为
部分收集 Partial GC
收集整个java堆和方法去的垃圾收集
整堆收集 Full GC
GC种类
GC的主要区域
堆 heap
栈、堆、方法区关系
存储已被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码缓存 等数据
类型的完整有效名称(全名=包名.类名)
类型直接父类的完整有效名(对于interface或是java.lang.Object,都没有父类)
类型的修饰符(public,abstract,final的某个子集)
类型直接接口的一个有序列表
类的哪个类加载器加载的
类、接口、枚举、注解信息
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
域的相关信息包括:域名称、域类型、域修饰符(public、privateprotected,static,final,volatile,transient的某个子集)
域(Field)信息,成员变量
方法名称
方法返回类型(或void)
方法参数的数量和顺序(按顺序)
方法的修饰符
方法的字节码、操作数栈、局部变量表大小(abstract和native方法除外)
异常表(abstract和native方法除外)
方法信息
方法区的内部结构
元空间在1.8中不再与堆是连续的物理内存,而是改为使用本地内存(Native memory)
方法区是规范,永久代/元空间是实现
方法区在jdk1.8以前叫永久代,jdk1.8开始叫元空间
永久代是存放在堆区的,元空间存在于直接内存
永久代和元空间的区别?
为了避免发生OOM异常,替换成元空间不占用堆内存
为什么要移除永久代?
最小、最大元空间设置成一样大
大小设置为物理内存的 1/32
预留20%-30%的空间
元空间的调优
方法区
字面量
符号引用量
字节码文件中的常量池(静态描述)
静态常量池只是一个文件结构,运行时常量池是一块内存区域
静态常量池
虚拟机会在类加载后把各个静态常量池载入到运行时常量池中,用于存放编译期生成的各种 字面量 和 符号引用
运行时常量池可以在运行期间将符号引用解析为直接引用(地址)
JDK1.8废弃了永久代的概念,转移到 元空间 中
运行时常量池
字符串常量池可以理解为运行时常量池的一部分,加载时对于静态常量池,如果其中包含字符串则被加载到这里
String.hashCode
通过字符串的内容计算hashValue
对hashValue进行哈希计算出index
key
存储的是字符串的引用
value
底层是hashtable
JDK1.7后字符串常量池中只保存字符串引用,实际存储位置转移到 堆 中
字符串常量池 String Pool
常量池
方法区/元空间 (线程共享)
运行时数据区
Java字节码 -> c/c++代码 -> 硬编码(机器码)
字节码解释器
Java字节码 -> 硬编码(机器码)
1、将new方法的硬编码拿过来2、申请一块可读可写可执行的内存3、将硬编码写入这块内存4、声明一个函数指针指向这块内存5、通过这个函数指针调用这块内存
底层原理
模板解释器
执行字节码的方式
-Xint:纯字节码解析器运行(interpreted mode)
-Xcomp:纯模板解释器运行(compiled mode)
-Xmixed:默认混合模式(mixed mode)
执行字节码的模式
Java -version 可查看
执行引擎
联系
本地库接口
四大组成
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是可回收的
不能解决循环引用的问题
引用计数算法
两个栈: Java栈 和 Native 栈中所有引用的对象
方法区:方法区中的常量和静态变量
所有跨代引用的对象
和已知 GCRoots 对象同属一个CardTable 的其他对象
GC Roots
强引用:只要强引用还在,垃圾收集器就不会回收
应用:特别适合缓存使用
软引用:在内存不足的情况下,发生内存溢出之前回收
应用:ThreadLocal对象使用弱引用来防止内存泄露
弱引用:非必须对象,只要发生GC就会把它干掉
背景:GC只能管理JVM内存,不能直接管理直接内存
作用:管理直接内存,虚引用被回收前释放直接内存
原理:虚引用的使用需要借助队列,虚引用被回收时会往队列中存入数据,监听队列即可获取通知
应用:Netty中零拷贝(Zero Copy)
虚引用:相当于没有引用,被GC干掉会收到通知
引用类型
不可达对象一定会被回收吗?
可达性分析算法
表示对象 尚未 被垃圾收集器访问过
可达性分析刚开始的时候所有对象都是白色,若分析结束时仍为白色,则认为该对象不可达
白色
表示对象 已经 被垃圾收集器访问过,且这个对象的 所有引用都已经扫描过
黑色表示安全存活的对象,其他引用指向黑色则无需重新扫描,不能直接指向白色
黑色
表示对象 已经 被垃圾收集器访问过,但这个对象上 至少存在一个引用还没有被扫描过
可达性分析的扫描过程可以看作 一股以灰色为波峰的波纹从黑向白推进的过程
灰色
可达性分析时如果只有收集器线程在工作,那不会有任何问题,但是 用户线程和收集器线程并发工作时会有问题
一种是把原本消亡的对象错误标记为存活,这不好但可以容忍,下次收集清理掉浮动垃圾即可
另一种是把原本存活的对象错误标记为已消亡,这是非常致命的,程序肯定会因此发生错误
两种后果
条件一:赋值器插入了一条或多条从黑色对象到白色对象的新引用
条件二:赋值器删除了全部从灰色对象到该白色对象的直接或间接引用
造成原因
两个条件破坏其一即可
破坏第一个条件:将原本的黑色转变为灰色,这样收集器线程会重新扫描灰色对象下的引用
解决方案就是要从头重新标记(remark)
导致CMS的重新标记阶段 STW 时间较长
并发标记时有漏标
使用标记-清除算法会产生大量碎片,如果有大对象进来后发现没有连续的空间存放,则会触发担保机制产生很长的 STW
CMS缺点
CMS解决方案:增量更新(Incremental Update)
当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将记录过的引用关系中的灰色对象为根,重新扫描一次
简化理解:无论引用删除与否,都会按照刚开始扫描那一刻的对象图的快照来搜索
破坏第二个条件
G1采用标记-整理算法,不会产生空间碎片
并发标记后,只扫描标记的灰色对象,停顿时间短
相比CMS
停顿时间可控,在不牺牲吞吐量的前提下,实现低停顿垃圾回收
充分利用CPU,多核条件下的硬核优势来缩短 STW
G1 的优点
G1 解决方案:原始快照(STAB)(Snapshot At The Beginning)
注意
三色标记
标记垃圾对象
此算法把内存空间划为两个相等的区域,每次只使用其中的一个区域,垃圾回收时,遍历当前使用区域,把还存活的对象复制到另外一个区域
存活对象少时效率高,不产生碎片
可用内存变为原来的一半,存活对象多时效率低
优缺点
复制算法
此算法执行分两阶段:第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除
相比于复制算法不浪费内存
会产生碎片,效率低,因为要扫描两次
标记清除
分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,清除未标记的对象并且把存活对象“压缩”到堆的其中一块,按顺序排列
避免了碎片问题和空间占用问题
效率比复制算法低,因为要多维护一个链表使幸存对象连续
标记整理
一个Eden区、两个Survival区(From Survival、To Survival)(8:1:1)
大多数情况下对象在Eden区中分配,当Eden区没有足够的空间时,发起一次Minor GC
为了降低大对象分配内存时造成的内存复制的开销
大对象直接进入老年代
对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁
长期存活的对象进入老年代
Minor GC之后,如果检测到可能长期存活的对象,则让其尽早迸入老年代
动态对象年龄判断
Minor GC之前做风险判断,是否允许担保失败,如不允许则改为Full GC
老年代空间分配担保机制
内存分配回收策略
新生代
标记-清除/整理-算法
Major GC/Full GC
老年代
对永久代的回收主要包括废弃的常量和无用的类
永久代和元空间的区别在于永久代位于JVM的方法区中,元空间并不在虚拟机内存中,而是使用本地内存
方法区/永久代/元空间
Full GC
整个Java堆
分代回收
垃圾回收算法
简单高效,单线程,收集时暂停其他所有线程(STW)
Serial
Serial的多线程版本,是首选新生代收集器,可配合CMS
ParNew
以吞吐量为优先的多线程收集器,不支持CMS
Parallel Scavenge
Serial的老年代版本,可作为CMS的后备预案
Serial Old
Parallel Scavenge的老年代版本,多线程,吞吐量优先
Parallel Old
适用于当今互联网网站或基于浏览器的B/S系统的服务器上,这类应用通常都较为关注服务的响应速度,尽量缩短系统的停顿时间,给用户更好的交互体验
并发收集、低停顿
标记-清除
初始标记:标记GC Roots能直接关联到的对象,速度很快,STW
并发标记:从GC Roots的直接关联对象开始遍历整个对象图,耗时长
重新标记:修正并发标记期间可能造成的改动,速度较慢,STW 略长
并发清除:清除标记阶段已经死亡的对象,此阶段和用户线程并发工作
流程
由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的
对CPU敏感,并发阶段虽然不会导致用户线程停顿,但却由于占用资源导致应用程序变慢,降低总吞吐量
由于CMS在清理时与用户线程并发,运行期间还伴随有新垃圾产生,有可能触发担保机制而产生较大停顿
基于标记清除算法,造成大量的空间碎片,导致没有足够的连续空间存放大对象,不得不提前触发Full GC
缺点
CMS
标记-整理
Parallel Scavenge + Parallel Old(JDK1.8默认)
组合使用
分代模型
把连续的Java堆划分为多个大小相等的独立区域(Region)
专门用来存放大对象的区域(Humongous)
引入分区的思路,弱化了分代的概念
STW
初始标记:标记GC Roots能直接关联到的对象,需要停顿用户线程,但时间很短
并发标记:从GC Roots开始对堆中对象进行可达性分析,找出要回收的对象,耗时较长
最终标记:标记那些在并发标记阶段发生变化的对象,将被回收,需要暂停用户线程
筛选回收:对各个Regin的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,回收一部分Region(必须暂停用户线程,由多条收集器线程并行完成)
Young GC
Mixed GC
G1 收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,也就是说,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量
目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势
采用标记整理算法,不会产生空间碎片
优点
内存占用高
执行负载大
特点
它将 Region 作为单次回收的最小单元,即每次收集到的内存空间都是 Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集
在后台维护一个优先级列表,优先回收价值大的 Region(Garbage First的由来)保证了 G1 收集器在有限的时间内获取尽可能高的收集效率
G1如何实现停顿时间可控?
G1 收集器的停顿预测模型是以衰减平均值(Decaying Average)为理论基础来实现的
衰减平均值更准确地代表“最近的”平均状态,Region的统计状态越新越能决定其回收的价值
怎样建立起可靠的停顿预测模型?
使用记忆集避免全堆作为GC Roots扫描
Region里面存在的跨Region引用对象如何解决?
CMS 收集器采用增量更新算法实现
G1 收集器则是通过原始快照(SATB)算法来实现的
在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?
G1:逻辑分代物理不分代
ZGC
Shenandoah
分区模型
Epsilon
PGC、C4、OpenJ9等
其他收集器
-XX:SurvivorRatio
-XX:PreTenureSizeThreshold:大对象到底多大
-XX:MaxTenuringThreshold:
-XX:+ParallelGCThreads:并行收集器的线程数,一般设置为和CPU核数相同(同样适用于CMS)
-XX:+UseAdaptiveSizePolicy:自动选择各区大小比例
Parallel常用参数
-XX:+UseConcMarkSweepGC
CMS线程数量
-XX:ParallelCMSThreads
使用多少比例的老年代后开始CMS收集,默认是68%(近似值)如果频繁发生SerialOld卡顿,应该调小(频繁CMS回收)
-XX:CMSInitiatingOccupancyFraction
在FGC时进行压缩
-XX:+UseCMSCompactAtFullCollection
-XX:+CMSClassUnloadingEnabled
达到什么比例时进行Perm回收
-XX:CMSInitiatingPermOccupancyFraction
设置GC时间占用程序运行时间的百分比
-XX:GCTimeRatio
停顿时间(建议)GC会尝试各种手段达到这个时间,比如减小年轻代
-XX:MaxGCPauseMillis
CMS常用参数
-XX:+UseG1GC
建议值,G1会尝试调整Young区的块数来达到这个值
GC的间隔时间
-XX:+GCPauseIntervalMillis
分区大小,建议逐渐增大该值
-XX:+G1HeapRegionSize
新生代最小比例,默认为5%
G1NewSizePercent
新生代最大比例,默认为60%
G1MaxNewSizePercent
GC时间建议比例,G1回根据这个值调整堆空间
GCTimeRatio
线程数量
ConcGCThreads
启动G1堆空间占用比例
InitiatingHeapOccupancyPercent
G1常用参数
调优
垃圾收集器
默认为 Info
Trace、Debug、Info、Warning、Error、Off 共六种
日志级别从低到高
JDK9之前使用
JDK9之后使用
-Xlog:gc
查看GC基本信息
JDK9之前
JDK9之后
-Xlog:gc*
查看GC详细信息
-Xlog:gc+heap=debug
查看GC前后 堆和方法区 可用容量的变化
-XX:+PrintGCApplicationConcurrentTime
-XX:+PrintGCApplicationStoppedTime
-Xlog:safepoint
查看GC过程中用户线程并发时间以及停顿的时间
-XX:+PrintTenuring-Distribution
-Xlog:gc+age=trace
查看熬过收集后剩余对象的年龄分布信息
命令
垃圾收集日志
GC
JVM
0 条评论
回复 删除
下一页