JVM详解(Hotspot)
2023-02-15 16:29:27 0 举报
AI智能生成
登录查看完整内容
JAVA8 JVM
作者其他创作
大纲/内容
指当前线程正在执行的字节码指令的地址
如果正在执行的是Navite方法,这个计数器值则为空(undefined)
确保多线程情况下程序正常执行
JVM中唯一不会OOM的内存区域
它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
程序计数器
局部变量存储八大基本数据类型数据
对象引用(reference类型)
returnAddress 类型
局部变量表
存放我们方法执行的操作数
操作数栈
Java 语言特性多态(需要类加载、运行时才能确定具体的方法)
动态链接
恢复上层方法的局部变量表和操作数栈
把返回值(如果有的话)压入调用者栈帧的操作数栈中
调整 PC 计数器的值以指向方法调用指令后面的一条指令
正常返回
通过异常处理器表<非栈帧中的>来确定
异常
方法返回地址
栈帧
线程请求的栈深度大于虚拟机所允许的深度:StackOverflowError
JVM 动态扩展时无法申请到足够的内存时:OutOfMemoryError
虚拟机栈(-Xss1M)
与虚拟机栈缩发挥的作用非常相似
本地方法栈 native 方法调用 JNI 到了底层的 C/C++(c/c++可以触发汇编语言,然后驱动硬件)
本地方法栈
线程私有区域
即在编译时用符号引用来代替引用类,在加载时再通过虚拟机获取该引用类的实际地址
符号引用
字符串的字面量,如“abc”
八种基本类型的值
声明为final的常量
字面量
运行时常量池
static静态变量
final类型常量
类的完整有效名、返回值类型、修饰符(public,private...)、变量名、方法名、方法代码、这个类型直接父类的完整有效名(除非这个类型是 interface 或是java.lang.Object,两种情况下都没有父类)、类的直接接口的一个有序列表
类型信息
即时编译后的代码缓存等数据
方法区/永久代
即时编译器:可以把Java的字节码,包括需要被解释的指令的程序转换成可以直接发送给处理器的指令的程序
逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者其其它线程所引用。
概念
Eden区
Survivor(from)区
Survivor(to)区
新生代
老年代
永久区/元空间(8之后)
堆
线程共享区域
使用 Native 函数库直接分配堆外内存(NIO)
不能设置为大于堆最大值,大于堆最大值以堆最大值为准
直接内存避免了在 Java 堆和 Native 堆中来回复制数据,能够提高效率
直接内存
第一节:内存结构
判断对象对应的类是否在家、链接、初始化
指针碰撞
如果内存规整
虚拟机需要维护一个列表
空闲列表分配
如果内存不规整
说明
为对象分配内存
采用CAS失败重试、区域加锁保证更新的原子性
每个线程预先分配一块TLAB---通过-XX:+/-UseTLAB参数来设定
处理并发安全问题
所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用
初始化分配到的空间
设置对象的对象头
执行init方法进行初始化
对象创建步骤
使用过的内容放在一边,空闲的内存放在另一边,中间放一个指针,分配内存时只需要把指针往空闲内存一段距离
已使用内存和空闲内存交错在一起,维护一个空闲内存列表,分配时从列表中直接找到一块足够大的内存分配
空闲列表
分配方式
CAS:直接分配到堆上
分配缓冲TLAB:把内存分配的动作按照线程划分在不同的空间之中
分配位置
对象的分配
哈希码
GC分带年龄
锁状态标志
线程持有的锁
偏向线程 ID
偏向时间戳
......
运行时数据
即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
是类型指针
如果是数组还需要记录数组的长度
在32位系统下,对象头8字节,64位则是16个字节【未开启压缩指针,开启后12字节】
对象头
对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)
想同宽度的字段总是被分配在一起
父类定义的变量会出现在子类之前
规则
对象的实例数据
对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用
对齐填充
对象的内存布局
Java 堆中将会划分出一块内存来作为句柄池
栈中的reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息
句柄
reference 中存储的直接就是对象地址
指针
使用句柄来访问的最大好处就是 reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要修改
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在 Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本
两种方式的比较
对 Sun HotSpot 而言,它是使用直接指针访问方式进行对象访问的
对象的访问方式
对象优先在Eden区分配
大对象直接进入老年代
长期存活对象进入老年区
对象年龄动态判定
空间分配担保
内存分配
JVM如何实现泛型
第二节:JVM中的对象
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收
优点:快,方便,实现简单
缺陷:对象相互引用时(A.instance=B 同时 B.instance=A),很难判断对象是否该回收
引用计数法
如:Java类的引用类型静态变量
方法区中类静态属性引用的对象
如:字符串常量池(String Table)里的引用
方法区中常量引用的对象
如:各个线程被调用的方法中使用到的参数、局部变量等
虚拟机栈(本地变量表)中引用的对象
本地方法栈JNI(Native方法)中引用的对象
基本数据类型对于的Class对象
一些常驻的异常对象(如NullPointException、OutOfMenoryError)等
系统类加载器
Java虚拟机内的引用
所有tongue锁(synchronized关键字)持有的对象
反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
GC Roots
可达性分析算法
可以完成对象的拯救,虚拟机不能保证该方法执行
finalize方法一个对象只能执行一次
不推荐使用,其运行代价昂贵,不确定性大,无法保证各个对象的调用顺序
finalize
对象的存活
在程序代码中普遍存在的引用赋值
Object obj = new Object()
只要引用关系还存在,永远不会被回收,直至系统OOM
强引用
还有用,但非必须的对象
内存不足,即将OOM时会被回收
软引用SoftReference
无论内存是否足够,在每次垃圾回收时就会被回收掉
弱引用WeekReference
无法通过虚引用来获得一个对象实例
设置虚引用的目的只是为了能在这个对象内回收时收到一个系统通知
虚引用PhantomReference
回收对象引用类型
废弃的常量
该类所有的实例都已经被回收,java堆中不存在该类及其任何派生子类的实例
加载该类的类加载器已经被回收
该类对于的Class对象没有在任何地方被引用
不在使用的类型
回收内容
方法区回收
主要发生在老年代上(新生代也会回收),较少发生,执行速度较慢
调用 System.gc()
老年代区域空间不足
空间分配担保失败
JDK 1.7 及以前的永久代(方法区)空间不足
CMS GC 处理浮动垃圾时,如果新生代空间不足,则采用空间分配担保机制,如果老年代空间不足,则触发 Full GC
触发条件
Full GC
发生在新生代上,发生的较频繁,执行速度较快
Eden 区空间不足\\空间分配担保
新生代收集(Minor GC)
目标只是老年代的垃圾收集
目前只有CMS收集器会有单独收集老年代的行为
老年代收集(Major GC)
收集整个新生代以及部分老年代
目前只有G1收集器会有
混合收集(Mixed GC)
部分收集(Partial GC)
内存回收
当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
过程
将可用内存缩小为原来的一般,浪费太多空间
缺点
复制算法
首先标记所有需要回收或者不需要回收的对象
根据标记结果清理不需要的对象
效率问题,标记和清除效率都不高
标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
标记-清除
首先标记出所有需要回收的对象
后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
不存在内存碎片
优点
内存回收更加复杂
停顿时间更长
标记-整理
根据各个年代的特点选取不同的垃圾收集算法
分代回收算法
垃圾收集算法
标记-复制算法
单线程
Serial收集器
并行的多线程收集器
ParNew收集器
Parallel Scavenge收集器
标记-整理算法
Serial Old收集器
并发多线程收集器
ParNew Old收集器
标记-清除算法
并行与并发收集器
初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿(STW -Stop the world)。
并发标记:从 GC Root 开始对堆中对象进行可达性分析,找到存活对象,它在整个回收过程中耗时最长,不需要停顿。
重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿(STW)。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
并发清除:不需要停顿。
CMS收集器
跨新生代和老年代;复制回收+标记整理
Young GC
Mixed GC
模式
停顿用户线程
初始标记
不停顿用户线程
并发标记
最终标记
筛选回收
回收过程
空间整合:不产生内存碎片
可预测的停顿:使用 Region 划分内存空间以及有优先级的区域回收方式
特点
G1收集器
ZGC
Shenandoah
垃圾收集器
第三节:垃圾回收算法与垃圾回收器
每个 Class 文件的头 4 个字节称为魔数(Magic Number)
唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件
魔数
紧接着魔数的 4 个字节存储的是 Class 文件的版本号
第 5 和第 6 个字节是次版本号(MinorVersion),第 7 和第 8 个字节是主版本号(Major Version)。
Java 的版本号是从 45 开始的
版本号
文本字符串
声明为 final 的常量值等
被模块导出或者开放的包(Package)
类和接口的全限定名(Fully Qualified Name)
字段的名称和描述符(Descriptor)
方法的名称和描述符
方法句柄和方法类型
动态调用点和动态常量
常量池
这个 Class 是类还是接口
是否定义为 public 类型
是否定义为 abstract 类型
如果是类的话,是否被声明为 final 等
访问标志
类索引用于确定这个类的全限定名
父类索引用于确定这个类的父类的全限定名
接口索引集合就用来描述这个类实现了哪些接口
类索引,父类索引,接口索引集合
字段(field)包括类级变量以及实例级变量
字段表集合
名称索引
描述符索引
方法表集合
属性表集合
class文件结构
动态绑定
参数传递
解析
静态分派的典型应用是方法重载
静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的
静态类在创建其子类对象,类型定义为父类,则在重载方法中调用接收父类参数方法
静态分派
运行期根据实际类型确定方法执行版本的分派动作
动态分派
单分派
多分派
分派
方法调用
通过一个类的二进制字节流(通过一个类的全限定名)
字节流所代表的静态的存储结构转化为方法区的运行时数据结构
入口:在内存中生成一个代表这个类的java.lang.Class的对象,作为方法区这个类的各种数据的访问入口
加载
保证输入的字节流正确地解析并存储与方法区之内,格式上符合描述一个Java类型信息的要求
文件格式验证
对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》
元数据验证
通过数据流分析和控制流分析,确定程序语义是否是合法的、符合逻辑的
对元数据信息中的数据类型校验完毕后,对类的方法(Class文件中的Code属性)进行校验分析,保证被校验的方法在运行时不会做出危害虚拟机安全的行为
字节码验证
对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验
符号引用验证
验证
这时候进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java堆中
这里所说的初始值“通常情况”下是数据类型的零值
准备
虚拟机将常量池内的符号引用替换为直接引用的过程
链接
初始化阶段是执行类构造器<clinit>()方法的过程
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态代码块中只能访问到定义在静态语句之前的变量
初始化
使用
卸载
类生命周期
类加载过程
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性
除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器
无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载
双亲委派模型
负责将存放在<JAVA_HOME>\\lib 目录中的,或者被-Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的类库
启动类加载器(Bootstrap ClassLoader)
它负责加载<JAVA_HOME>\\lib\\ext 目录中的,或者被java.ext.dirs 系统变量所指定的路径中的所有类库
扩展类加载器(Extension ClassLoader)
它负责加载用户类路径(ClassPath)上所指定的类库
应用程序类加载器(Application ClassLoader)
类型
类加载器
Tomcat类加载机器
第四节:JVM执行子程序
程序在申请内存时,没有足够的内存空间
方法死循环递归调用(StackOverflowError)
不断建立线程(OutOfMemoryError)
栈溢出
不断创建对象,分配对象大于最大堆的大小(OutOfMemoryError)
堆溢出
方法区和运行时常量池溢出
本机内存直接溢出
内存溢出的构造方式
内存溢出
程序在申请内存后,无法释放已申请的内存空间
长生命周期的对象持有短生命周期对象的引用
连接未关闭
变量作用域不合理
内部类持有外部类
Hash值改变
原因
内存泄漏
浅堆 :(Shallow Heap)是指一个对象所消耗的内存
深堆 :这个对象被 GC 回收后,可以真实释放的内存大小,也就是只能通过对象被直接或间接访问到的所有对象的集合
分析工具MAT
列出当前机器上正在运行的虚拟机进程
jps
用于监视虚拟机各种运行状态信息的命令行工具
jstat
实时查看和调整虚拟机各项参数
jinfo
用于生成堆转储快照(一般称为heapdump 或 dump 文件)
jmap
分析jmap生成的堆转储快照
jhat
用于生成虚拟机当前时刻的线程快照
jstack
jsconsole
visualvm
JDK工具
基于服务性代理的调试工具
JHSDB
可视化工具
JVM编译期优化
调优案例与实战
生产服务器推荐开启
调优之前开启、调优之后关闭
考虑使用
GC 的重要参数
第五节:JVM性能优化
调优的原则
调优的目的
日志分析
阅读 GC 日志
调优步骤
项目启动 GC 优化
GC 调优实战
推荐策略
逃逸分析
响应时间
并发数
吞吐量
常用的性能评价/测试指标
不应该把大量的时间耗费在小的性能改进上,过早考虑优化是所有噩梦的根源
避免过早优化
所有的性能调优,都有应该建立在性能测试的基础上
进行系统性能测试
寻找系统瓶颈,分而治之,逐步优化
减少请求数
使用客户端缓冲
启用压缩
资源文件加载顺序
减少 Cookie 传输
cookie 包含在每次的请求和响应中,因此哪些数据写入 cookie 需要慎重考虑
友好的提示(非技术手段)
CDN 加速
反向代理缓存
WEB 组件分离
浏览器/App
缓存的基本原理和本质
合理使用缓存的准则
缓存
集群
异步
多线程
消息队列
程序
资源的复用
存储性能优化
应用服务性能优化
常用的性能优化手段
第六节:JVM 调优和深入了解性能优化
Builder 模式
构造器参数太多怎么办
不需要实例化的类应该构造器私有
避免无意中创建的对象,如自动装箱
不要创建不必要的对象
避免使用终结方法
使类和成员的可访问性最小化
使可变性最小化
复合优先于继承
接口优于抽象类
可变参数要谨慎使用
返回零长度的数组或集合,不要返回 null
优先使用标准的异常
用枚举代替 int 常量
将局部变量的作用域最小化
精确计算,避免使用 float 和 double
当心字符串连接的性能
控制方法的大小
第七节:编写高效优雅 Java 程序
JVM
0 条评论
回复 删除
下一页