1.JVM-知识结构图
2021-07-22 18:09:07 0 举报
AI智能生成
JVM相关知识,详尽
作者其他创作
大纲/内容
JVM
JVM内存模型(运行时数据区域)
JAVA内存区域分布
堆
几乎所有的对象实例
一般对象
最大的区域
逃逸对象除外
逃逸分析
栈上分配对象
标量替换
GC的主要工作区域,包含
新生
老年
永久
异常
执行完GC之后,堆内存依然不足支撑新对象的内存分配
堆内存不足: OutOfMemoryError:Java heap space
相关参数
-Xmx JVM最大内存
-Xms Xms用来设置程序初始化的时候内存栈的大小
线程共享
方法区
类型信息
全局有效名、父类全局有效名、修饰符、有序方法列表
域信息(字段信息)
字段名 类型 修饰符 字段声明顺序
方法信息
方法名、修饰符 , 返回类型。有序参数列表,异常表
类变量
静态变量(GC Root)
常量池
类常量(GC Root)、字段和方法的符号引用
运行时常量池
直接引用
符号引用以一组符号来描述所引用的目标
如果有了直接引用,那么直接引用的目标一定被加载到了内存中。包括
直接指向目标的指针
相对偏移量
一个间接定位到对象的句柄
GC管理
永久代
内存不足
OutOfMoneyError:PermGen Space Exception
虚拟机栈
栈帧(执行方法的同时创建)
局部变量槽: 内部变量(GC Root), 参数引用 基本数据类型(GC Root)
操作数栈 : 执行操作的区域
动态链接 : 指向运行时常量池中该栈帧所属方法的引用
返回地址
压栈、弹栈
压栈
方法被调用入JVM栈
多层调用:多层压栈
弹栈
方法执行完成,弹出JVM栈
1.恢复上层局部变量表和操作数栈
2.把返回值压入调用者的操作数栈
3.调用pc计数器执行后面的一行代码
方法执行过程
单层调用
多层调用
压栈的深度大于JVM所允许的最大深度
StatckOverFlowError
深度动态扩展无压栈最大深度,不断压栈,内存溢出
outOfMemoryError
线程私有
-Xss
本地方法栈
运行机制、异常类型都与虚拟机栈相同
用来执行java native方法
有些虚拟机会把此区域和JVM虚拟机合成一个区域使用 如HotSpot虚拟机
同JVM栈
程序计数器
用来指向下一行需要执行的代码
java方法
虚拟机字节码的指令位置
native方法
undefined
无异常
直接内存
就是JVM以外的机器内存,
比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区(Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来作为此区域的引用进行操作。
-XX
类加载机制
jvm的四种类加载器
启动类加载器(Bootstrap ClassLoader):C++实现,在java里无法获取,负责加载/lib下的类。
扩展类加载器(Extension ClassLoader): Java实现,可以在java里获取,负责加载/lib/ext下的类。
应用程序类加载器(Application ClassLoader):是与我们接触最多的类加载器,我们写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader返回的就是它。
自定义类加载器: 加载我们指定的.class
双亲委派原则
1.加载器收到加载请求, 不直接加载, 委派给父类加载器
每层都如此, 加载请求最终都会传给最顶层的启动类加载器
2.父加载无法找到所需的类时,反馈结果, 由子加载器尝试自行加载
作用
防止不同的类加载器避免重复加载同一类,结果不同
加载过程
加载文件
1.通过类的完全限定名称获取定义该类的二进制字节流。
2.该字节流表示的静态存储结构转换为方法区的运行时存储结构。
3.生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。放入堆区
验证
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证对象:文件类型/元数据/字节码/符号引用
准备
赋值:为类变量(static)分配内存并设置初始值
解析
符号引用转变为直接引用
初始化
顺序
父类
子类
初始化语句
触发条件
new
调用子类
调用类的静态方法
反射-Class.forName(\"com.sh.A\")
被标明启动类
过程
该类没有被加载,加载该类
父类没有被加载,加载父类
该类中有初始化语句,依次执行初始化语句
使用
卸载
类加载分几步,不一定严格按照顺序
1.加载
1.1 通过类的全限定名获取此类的二进制字节流 B
1.2 将B的静态存储结构转化为方法区的运行时数据结构
1.3 在内存中生成一个代表这个类的java.lang.Class对象,此对象作为1.2中运行时数据结构的访问入口
结果:B以虚拟机所需的格式存储在方法区中
连接
2.验证
验证B是否符合要求,没有恶意代码损害JVM
2.1 文件格式验证,要符合class文件格式要求,见https://www.processon.com/mindmap/5d458174e4b01ed2c6ac2cd3
2.2 元数据验证,即语义分析
比如此类是否有父类
如果此类不是抽象类,那是否实现了父类或者接口中要求实现的方法
类的字段和方法是否与父类矛盾
2.3 字节码验证,通过数据流和控制流分析,确定程序运行期间不会对JVM有害
2.4 符号引用验证
比如,这个全限定名字符串代表的类 C 能否被找到
找到了这个类 C,那C的字段和方法是否符合B中所描述的那样
或者,字段和方法能否被B访问,比如private,public等等
3.准备
为static变量在方法区中分配内存并初始化,注意3点
1.不包括实例变量,实例变量将在对象实例化时随着对象一起分配在堆中
2.通常情况下,初始化为零值,比如public static int value = 123,在准备阶段后的初始值为0,而123这个值是在程序编译后由类构造器中赋的
3.特殊情况下,当字段的字段属性表里存在ConstantValue属性时,则初始化为此属性的值如:public static final int value = 123,则在准备阶段后的初始值为123
4.解析
把常量池中的符号引用替换为直接引用
类或接口的解析
字段解析
类方法解析
接口方法解析
解析发生的时机:执行操作符号引用的字节码指令之前,对其使用的符号引用进行解析
ps:
符号引用:用符号描述所引用的目标,符号可以是任何形式的字面量。与JVM内存布局无关,目标不一定已经在内存中。
直接引用:直接指向目标的指针、相对偏移量、句柄等与JVM内存布局相关,目标必然已经在内存中
总之,符号引用就像是:我要引用一个名叫a,类型为A的目标。而直接引用:这个目标在内存单元0x5A3,见https://www.zhihu.com/question/30300585/answer/51313752
5.初始化
真正执行类中定义的Java程序代码
6.使用
7.卸载
垃圾回收机制
识别死亡对象
堆区
死亡的实例对象
引用计数法
优缺点
实现简单
需要额外的空间来存储计数器
没办法解决循环引用的问题
引用计数器 +1 -1 , 0 = 死亡
可达性分析法
机制
判断标准
对象属于根基中的对象
对象被一个可达的对象引用
根集中的对象称之为GC Roots,也就是根对象
虚拟机栈(本地变量表)中引用的对象
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中(Native方法)引用的对象
宣告一个对象的真正死亡,至少要经历两次标记过程:
一次标记
GC Roots为起点 搜引用链 不可达
二次标记
没有覆盖finalize()方法或者finalize()方法已经被JVM调用过
没必要执行finalize()
彻底死亡
否则
执行finalize()
放置在一个叫做F-Queue的队列 在的Finalizer线程执行之前重新与引用链上的任何一个对象建立起关联关系 则成功自救
finalize()方法(析构函数)
逃脱死亡的最后一次机会
可以解决引用计数的循环引用的问题
其他线程可能会更新已经访问过的对象引用
JDK1.2之后加入引用级别
强引用
Object obj = new Object()
软引用
有用但是非必须的对象
内存即将溢出,进行二次回收,若还不足, 则抛内存溢出异常
SoftReference 类来表识引用为虚引用
弱引用
非必需对象,强度要弱于软引用
活到下次垃圾收集前,下次GC运行,必回收
WeakReference 类来表识引用为虚引用
虚引用(幽灵引用或者幻影引用)
最弱的一种引用关系
直接回收
PhantomReference 类来表识引用为虚引用
有人利用幻象引用监控对象的创建和销毁
以GC Root为根节点向下遍历整个对象图,形成引用链, 若一个对象的引用链没有任何节点与GC Root的引用链相关联或者交叉,则此对象判定为无用对象。(过程中遵循三色标记理论来操作)
三色标记理论
废弃常量
一个字符串\"abc\"已经进入了常量池,但是没有被任何一个String对象引用
无用类
所有实例已经被回收
该类的类加载器已经被回收
Class对象没有任何其他地方被引用,无法通过反射访问到该类的方法
JVM可以对同时满足上述3个条件的无用类进行回收,也仅仅是“可以”而不是必然。在大量使用反射、动态代理等场景都需要JVM具备类卸载的功能来防止永久代的溢出。
垃圾回收算法
标记-清除算法
先标记所有回收对象,标记全部完成,统一回收
优点
对象存活多 比较高效 : 不需要对对象进行移动,仅处理不存活的对象
缺点
整体扫描了两次效率低
标记存活对象
清除没有标记的对象
内存空间琐碎
分配较大对象时,无法找到足够的连续内存提前触发另一次垃圾收集
适用场景
老年代
标记-复制算法
复制等大小的1、2块空间,每次只使用1,执行GC时,将1的存活对象复制并移动到2,清理1的内存区域
标记和复制的过程可异步进行
移动指针,按序分配
只在一块区域做回收操作
存活对象少 , 运行高效
需要更大的空闲空间
需要复制移动对象
适合场景
适合年轻代
标记-整理算法
标记所有回收的对象, 把所有的对象向一端移动,清理边界以外的内存,剩下的就是连续的未使用空间和存活的对象。
无碎-无内存空间碎片
移针-移动指针,按序分配
更引-GC处理时间长 更新引用
分代回收算法
思想
不同的对象不同生命周期,不同生命周期分代,不同代采用不同的算法回收,进而提高回收效率
分代
分代回收
新生代(Young Generation) GC复制算法
存放
几乎所有新生成的对象都在此区域
对象存活率低
Eden
大部分对象在此生成
Survivor
Survivor0(FromSpace)
Survivor1(ToSpace)
参数控制
-Xmn: 来控制新生代大小
-XX :SurvivorRatio来控制 Eden 和 Survivor 的比例
老年代(Old Generation) 标记清理或整理算法
生命周期较长的对象 - 新生代中躲过了N此回收的对象,
大对象(需要大量连续存储空间的对象,如大数组)直接分到此区域
分配的规则并不是百分之百固定的,这要取决于当前使用的是哪种垃圾收集器组合和 JVM 的相关参数。
对象存活时间比较长,存活率高
参数设置
参数: 指定新生代大小 -XX:NewSize
指定老年代与新生代大小比例 -XX:NewRatio
指定新生代中伊甸园和存活地大小比例: -XX:SurvivorRatio
回收过程
新生对象时,空间不足,引发YoungGC,将Eden存活对象复制到survivor0,然后清空Eden
当Survivor区也不足以存放Eden和Survivor0的存活对象时, 则直接将放不下的存活对象移动到老年代- 担保机制
当老年代也存放满了,触发 Full GC,也就是新生代、老年代都进行回收
永久代/元空间(Permanent Generation)
连续的堆空间
存放静态类,方法和常量的空间
永久代的垃圾收集和老年代(old generation)捆绑在一起,因此无论哪个满了,都会出发永久代和老年代的垃圾收集
GC回收废弃常量和无用类
永久代的最大可分配内存空间
-XX:MaxPermSize
默认大小64M(64位JVM由于质真膨胀,默认是85M)
异常:java.lang.OutOfMemoryError: PermGen error
Java SE8 特性中,移除永久代,使用元空间
替换原因
随着操作系统的发展,计算机支持的内存从32位的最大2^32字节,变为64位的最大
随着Java 在Web领域的发展,java程序变的得越来越大,需要加载的内容也越来越多,如果使用永久代实现方法区,那么需要手动扩大堆的大小,而使用元空间之后,就可以直接存储在内存当中,不用手动云修改堆的大小。
主要原因
永久代的缺点
永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
永久代空间大小很难确定,太小容易GC/OOM异常,太大占用内存(元空间并不在虚拟机中、而是使用本地内存,大小仅受本地内存限制)
永久代调优困难
垃圾回收频率低
元空间的优点
大小
内存分配
调优方便
工具便捷
GC性能
元空间只有少量的指针指向Java堆
减少了根对象的扫描(不再扫描虚拟机里面的已加载类的字典以及其它的内部哈希表)
减少了Full GC的时间
G1回收器中,并发标记阶段完成后可以进行类的卸载
元空间
JDK 8的HotSpot JVM现在使用的是本地内存来表示类的元数据,这个区域就叫做元空间。
原来类的静态变量和Interned Strings 都被转移到了java堆区,
类和其元数据的生命周期和其对应的类加载器是相同的
Hotspot中的元数据现在存储到了元空间里。mmap中的内存块的生命周期与类加载器的一致。
晋升机制
根据存活时间(年龄>15)
GC模式
Partial GC(部分收集)
MinorGC(新生代的GC)
当Eden区写满
频繁 回收速度快
Magor GC(老年代的GC)
只有CMS的concurrent collection是这个模式
Mixed GC混合收集
Full GC(整个Java堆和方法区收集)
整个Java堆和方法区垃圾收集
老年代写满
永久代(方法区)写满
System.gc时,系统建议执行Full GC,但不是必须执行
GC发生时的线程处理
抢先式中断
不需要线程的执行代码主动去配合,在 GC 发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上
现在几乎没有虚拟机采用这种方式来暂停线程从而响应 GC 事件。
主动式中断
当发生GC时,设置一个标记,所有线程在到达Safepoint时判断这个标记,如果中断标记为真就自己中断挂起
JVM调优+OOM
OOMOutOfMemery(内存不够用)
直接内存不够用
内存利用不当
①内存溢出 需要内存比剩余内存多 ②内存泄漏 对象使用完毕后,不能够及时销毁
堆OOM:Java heap space
增大Xmx值 使用MAT,JProfile等工具进行代码分析与优化
直接内存OOM
增加MaxDirectMemorySize的值
过多线程OOM:OOM unable to create native thread
你的应用创建了太多的线程,一个应用进程创建多个线程,超过系统承载极限
你的服务器并不允许你的应用程序创建这么多线程,linux系统默认允许单个进程可以创建的线程数是1024个。你的应用创建超过这个数据,就会报java.lang.OutOfMemoryError:unable to create new native thread.
解决方法:1.降低应用程序创建线程的数量
解决方法:2确实需要创建很多线程,远超过linux系统默认的1024个线程的限制,可以通过修改linux服务器配置,扩大linux默认限制
永久代(Pern)溢出:OOM Permgen sace
增大MaxPremSize的值
减少系统需要的类的数量
使用ClassLoader合理装载类,并进行回收
GC效率低下引起的OOM
花在GC上的时间是否超过了98%
老年代释放的内存是否小于2%
Eden区释放的内存是否小于2%
是否连续5次GC都出现了上述几种情况
关闭OOM可以通过参数:-XX:-UseGCOverheadLimit 进行设置
Java进程消耗CPU过高、进程线程数过多、内存泄露、线程死锁、锁争用
垃圾回收器
CMS
以获取最短回收停顿时间为目标的收集器。
年轻
edan
s1
s2
minor gc
通过阈值晋升
major gc 等价于 full gc
对cpu资源敏感
无法处理浮动垃圾
基于标记清除算法 大量空间碎片
G1
分区概念 弱化分代
标记整理算法 不会产生空间碎片 分配大对象不会提前full gc
可以设置预设停顿时间
充分利用cpu 多核条件下 缩短stw
收集步骤
初始标记 stw 从gc root 开始直接可达的对象
并发标记 gc root 对对象进行可达性分析 找出存活对象
可达性分析算法
最终标记
筛选回收
根据用户期待的gc停顿时间指定回收计划
回收模式
young gc
回收所有的eden s区
复制一些存活对象到old区s区
mixed gc
区别
g1分区域 每个区域是有老年代概念的 但是收集器以整个区域为单位收集
g1回收后马上合并空闲内存 cms 在stw的时候做
内存区域设置
XX:G1HeapRegionSize
复制成活对象到一个区域 暂停所有线程
serial收集器
新生代:采用复制算法,Stop-The-World
老年代:采用标记-整理算法,Stop-The-World
ParNew收集器
是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,目前只有它能与CMS收集器配合工作。
实战
性能调优
设置堆的最大最小值 -xms -xmx
调整老年和年轻代的比例
-XX:newSize设置绝对大小
防止年轻代堆收缩:老年代同理
主要看是否存在更多持久对象和临时对象
观察一段时间 看峰值老年代如何 不影响gc就加大年轻代
每个线程默认会开启1M的堆栈 存放栈帧 调用参数 局部变量 太大了 500k够了
原则 就是减少gc stw
FullGC 内存泄露排查
jasvism
dump
监控配置 自动dump
方法内部的对象,被外部引用,导致无法进行回收,造成内存逃逸。
关闭逃逸分析命令: -XX:-DoEscapeAnalysis
栈上分配:
控制对象的内存分配只存在于栈中, 这样方法结束,就会随栈销毁,提高GC效率
JVM性能检测工具
Jconsole
Jprofiler
jvisualvm
MAT
0 条评论
回复 删除
下一页