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