JVM知识整理
2021-03-07 19:51:17 985 举报
AI智能生成
Java虚拟机(JVM)是Java技术的核心和基础,它是Java能实现“一次编写,到处执行”的关键。JVM是一个虚拟的计算机,它有自己的指令集和运行时内存区域。Java程序在运行之前,会被编译成字节码文件,然后由JVM加载并执行。JVM负责管理内存,包括堆内存的分配和回收,垃圾收集等。此外,JVM还提供了一些机制,如类加载器,保证Java程序的动态性和安全性。JVM的知识包括JVM的架构、内存模型、类加载机制、垃圾收集算法、调优策略等。掌握JVM知识,可以帮助我们更好地理解Java程序的运行机制,提高Java程序的性能和稳定性。
作者其他创作
大纲/内容
调优工具
查看一个java进程的内存和gc情况
jstat -gc PID
S0C
From Survivor区的大小
S1C
To Survivor区的大小
S0U
From Survivor区当前使用的内存大小
S1U
To Survivor区当前使用的内存大小
EC
Eden区的大小
EU
Eden区当前使用的内存大小
OC
老年代的大小
OU
老年大当前使用的内存大小
MC
方法区(永久代,元数据区)的大小
MU
方法区(永久代,元数据区)当前使用的内存大小
YGC
系统运行迄今为止Young GC的次数
YGCT
Young GC的耗时
FGC
系统运行迄今为止的Full GC次数
FGCT
这是Full GC的耗时
GCT
所有GC的总耗时
CCSC
当前压缩类空间的容量 (字节)
CCSU
当前压缩类空间目前已使用空间 (字节)
其他jstat命令
jstat -gccapacity PID
堆内存分析
jstat -gcnew PID
年轻代GC分析,这里的TT和MTT可以看到在年轻代存活的对象的年龄和最大年龄
jstat -gcnewcapacity PID
年轻代内存分析
jstat -gcold PID
老年代GC分析
jstat -gcoldcapacity PID
老年代内存分析
jstat -gcmetacapacity PID
元数据内存分析
如何使用jstat工具
分析线上JVM进程时想知道的信息
新生代对象增长的速率
Young GC触发频率
Young GC的耗时
每次Young GC后有多少对象存活
每次Young GC有多少对象进入老年代
老年代对象增长速率
Full GC的触发频率
Full GC的耗时
jstat -gc PID 1000 10
根据自己的系统灵活多变的使用,如果系统负载很低,就把时间调高去观察对象增长的速率。另外一般系统又高峰和日常两种状态,可以分时段去查看对象增长速率。
jmap/jhat
jmap -heap PID
这个命令会打印出来内存的相关一些参数的设置,然后就是当前堆内存里面一些基本各个区域的情况,比如Eden区总容量,已经使用的容量,剩余的空间容量,两个Survivor区的总容量,已经使用的容量和剩余的空间容量,老年代的总容量,已经使用的容量和剩余容量。
jmap -histo PID
按照各种对象占用内存空间的大小降序排序,把占用内存最多的对象放在最上面。
jmap -dump:live,format=b,file=dump.hprof PID
jmap命令生成一个堆内存快照放到当前目录的一个文件dump.hprof里面去,这是二进制的格式。
jhat dump.hprof -port 7000
使用此命令即可启动jhat服务器,指定自己想要的http端口号,接着就可以在浏览器上访问当前这台机器的7000端口号,就可以通过图形化的方式去分析堆内存里的对象分布情况了。
MAT
用来分析OOM时的内存快照
4C8G JVM参数模板
意义
这个非常有用,因为并不是每个人都比较精通JVM核心原理和性能优化,所以如果有一套为团队或者公司定制一套基本的JVM参数模板,基本可以保证JVM性能不会太差,避免许多初中级工程师直接使用默认的JVM参数,并不能满足生产需要。
模板内容
-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:SurvivorRatio=8 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=10 -XX:PretenureSizeThreshold=2M -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+DisableExplicitGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:./gclog/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/app/oom
内存溢出的处理
元空间溢出
Metaspace区域内存空间不够,但是还要往这里塞入更多类的时候,就会发生内存溢出的问题。
原因
系统上线时,Metaspace直接使用的默认的参数,而默认参数往往比较小,很容易不够用。
代码中使用cglib之类的技术生成类,没有控制好,导致生成的类过多,也容易塞满Metaspace,导致内存溢出。
解决方法
合理分配Metaspace区域
避免无限生成动态类。
栈内存溢出
原因
如果不停的让线程调用方法,不停的往栈里面放入栈帧,最终会有一个时刻,大量栈帧会消耗完这个栈内存,最终就会出现栈内存溢出的情况。
解决方法
避免不合理的调用递归方法等
堆内存溢出
原因
系统承载高并发请求,因为请求量过大,导致大量对象是存活的,所以要继续放入新的对象实在不行了,此时就会引发OOM系统崩溃。
系统有内存泄漏的问题,就是莫名其妙弄了很多对象,结果对象都是存活的,没有及时取消对他们的引用,导致触发GC还是无法回收,此时只能引发内存溢出,因为内存实在放不下更多对象了。
直接内存溢出 Direct Buffer Memory
原因
内存设置不合理,导致DirectByteBuffer对象一直慢慢进入老年代,导致堆外内存一直释放不掉。
设置了-XX:+DisableExplicitGC导致Java NIO没法主动提醒去回收掉一些垃圾DirectByteBuffer对象,同样导致堆外内存无法释放掉。
类加载
类加载,使用的过程
加载
加载时机
代码中包含main()方法的主类再JVM进程启动后会被加载到内存,开始执行其中代码时,若其中使用了别的类,也会把其对应的“.class”文件加载到内存里。
验证
根据java虚拟机规范,校验加载的".class"文件是否符合指定的规范
准备
给类分配一定内存空间,并赋予默认初始值
解析
把符号引用替换成直接引用
初始化
触发初始化时机
当新建某个类的对象时,如通过new或者反射,克隆,反序列化等
当虚拟机启动某个被表明为启动类的类,即包含main方法的那个类,所以system.out.println(Test.class)
初始化一个子类的时候,会先初始化其父类
当调用某个类的静态方法时
当使用某个类或者接口的静态字段时
调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中类的方法时
使用
卸载
类加载器和双亲委派机制
类加载器的分类
启动类加载器 Bootstrap ClassLoader
加载jdk安装目录下lib目录中的核心类库
扩展类加载器 Extension ClassLoader
加载jdk安装目录下lib\ext目录
应用程序加载器 Application ClassLoader
加载ClassPath环境变量中指定路径中的类,可以理解为加载你写的代码
自定义类加载器
双亲委派机制
假设应用程序类需要加载一个类,首先会委派给父类加载器加载,最总传导到顶层的类加载器去加载。但如果父类加载器在自己负责的加载范围内没找到这个类,就会下推权力给自己的子类加载器。
图示
Tomcat这种web容器中类加载器设计实现思路。
图示
如何防止“.class”被反编译窃取公司源码
编译时,可采用小工具对字节码进行加密,或者做混淆处理
购买专业的第三方公司做商业级字节码文件加密的产品。
JVM内存区
内存区域划分
意义
JVM在运行我们写好的代码时,需要使用多块内存空间,不同的内存空间用来存放不同的数据,然后配合我们写代码的流程,才能让我的系统运行起来。
图示
详情
线程共享数据区域
方法区
jdk1.8后也称元数据空间 Metaspace
存放我们自己写的各种类的信息
堆内存
存放我们在代码中创建的各种对象的
堆内存的划分
新生代
区域划分
Eden区
Survivor(from)区 设置survivor是为了减少送到老年代的对象
Survivor(to)区 设置两个Survivor区是为了解决碎片问题
划分比例
eden:survovir:survivor=8:1:1
老年代
线程私有数据区域
程序计数器
我们写好的java代码会被翻译成字节码,对应各种字节码指令,字节码指令一条条被执行,才能实现我们写好的代码的执行效果。
当JVM加载类信息到内存之后,会使用自己的字节码执行引擎,去执行编译后的代码指令。
执行时,JVM需要一个特殊的内存区域,程序计数器(用来记录当前执行字节码指令位置的,每一个线程都会有自己的程序计数器),记录目前执行到哪一条字节码指令。
当JVM加载类信息到内存之后,会使用自己的字节码执行引擎,去执行编译后的代码指令。
执行时,JVM需要一个特殊的内存区域,程序计数器(用来记录当前执行字节码指令位置的,每一个线程都会有自己的程序计数器),记录目前执行到哪一条字节码指令。
虚拟机栈
每个线程都有自己的java虚拟机栈,用来存放自己执行的那些方法的局部变量,执行了一个方法,就创建一个栈帧。
栈帧里就有这个方法的局部变量表,操作数栈,动态链接,方法出口等东西。
栈帧里就有这个方法的局部变量表,操作数栈,动态链接,方法出口等东西。
调用执行任何方法时,都会给方法创建栈帧后入栈,在栈帧里存放了这个方法对应的局部变量之类的数据,包括这个方法执行的其他相关信息,方法执行完毕后出栈。
本地方法栈
调用native方法的时候,就会有线程对应的本地方法栈。这个虚拟机栈类似,是存放各种native方法的局部变量表之类的信息。
拓展
还有一个区域,不属于JVM,通过NIO中的allocateDirect这种API,可以在java堆外分配内存空间,然后,通过java虚拟机里面的DirectByteBuffer来引用和操作堆外的空间。
内存的消耗
我们在java堆内存里创建的对象,都是占用内存资源的,而且内存资源有限。
举例
一个方法执行完毕,就会从虚拟机中出栈,那么原来那个栈帧里面的局部变量,比如一个实例对象,就没有任何一个变量指向它了。这种空占着资源着的内存资源,如何处理?答案是通过 JVM的垃圾回收机制。
我们创建的对象,在java堆内存中占用多少内存空间?
对象本身的一些信息
对象的实例变量作为数据占用的空间
内存相关参数
-Xms
Java堆内存的大小
-Xmx
Java堆内存最大大小
-Xmn
Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小了
-XX:PermSize
永久代最大大小
JDK1.8以后,这个参数被替换成了-XX:MetaspaceSize
-XX:MaxPermSize
永久代最大大小
JDK1.8以后,这个参数被替换成了-XX:MaxMetaspaceSize
-Xss
每个线程的栈内存大小
-XX:SurvivorRatio=8
定义了新生代中Eden区域和Survivor区域(From幸存区或To幸存区)的比例,默认为8,也就是说Eden占新生代的8/10,From幸存区和To幸存区各占新生代的1/10
-XX:ThreadStackSize
设置JVM栈内存
-XX:+HeapDumpOnOutOfMemoryError
在OOM的时候,自动dump内存快照出来
-XX:HeapDumpPath
在OOM时候,dump内存快照保存位置
垃圾回收机制
对象分配机制
大部分正常对象都优先在新生代分配内存
新生代如果对象满了,会触发Minor GC回收掉没人引用的垃圾对象
默认设置下,当对象年龄达到15岁的时候,也就是躲过15次GC的时候,会转移到老年代里去。
JVM参数:-XX:MaxTenuringThreshold,默认值15
如果老年代也满了,那么就会触发垃圾回收,把老年代里没人引用的垃圾对象清理掉
新生代垃圾回收(Minor GC)之后,如果存活对象太多,超过survivor区的大小,就会导致大量对象直接进入老年代
特别大的超大对象直接不经过新生代就进入老年代
参数:-XX:PretenureSizeThreshold可以把他的数值设置为字节数。
如果新生代里创建了一个大于这个大小的对象,就把这个大对象直接放到老年代。之所以这么做时为了避免新生代出现的那种大对象,多次躲过GC,导致其在两个Survivor区域里来回复制多次后才能进入老年代。
动态对象年龄判断机制
例如说当前对象的Survivor区域里,一批对象(年龄可以不同)的总大小大于了这块Survivor区域的内存大小的50%,那么此时大于等于这批对象的年龄的对象,就可以直接进入老年代。
老年代空间担保机制
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的,如果担保失败则会进行一次Full GC;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
类被回收的条件
该类所有的实例都已被回收
该类的ClassLoader已经被回收。
对该类的Class对象没有任何引用。
垃圾回收两部曲
确定垃圾
可达性分析算法
GC Roots
局部变量(栈中)
静态变量
不同的引用类型
强引用
定义:一个变量引用一个对象
只要是强引用类型,那么垃圾回收的时候绝对不会去回收这个对象。
软引用
定义:把实例对象用一个“SoftReference”软引用类型的对象包裹起来了,赋值给一个变量,这时这个变量对这个对象就是一个软引用。
正常情况下垃圾回收是不会回收软引用对象的,但是如果你进行垃圾回收之后,发现内存空间还是不够存放新的对象,内存都快溢出了,此时就会把这些软引用对象给回收掉,哪怕他被变量引用了,但是因为他是软引用,所以还是要回收。
弱引用
定义:WeakReference 弱应用类型
弱应用就和没引用是类似的,如果发生垃圾回收,就把这个对象回收掉
虚引用
回收垃圾
垃圾回收的痛点
Stop the world
老年代垃圾回收时机
在Minor GC之前,一通检查发现可能Minor GC之后要进入老年代的对象太多了,老年代放不下,此时要提前触发Full GC,然后再进行Minor GC
或者在Minor GC之后,发现剩余对象太多,放入老年代都放不下了。
垃圾回收算法
复制算法
把新生代内存划分为两块内存区域,然后只使用其中一块内存,待那块内存快满的时候,就把里面的存活对象一次性转移到另一块内存区域,保证没有内存碎片。接着一次性回收原来那块内存区域内的垃圾对象,再次空出来一块内存区域,两块内存区域就这么重复着循环使用。
优点
减少内存空间碎片造成的空间浪费
缺点分析
从始至终,只有一半的内存可用,这样的算法显然对内存的使用效率太低了。
复制算法的优化
针对上述缺点,真正复制算法把新生代内存区分为三块,1个Eden区,2个Survivor区,其中Eden区占80%内存,每一块Survivor占用10%的内存。对象内存分配的时候,只在Eden区和其中一块survivor区上分配,回收时,将存活对象复制到另一块survivor区。这样做最大的好处就是只有10%的内存空间是被闲置的,90%的内存都被使用上了。无论垃圾回收的性能还是内存碎片的控制,内存的使用效率,都非常的好。
标记整理算法
步骤
标记出来老年代当前存活对象
让这些存活对象在内存里进行移动,把存活对象尽量都挪动到一边去,把存活对象紧凑的靠在一起,避免垃圾回收过后出现太多碎片
一次性把垃圾回收掉
缺点
这个老年代的垃圾回收算法的速度至少比新生代的垃圾回收算法速度慢10倍,如果系统频发的出现老年代Full GC垃圾回收,会导致系统性能被严重影响,出现频发卡顿的情况。
应对思路
所谓JVM优化,就是尽可能让对象都在新生代里分配和回收,尽量别让太多对象进入老年代,避免频繁对老年代对象进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁进行垃圾回收。
标记-清除算法
将所有待回收的对象标记出来,然后一起清理
缺点
1.标记和清理的效率都不高
2.可能产生大量的内存碎片
分代收集算法
根据不同的年代选取不同的垃圾回收算法
垃圾回收器
Serial/Serial Old
Serial
用一个线程进行垃圾回收,此时会暂停系统工作线程
用的少
适用于单线程回收
Serial Old
回收老年代
工作原理
单线程运行,垃圾回收的时候会停止我们自己写的系统的其他工作线程,让我们的系统直接卡死不动,然后让他们垃圾回收,这个现在一般不用。
ParNew/CMS垃圾回收器
ParNew
针对服务器一般都是多核进行了优化,支持多线程垃圾回收,可以大幅度提升回收性能,缩短回收时间。
适用于多核多线程回收
相关JVM参数说明
-XX:+UseParNewGC
可指定该垃圾收机器对新生代进行回收,一旦我们指定使用ParNew垃圾收集器后,它默认给自己设置的回收线程数和CPU核数一致。
-XX:ParallelGCThreads
可设置线程数量
CMS
触发时机
当老年代内存占用达到一定比例了,就自动执行GC。
为了解决Stop the world时,系统卡死时间过长,响应无法处理的问题,CMS垃圾收集器采取的是垃圾回收线程和系统工作线程尽量同时执行的模式来处理。
垃圾回收过程
初始标记
这个阶段会让系统工作线程全部停止,进入stop the world,然后标记出来所有GC Roots直接引用的对象。
速度很快
并发标记
这个阶段会让系统线程可以随意创建各种新对象,继续运行。也可能让部分存活对象失去引用,变成垃圾对象,在这个过程中,垃圾回收线程,会尽可能地对已有对象进行GC Roots追踪,找出被GC Roots 间接引用 的对象。
是最耗时的
重新标记
为了解决第二阶段,很多存活对象和垃圾对象没被标记出来的问题,进入此阶段时,要继续让系统停下来,再次进入stop the world 阶段,重新标记第二阶段里新创建的对象,还有一些可能失去引用而变成垃圾的对象。
因为只是对少数在第二阶段中被系统程序运行变动过的对象进行标记,所以运行速度很快。
并发清理
让系统程序随意运行,然后清理掉之前标记的为垃圾的对象即可。
这个阶段其实很耗时,因为需要对对象进行清理,但是与系统程序并发运行,不影响系统程序的执行。
缺点
第二阶段和第四阶段非常耗费CPU资源,CMS默认启动的垃圾回收线程数量是(CPU核数 + 3)/ 4,当CPU核数比较少时,这个问题尤为严重。
Concurrent Mode Failure的问题
在第四阶段,CMS只不过回收之前标记好的垃圾对象,但是这个阶段系统一直在运行,可能会随着系统运行让一些对象进入老年代,同时还变成垃圾对象。这种垃圾对象是浮动垃圾。但是CMS只能回收之前标记出来的垃圾对象,不会回收他们,需要等到下一次GC的时候,才会回收他们。所以为了保证在CMS垃圾回收期间,还有一定的内存空间可以让一些对象进入老年代,一般会预留一些空间。
如果CMS垃圾回收期间,系统程序放入老年代的对象,大于了可用内存空间,就会发生Concurrent Mode Failure,内存不够了。
这时就会自动用Serial Old 垃圾回收器替代CMS,就是直接强行把系统程序Stop the world,重新进行长时间的GC Root追踪,标记出来全部的垃圾对象,不允许新的对象产生。然后一次性把垃圾对象都回收掉,完事了,再恢复系统线程。
相关参数设置
-XX:+UseCMSCompactAtFullCollection
老年代CMS采用“标记-清理”算法,每次标记出来对象,再一次性回收,这样会导致大量的内存碎片产生。如果内存碎片太多,会导致后续对象进入老年代找不到可用的内存空间,然后触发Full GC, 所以CMS不是完全就仅仅用“标记-清理”算法的,因为太多的内存碎片实际上会导致更加频繁的Full GC。这个参数的意思是在Full GC之后要再次进行一次stop the world,停止工作线程,然后进行碎片整理,把存活对象挪到一起,空出来大片连续空间,避免内存碎片。
-XX:CMSFullGCsBeforeCompation
执行多少次Full GC之后进行一次内存碎片整理的工作,默认是0
-XX:+CMSScavengeBeforeRemark
这个参数会在CMS重新标记阶段之前,先尽量执行一次Young GC,提前回收掉一些没人引用的对象,这样在CMS的重新标记阶段就可以少扫描一些对象,此时就可以提升这个阶段的性能,减少耗时。
-XX:+UseConcMarkSweepGC
开启CMS垃圾回收器
-XX:+CMSParallelRemarkEnabled
在CMS垃圾回收器的重新标记阶段开启多线程并发执行,减少stop the world
-XX:+CMSParallelInitialMarkEnabled
在CMS垃圾回收器的初始标记阶段开启多线程并发执行,减少stop the world。
-XX:CMSInitiatingOccupancyFaction
这个参数可以用来设置老年代占用多少比例的时候触发CMS垃圾回收,JDK1.6里面默认值是92%。预留8%的空间给并发回收期间,系统程序把一些新对象放入老年代中。
如果老年代可用内存大于历次新生代GC后进入老年代的对象的平均大小,但是老年代已经使用的内存空间超过了这个参数的比例,也会触发Full GC
-XX:+UseCMSInitiatingOccupancyOnly
和前面的参数配套使用,如果不设置后者,jvm第一次会采用设置的值,比如92%,如果设置后者,则jvm每次都在92%的时候进行GC。
G1
特点
把Java堆内存拆分为多个大小相等的Region
也有新生代和老年代的概念,不过是逻辑上的概念,也就是说,新生代可能包含了某些region,老年代可能包含某些region,且新生代和老年代各自内存区域在不停变动,由G1控制。
设置一个垃圾回收的预期停顿时间
追踪每个region中可以回收的对象的大小和预估时间,最后在垃圾回收的时候,尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,尽可能在有限地时间内尽量回收尽可能多的垃圾对象。
触发垃圾回收的时候,可根据设定的预期系统停顿(stop the world)时间,来选择最少回收时间和最多回收对象的region进行垃圾回收。保证GC对系统停顿的影响控制在可控范围内,同时还能尽可能回收最多的对象。
触发垃圾回收的机制是类似的
相关参数配置
-XX:+UseG1GC
指定G1垃圾回收器,此时会自动用堆大小除以2048,因为JVM最多有2048个region,region大小必须是2的倍数。
-XX:G1HeapRegionSize
手动指定region大小
-XX:G1NewSizePercent
设置新生代初始占比,默认5%,这个维持默认值即可。
-XX:G1MaxNewSizePercent
在系统运行中,JVM会不停给新生代增加更多Region,但最多新生代占比不会超过60%,可通过此参数设置这个百分比。
-XX:MaxGCPauseMills
最多可以让系统停顿多长时间。
-XX:MaxTenuringThreshold
新生代进入老年代的年龄。
-XX:SurvivorRatio
eden区在整个新生代中的占比,默认值为8
-XX:InitiatingHeapOccupancyPercent
如果老年代占据了堆内存45%的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段。
-XX:G1MixedGCCountTarget
在一次混合回收的过程中,最后一个阶段执行几次混合回收,默认是8次
-XX:G1HeapWastePercent
在混合回收的时候,对Region的回收都是基于复制算法进行的,都是要把回收的里的存活对象放入其他Region,然后这个里面的垃圾对象全部清理掉。这个的话,回收过程就会不断空出来Region,一旦空闲出来的数量达到了堆内存的5%。此时就会立即停止混合回收,意味着本次混合回收就结束了。
-XX:G1MixedGCLiveThresholdPercent
默认值是85%,意思是确定要回收region的时候,必须存活对象低于85%的region才可以进行回收。否则复制成本会很高。
对象进入老年代的条件
1.对象在新生代躲过了多次垃圾回收,达到一定年龄了(如上,可设置),进入老年代。
2.动态年龄判断规则,一旦发现某次新生代GC过后,一批存活对象超过了Survivor的50%,大于这批对象年龄的对象就会进入老年代。
大对象region
在G1中,大对象的判定规则就是一个大对象超过了一个region大小的50%,如果一个对象太大,可能会横跨多个来存放。
新生代,老年代在回收的时候,会顺带带着大对象一起回收。
垃圾回收过程
初始标记
进入stop the world, 仅仅标记一下GC Roots直接引用的对象。
速度很快
并发标记
允许系统程序的运行,同时进行GC Roots追踪,从GC Roots追踪所有存活对象。还会堆对象做出的一些修改记录起来,比如哪个对象被新建了,哪个对象失去了引用。
很耗时
最终标记
会进入stop the world,系统程序是禁止运行的,但是会根据并发标记阶段记录的哪些对象修改,最终标记一下有哪些存活对象,有哪些垃圾对象。
混合回收
会计算老年代中每个中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率。接着会进入stop the world,全力以赴尽快进行来及回收,选择部分进行回收,并让垃圾回收的停顿时间控制在我们指定的范围之内。
回收失败应对策略
如果在进行Mixed回收的时候,无论在年轻代还是老年代都是基于复制算法进行回收,都要把各个region的存活对象拷贝到别的region里面去,此时万一出现拷贝的过程中发现没有空闲的region可以承载自己的存活对象了,就会触发一次失败。一旦失败,就会立马切换为停止系统程序,采用单线程进行标记,清理,和压缩整理,空闲出来一批region,这个过程极慢极慢。
优化思路总结
由于新生代存活对象少,速度较快,老年代存活对象多,需要进行GC Roots追踪的对象量大,且使用的算法需要进入stop the world,非常耗时。所以JVM优化的思路主要应该集中于在使对象在新生代就得到回收,避免他们快速进入老年代,引发Full GC。这个可以从两个方面下手。
一,避免新生代通过复制算法回收后,survivor区放不下直接进入老年代的问题,这个可以通过合理配置eden区和survivor区的比例,尽量让survivor区有足够空间去容纳新生代回收后的对象。
二,规避young gc后,某一批次新生代存活对象在survivor区占比达到50%的情况,由于大于这批对象年龄的对象会直接进入老年代。所以要尽量避免这种情况的发生。
大对象如果在Young GC时候,老在新生代内存空间中挪来挪去也很消耗cpu资源,所以可以让这些长寿的大对象超过一定阈值时,直接进入老年代。
老年代如果一直有大量对象无法回收掉,年轻升入老年代的对象并不多,频繁触发Full GC时,那就用dump出来内存快照,然后用MAT工具分析。
在上述优化之后,还是慢慢会有对象进入老年代里,(毕竟系统的负载也很高,彻底让对象不进入老年代也很难做到)。所以调优过后每小时还是会有一次Full GC, 这就会造成老年代里面有很多内存碎片,连续可用的内存变少,最终必然会导致Full GC更加频繁。因此可以设置上面参数整理下内存碎片。
Metaspace永久代也可能因为加载太多的类触发Full GC,所以要避免一次加载太多类到内存中,或者合理设置-Xss参数值。
错误使用System.gc()也可能频繁触发Full GC,可以在jvm参数中加入-XX:DisableExplicitGC,禁止显示的执行gc,不允许通过代码触发GC.
对于一般机器来讲,parnew+cms的gc回收方案是很受用的,但是对于大内存机器来讲,由于使用此方案每次gc会产生大量的对象,系统停顿的时间太长,导致系统应用线程无法正常处理业务,所以最好使用G1回收器,可以有效缩短停顿时间,保证系统可用性。
老年代自身有一个设置其空间占用达到一定比例可以触发Full GC的阈值,可以适当调大一些。
其他参数
日志分析相关参数
-XX:+PrintGCDetails
打印详细的Gc日志
-XX:+PrintGCTimeStamps
打印每次的gc的时间
-Xloggc:gc.log
将gc日志写入一个磁盘文件
-XX:+PrintHeapAtGC
分别打印GC前后的堆内存的情况
类加载/卸载相关
-XX:TraceClassLoading
追踪类加载的情况
-XX:TraceClassUnloading
追踪类卸载的情况
其他
SoftRefLRUPolicyMSPerMB
clock - timestamp <= freespace * SoftRefLRUPolicyMSPerMB
clock记录是上一次GC的时间戳,timestamp则是最近一次读取soft-reference引用对象(即最近调用get())的时间戳。他们的差【clock - timestamp】表示了soft-reference有多久没用了,越大表示越久没用。如果他们的差为负数,表示刚刚用过。而【freespace * SoftRefLRUPolicyMSPerMB】表示能够VM的忍耐度,VM能够忍耐软引用对象多久没有被回收,而VM的忍耐度从公式可以知道是由VM计算得出的空闲空间大小和用户指定的忍耐度SoftRefLRUPolicyMSPerMB来决定的。
也就是说,如果软引用上次被get()的时间离最近一次GC的时间不会太久远的话就可以不被当前GC回收。
也就是说,如果软引用上次被get()的时间离最近一次GC的时间不会太久远的话就可以不被当前GC回收。
拓展
finalize()方法
通过重写finalize()方法,可以使得没被GC Roots引用的对象不被回收
0 条评论
下一页