JavaVritualMachine知识点总结
2021-10-25 15:27:34 9 举报
AI智能生成
JVM面试点一网打尽
作者其他创作
大纲/内容
什么时候会触发full gc
1、minor gc时,老年代可用空间大小比minor gc所有对象大小小,且没有设置担保,就会进行full gc
2、minor gc......设置了空间担保参数,minor gc 平均存活对象大小大于老年代可用空间大小,会触发full gc
3、minor gc.....设置了空间担保,minor gc 平均大小小于老年代可用,minor gc后存活对象大于survivor及老年代大小,触发full gc
4、老年代内存使用率超过92%,直接触发Full gc
5、方法区/永久代满了
什么时候触发minor gc
新生代没有足够的内存空间来分配对象时,就会触发一次minor gc
类加载过程
定义
jvm把描述类的数据从class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被jvm直接使用的Java类型
过程(由上到下)
加载
查找和导入class文件
将二进制的class文件数据读到内存,放到jvm运行时数据区的方法区,然后在堆内存创建一个java.lang.Class对象,用来封装方法区的Class对象
链接
验证
检查载入Class文件中字节流包含信息符合当前jvm要求的,不会危害jvm安全
1、文件格式校验,保证输入的字节流都能正确解析并存储到方法区内
2、元数据校验
3、字节码校验
4、符号引用校验
准备
给类的静态变量分配存储空间并设置初始值,初始值为数据类型的默认零值,而不是代码中显示赋予的值
解析
把常量池内符号引用转成直接引用
初始化
对类的静态变量,静态代码块执行初始化操作,JVM对类变量进行初始化
对类变量进行初始值设定有两种方式
声明类变量时指定初始值
使用静态代码块为类变量指定初始值
使用
卸载
何时开始类的初始化
主动引用,触发类的初始化
1、创建类的实例
2、访问类的静态变量(除常量【被类修饰的静态变量,编译器不会通过字节码来加载值,而是直接把值插入字节码中】)
3、访问类的静态方法
4、反射
5、初始化类时,发现父类还没初始化,则先初始化父类
6、定义了main()方法那个类先初始化
被动引用,不会触发类的初始化
子类调用父类的静态变量,只会初始化父类
通过定义的数组引用类不会初始化
调用常量不会初始化
类初始化的顺序
1.类从顶至底的顺序初始化,所以声明在顶部的字段的早于底部的字段初始化
2.超类早于子类和衍生类的初始化
3.如果类的初始化是由于访问静态域而触发,那么只有声明静态域的类才被初始化,而不会触发超类的初始化或者子类的
4.初始化即使静态域被子类或子接口或者它的实现类所引用
5.接口初始化不会导致父接口的初始化
6.静态域的初始化是在类的静态初始化期间,非静态域的初始化时在类的实例创建期间。这意味这静态域初始化在非静态域之前。
7.非静态域通过构造器初始化,子类在做任何初始化之前构造器会隐含地调用父类的构造器,他保证了非静态或实例变量(父类)初始化早于子类
类加载器
实现加载阶段通过类的全名获取二进制字节流的过程
双亲委派模型
类加载器
启动类加载器
负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库到JVM中
无法被java程序直接引用
扩展类加载器
负责加载JAVA_HOME\lib\ext
可被开发者直接使用
应用程序类加载器
加载Classpath上指定的类库到JVM
开发者可以直接使用
用户自定义类加载器
过程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
自定义类加载器
1、通过继承java.lang.ClassLoader
2、重写findClass方法
老年代中引用了新生代的对象,那么新生代对象会被回收吗
不会
当老年代中存活的存活的对象很多时,为了避免新生代GC来扫描老年代的对象影响性能,老年代终会有一个写屏障来管理一个卡表(card table),保存了老年代对新生代对象的引用
card table
目的:解决跨代引用问题
本质:用来标记卡页状态,每个卡表项对应一个卡页,当卡页中一个对象引用有写操作时,写屏障将会标记对象所在的卡表状态为dirty
调优经验
目的:尽可能避免对象进入老年代,导致最终的full gc
新生代调优
1、survivor空间够不够
2、多少岁的对象进入老年代
3、多大对象直接进入老年代
老年代调优
运行时数据区域
程序计数器
字节码指令指示器
metaspace/方法区
存放类相关信息,静态变量,常量
本地方法栈
一些不是java写的native方法
虚拟机栈
一个方法创建栈帧进行出入栈,局部变量引用
堆
实例对象
垃圾回收
垃圾的定义
实例对象没有被任何方法的局部变量指向它,也没有任何一个类的静态变量或者是常量指向他
判定回收对象的算法
引用计数法
当一个对象被局部变量或者静态变量引用时计数器+1,引用失效就-1,为0就表示没被任何变量引用
问题: 解决不了循环引用的问题
可达性分析
GCRoots
虚拟机栈方法中的局部变量
方法区中的静态变量
方法区中的常量
通过GCRoots一层一层向下去看能到达的对象,说明该对象就是被引用
引用
强引用
最普通的代码,一个变量引用一个对象,这种情况下绝对不会被回收
软引用
实例对象通过“SoftReference”弱引用对象包装起来,此时的栈帧中的引用就是软引用,一般不回收,但是回收以后发现空间仍然不够就要进行回收软引用的对象
一般用作缓存
弱引用
实例对象通过“WeakReference”弱引用对象包装,此时栈帧中的引用就是弱引用,垃圾回收就给回收了
应用:threadlocal
虚引用
作用:管理堆外内存,比如NIO中的DirectByteBuffer,当buffer对象被回收,虚引用信息会放到队列中,有个GC线程监听队列中是否有数据,有数据则清理相关堆外内存,避免内存泄漏
对象“重生”
不可达对象也不是马上就被回收
如果不可达对象重写了finalize()方法并且在其中重新被引用并且是finalize方法第一次被执行时,对象可以重生
内存分代对象分配
由于大部分对象都是存活时间很短,只有少部分对象长时间存活所以区分了新生代和老年代
新生代
新生代没有空间存放对象时触发Minor gc
-Xmn:新生代大小
老年代
对象躲过15次Minor Gc,具体次数通过参数“-XX:MaxTenuringThreshold”控制
动态年龄判断: 年龄1 + 年龄2 + ... + 年龄n 占据空间大于50%,把年龄n以上的送入老年代
大对象直接进入老年代: 通过参数“-XX:PretenureSizeThreshold”控制单位是字节
minor gc 后,survivor区域放不下存活对象,则直接进入老年代
老年代空间担保策略
在Minor Gc前都会比较老年代剩余空间大小和新生代所有对象大小
前者大于后者
直接Minor Gc,即使GC后所有对象存活也可以进入老年代
前者小于后者
看是否配置了“-XX:HandlePromotionFailure”
没配置
FullGC
full gc 一般会带着minor gc 如果full gc后仍然放不下minor gc存活对象就OOM,否则进入老年代
判断老年代可用大小与Minor Gc平均存活大小
前者大于后者
Minor Gc
Gc后存活对象小于survivor
Gc后存活对象大小大于survivor小于老年代可用大小,则存活对象进入老年代
GC后存活大小大于老年代可以用大小
Full GC
full gc 一般会带着minor gc 如果full gc后仍然放不小minor gc存活对象就OOM,否则进入老年代
后者大于前者
Full Gc
full gc 一般会带着minor gc 如果full gc后仍然放不小minor gc存活对象就OOM,否则进入老年代
GC触发
Minor Gc前发现老年代可用大小小于MinorGc ,可能触发Full GC顺带着minor gc
Minor Gc 后发现存活对象大小大于survivor区 与 老年代可用大小
永久代
方法区
1.8以前
-XX:PermSize 永久代大小
-XX:MaxPermSize 永久代最大大小
1.8以后
-XX:MetaspaceSize
-XX:MaxMetaspaceSize
-Xms:初始堆大小,-Xmx:堆的最大值
-Xss 每个线程栈内存大小
垃圾回收算法
标记-清除算法
思想:分为标记和清除两个阶段,先标记哪些可以回收,然后进行回收
缺点
标记和清除的效率不高
产生大量的内存碎片导致内存空间浪费
复制算法(新生代垃圾回收)
思想:将内存区域一分为二,所有对象分配在一块区域,垃圾回收时所有存活对象移动至另一区域,清空当前区域,循环交替
优点:效率高,不产生碎片
缺点:内存利用低
解决方法:划分为Eden和Survivor区 8 : 1 : 1,每次对象分配在Eden和一个Survivor区内存利用率90%
Eden 1个
Survivor 2个
为什么要两个Survivor?解决碎片化问题
标记-整理算法(老年代垃圾回收)
思想:先标记存活对象,再把存活对象挪到一端,最后清除垃圾对象
优点:碎片少
缺点:效率低
垃圾收集器
新生代垃圾收集器
Serial
复制算法下单线程垃圾回收
ParNew
复制算法下多线程垃圾回收
开启:"-XX:+UseParNewGC"
默认垃圾回收线程数量:与CPU核数一致
老年代垃圾收集器
Serial Old
单线程垃圾回收器,需要长时间"stop-the-word"进行GcRoots追踪,垃圾全部标记,全部清除,再恢复系统
CMS
整体思想:“标记-清理算法与标记-整理”兼备,为了解决stop-the-word导致系统长时间没响应,所以CMS尽量让垃圾清理线程和工作线程同时进行
步骤
初始标记阶段
思想:标记出GCRoots所有引用对象
需要stop-the-world,但是可作为GCRoots的对象很少:方法中的局部变量引用,类的静态变量对象,类的常量引用对象
并发标记阶段
思想:对GCRoots对象进行追踪,其他的引用对象
最耗时,但是可以有多线程同时工作,不需要stop-the-world
重新标记阶段
思想:重新标记第二阶段产生的新对象及一些失去引用的对象
需要stop-the-world,但是状态变化的对象少,所以速度很快
并发清理阶段
清理之前所标记的垃圾
不需要stop-the-world
问题
CPU资源消耗
在并发标记和并发清理两个阶段多线程处理,一个机器的总线程数一定,所有消耗一定CPU资源:默认线程数(CPU核数+3)/4
Concurrent Mode Failure
由于在并发清理阶段没有stop-the-world,所以还是会有新的老年代对象变为的浮动垃圾,这部分垃圾不会被回收,因为没被标记,所以老年代一般会预留一部分空间来存放垃圾清理阶段新进入老年代的对象,当老年代存放对象到达一定比例时就会触发CMS垃圾回收,这个比例可以用参数进行控制
控制参数:"-XX:CMSInitiatingOccpancyFaction" 一般情况设置为92%,就是说当老年代对象达到老年代空间的92%就会触发CMS,即预留了8%的空间来存放在并发清理阶段,系统进入老年代的对象,如果在这期间,系统进入老年代的对象大小超过了预留空间大小,就会触发Concurrent Mode Failure,强制把CMS--->SerialOld 进行垃圾回收
内存碎片问题
由于CMS采用“标记-清理”算法思想,导致产生大量内存碎片问题,造成内存浪费,频繁GC
为什么不采用“标记-整理”
老年代中要么是大对象,要么是存活很久的对象,移动他们过程需要拷贝,所以性能不划算
解决方案
参数:“-XX:UseCompactAtFullCollection” 默认打开,意思是FullGc后再Stop-the-world,进行对象碎片整理
参数:“-XX:CMSFullGcsBeforeCompactions”= 0 表示一次FullGc后就进行碎片整理 =1 表示两次FullGc之后再进行碎片整理
G1
为什么需要G1
ParNew+CMS的最大痛点是会导致Stop-the-world,且不可控
开启G1
参数“-XX:UseG1GC”
主要思想
只回收部分垃圾来减少停顿时间
将堆内存划分为多个大小相等的region
只有逻辑上的新生代与老年代,region既可能是新生代也可能是老年代,由G1自动控制
回收价值
回收多大的内存需要的停顿时间
特点
可以设置在一定时间内的垃圾回收预期停顿时间
预测下一次GC会让应用系统暂停多长时间
适合处理大堆,并能够高效率利用服务器多核的性能
region数量和大小
region大小默认1MB
TARGET_REGION_NUMBER数量
2048
大小
自动
根据-Xms和-Xmx来限定堆内存大小,-Xms除2048,每个region大小必须是2的倍数
手动
参数“-XX:G1HeapSize”
参数
MIN_REGION_SIZE = 1MB(最小的regionSize)
MAX_REGION_SIZE= 32MB(最大的regionSize)
逻辑上region分代
新生代
默认占堆内存的5%,也可以通过"-XX:G1NewSizePercent" 指定,通过“-XX:G1MaxNewSizePercent”设置新生代最大比例,默认60%
Eden
Survivor
老年代
除大对象以外,其他进入老年代的条件一致
region之间的引用rememberSet来记录,rememberSet大小占整个堆的20%或以上
新生代的垃圾回收
触发:当新生代大小达到堆内存的60%,或者达到 -xx:G1MaxNewSizePercent设置的值,就会触发新生代GC,采用复制算法,进入stop-the-world,通过-XX:MaxPauseMills 来控制最大停顿时间,默认200ms
大对象region
存放
专门的存大对象的region
垃圾回收
在新生代、老年代回收时顺带回收大对象region
新生代+老年代的混合垃圾回收
触发:通过参数"-XX:InitiatingHeapOccupancyPercent" 控制,默认是45%,当老年代占据堆内存的45%的时候们就会尝试触发新生代+老年代的混合垃圾回收
混合回收
采用复制算法,不存在碎片问题
计算每个region中的对象存活数量,存活数量占比,执行回收的性能和效率,因为要控制每次垃圾回收的停顿时间
由于每次回收有时间限制,所以最后一个阶段的混合回收并不是只混合回收一次,混合回收一次,系统停滞,接着系统恢复运行,过一会再混合回收,系统停滞,默认是8次,通过"-XX:G1MiexedGcCountTarget"控制
参数:-XX:G1HeapWastePercent 默认5%, 回收过程有不断空出来新的region,一旦空闲出来的Region数量达到了堆内存的5%,就立即停滞本次混合回收
参数:-XX:G1MixedGCliveThresholdPercent 默认85%,region中存活对象低于85%才可以回收这个region
回收过程
初始标记
需要STW,进行GCRoots追踪
为每个region创建两个位图
NextBitmap
本次进行标记的位图
PrevBitmap
上次标记问题的结果
并发标记
从GCRoots出发,并发的追踪大多数存活对象,划重点:大多数!扫描完成以后还要重新处理SATB记录下的在并发阶段引用发生变动的对象
同时还有两个TAMS指针,把Region中的一部分空间划分出来用于回收过程中新对象的分配,新分配的对象默认存活,不在此次回收范围内,如果回收速度赶不上分配速度就会导致Full GC,这一点类似于CMS中的Concurrent Model Failure
为什么是追踪大多数?
每个线程独有一个STAB队列,当在并发标记阶段,有对象引用发生变化,就会在STAB中记录变化之前的引用关系,且入队到STAB队列中,当STAB队列满队后就会把STAB队列放到一个全局STAB队列集合中,GC线程会定期去扫描这个全局STAB队列集合,如果发现其中有队列就会对队列中对象进行扫描和标记
最终标记
需要STW,用于处理并发阶段结束后仍遗留下的一些少量的STAB
筛选回收
必须STW,负责更新region的统计数据,对各个Region的回收价值和成本进行排序,根据用户设定的期望停顿时间,自由选择多个region进行回收。采用复制算法
Full Gc
在混合回阶段 新生代老年代都是进行,复制算法,如果没有足够的空间来存放存活对象就会触发Full Gc,系统进入stop-the-world 采用单线程的标记-清理和压缩整理来空闲出一批region
G1相对于CMS只有在大堆的时候才有优势,因为CMS比较伤的是remark阶段,如果堆太大要扫描的东西太多,而G1在大堆时可以选择部分收集,达到停顿时间的要求
G1的一些问题
G1怎么处理跨Region引用
同CMS一样,G1也是用的记忆集(Remeber Set)来处理跨代引用
与CMS不同的是,G1每个Region都有一个Remeber Set
用来记录别的region指向自己的指针,并记录页表范围
Remeber Set的记忆精度是卡精度,也就是说是利用卡表来记录
数据结构本质上是一个Hash表
key是Region的起始地址
value是一个集合,里面的元素是卡表的索引
G1在并发标记阶段怎么做到用户线程和收集线程互不干扰
意思就是如何保证在并发标记阶段,用户线程改变对象引用关系时,必须保证其不能打破原本的对象图结构,保证回收的正确性
STAB(原始快照),在并发标记阶段,如果有对象的引用发生变化,那么就将它记录下来
G1怎么建立停顿预测模型
因为G1可以根据用户设定的停顿时间来进行垃圾回收,所以垃圾回收前就要能预测回收一个Region需要花费的时间
衰减均值
记录Region的回收时间以及记忆集里面脏卡数量来计算平均值
如何发现对象的引用发生变化
写屏障
当写屏障感知到对象引用发生变化后,就会放在remeber set log,当log满了以后也会在全局转移记忆集合中,然后会有线程去处理,并分配一个新的转移记忆集合日志
ZGC
与G1一样基于Region,几乎所有阶段都是并发,整堆扫描,部分收集
ZGC不分代,也就是没有老年代和新生代
目标
低延迟
region大小
2M
32M
X M
过程
1、初始标记
需要STW,标记GCRoots可达对象
2、并发标记
根据初始标记开始并发遍历对象图,还会统计每个region的存活对象数量
3、再标记阶段
需要STW,并发标记阶段应用线程还是在运行,所以会修改对象的引用导致漏标的情况
非强引用并发标记和并发处理
4、重置转移集
在标记阶段已经完成了重定位,所以现在需要清空该集合为后续垃圾回收做准备
5、回收无效分区
回收物理内存已经被释放的无效虚拟内存页面,内存紧张的时候,回释放物理内存,如果同时释放虚拟空间的话也不能释放分区,因为分区要在新一轮标记完成之后才能释放
6、选择回收的分区
7、初始化待转移集合的转移表
初始化待回收的分区的forwardingTable
内存泄露
有一块内存始终释放不掉
ThreadLocal使用不当就会产生内存泄露(当使用线程池的时候,由于Thread是重复利用的,所以会这样)
技能
jstat
用法:jstat -gc PID
PID为java 进程通过 jps 查看
关注点
新生代、老年代大小
新生代对象增长速率
新生代young gc触发频率和每次耗时
young gc有多少对象存活和进入老年代
full gc 频率和耗时
jmap
用法:jmap -heap pid
运行时各数据区域的情况
用法:jmqp -histo pid
各种对象的内存使用大小降序排列
用法:jmap -dump:live format=b, file=dump/hprof pid
生成一个堆内存所有对象的快照
jhat
分析 dump文件,默认http端口7000
是不是所有的对象和数组都分配在堆内存上
不一定,随着JIT技术的发展,如果JIT编译器在编译期间通过逃逸分析发现方法中的对象没有逃逸出方法,就会有可能把堆内存分配改为栈内存分配来降低内存分配压力
逃逸分析
一种降低同步负载和减小内存分配压力的全局数据流算法
定义:对象在方法内定义,如果会被方法外部引用到就称为方法逃逸
打开方式
打开
-XX:+DoEscapeAnalysis
jdk1.7以后默认开启
关闭
-XX:-DoEscapeAnalysis
用途
同步省略(锁消除)
内存分配优化
对象没有发生逃逸可能会被分配到栈上
依赖于标量替换
分离对象或标量替换。
有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
标量
无法再分解成更小的数据
标量替换
逃逸分析发现一个对象不会被外界访问,经过JIT优化,就会把这个对象拆解成其中包含的若干个成员来代替
名词概念
吞吐量
CPU用于运行用户代码时间与CPU总耗时的比值
STW
stop the world
用户工作线程被挂起,只有垃圾回收线程工作
安全点
代码执行过程中的一些特殊位置
0 条评论
下一页