我看过一本关于JVM的书,根据自己所看的所得,写下这篇文章。
-----------------------------------------------个人见解,如有不足,见谅。
1. jvm为什么分代回收
因为不同区域代的对象存活的时间不同,有的老年代获得久,发生GC的次数就比较少,像年轻代,对象生命周期短,发生垃圾回收的次数久多。每进行一次垃圾回收,都会都区域内的对象整体进行遍历,遍历很耗时。进行分代回收,可减少对象遍历的次数,个数。根据对象存活时间选择不同的回收算法,收集器,提高效率。
2.什么时候发生垃圾回收
先说jvm的区域。
1.年轻代,ScanvengeGc发生频繁,当有对象申请空间时,Eden区满了,会进行一次垃圾回收,将存活的对象复制到survivor区内。发生一次。这个垃圾回收频繁,需要使用效率高的回收算法
回收的对象是垃圾对象,什么是垃圾对象:没有引用指向的对象。
静态变量引用的对象,方法执行过程中引用的变量不能回收。
怎么回收:就是回收算法
2.FullGc是对整个堆,进行的。发生在老年代写满,永久代写满。
显示调用,上一次GC之后Heap的各域分配策略动态变化,发生次数少,比较耗时。
我们把我们的项目打成包,部署到服务器(tomacate),当服务器启动的时候,jvm会启动进程,通过类加载器把我们的类加载到jvm内存,存到元数据区(存放类信息)
服务器启动的时候,会启动spring容器,实例对象。反射技术。
一次请求过程的jvm情况?
当服务器有请求过来,会有线程执行请求,线程执行请求在jvm内存有独享的工作内存空间,栈内存。
然后执行对象中的一些方法,执行方法过程中,会有局部变量,引用对象,局部变量放在线程的栈内存中,会有执行方法的栈帧,局部 变量放到栈帧的局部变量表里面,也可能会创建对象,栈帧里面的引用,会引用对象。方法执行完,栈帧弹出去。
栈,先入后出,先入栈的压到最尾,出去从栈头出。
jvm运行起来,对象怎么分配的?
一个程序,方法main是入口 ,一个main线程执行程序代码。创建对象
而我们的应用:像spring,会初始化一些bean,这些bean的实例化,就是有工作线程进行创建bean实例,执行代码。业务逻辑。
3.内存划分
jvm内存8以后,元数据区就是以前的永久代,存放类信息。常量池放置到了堆内存。
年清代和老年代通称堆。默认eden:s1:s2=8:1:1
每个方法执行,都会创建相应的栈帧执行对应的方法,每个栈帧有局部变量表,存储变量
4.老年代如何进行回收垃圾(算法和垃圾收集器)
4.1 由于老年代的对象都是存活比较长的,大部分都是需要一直存活的(spring的bean),少量回收的。所以不适合使用复制算法,占用空间。采用标记清除(内存碎片,大对象碎片无法存放,浪费空间)不行。标记-整理合适。
算法:标记-整理
4.2 垃圾收集器
使用并发收集:减少应用停顿,发多核cpu.但是由于应用没有停止,会导致垃圾回收不彻底。还要运行垃圾回收线程,需要设定运行线程的空间,不然也会造成应用停止。
jdk8之前:parellNew+cms收集器, jdk9以后:g1收集器
parellNew:年轻代
cms:分阶段,
5.垃圾回收算法
6.垃圾收集器
串行收集器:单线程,应用线程会停止。无法发挥多核cpu的优势。安全,利用这个开启 -XX:+UseSerialGC
并行收集器:多线程,年轻代使用,减少收集时间。应用停止
并发收集器:应用不停止,减少应用停顿时间。利用处理器减少应用停顿的时间。
由于是在应用线程不停止的情况下,会在收集完后,有剩余的垃圾没有回收,收集中,应用产生的。需要设置预留空间20%,供这些使用
由于需要运用垃圾回收的线程,需要有足够的内存空间,如果收集时,堆满了,会出现Concurrent Mode Failure,造成应用停止,只进行垃圾回收。
。通过设置-XX:CMSInitiatingOccupancyFraction=<N> 指定还有多少剩余堆时开始执行并发收集。
增量收集器:采用划分区域的方式,不是针对整个堆进行回收,针对区域回收,采用复制算法。将存活对象最少的区域进行复制回收。实现边回收边使用的效果。
总结:
串行:数据量小,小型应用
并行:大型web,响应时间没有要求,多核cpu,应用吞吐高。:后台处理、科学 计算。
并发:应用需要响应时间长。大中型应用Web服务器/应用服务器、电信交换、集成开发环境。
老年代一般是:并发收集器,要考虑内存碎片的问题
年轻代一般使用的是,并行收集器。也可与设置老年代并行收集器
7.调优
年轻代的大小:
总结:设置大点:减少对象到达老年代的数量,减少yongGc。
解析:吞吐高的,年轻代尽量设置大些,减少垃圾回收的次数,减少对象到达老年代的机会。使得老年代都是永久对象:可能到达Gbit的程度。因为对响应时间没有要求
响应时间快的,年轻代也设置大些,减少发生gc。尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)
老年代大小
吞吐量:设置大的年轻代,小的老年代。
响应时间优先:老年代使用的是,并发收集器。要合理设置堆的大小,减少内存碎片的产生(可通过设置多少次Gc进行内存碎片的整理)。堆大了,收集的时间长。
8.垃圾回收的瓶颈
由于并行收集,提高了应用的吞吐量,但是会带来应用的暂停,很大程度上,对一些实时性要求较高的应用无法满足。
虽然jvm提供了相应的并发收集器,但是并发收集器会产生内存碎片的问题。并且也不能完成保证用用不暂停。并且是针对整个堆进行回收的, 无法分代回收,在暂时时间的控制上还是和弱的。
需要解决,内存碎片和响应时间的瓶颈:增量收集器,不能精确控制停顿时间
增量收集器:通过将堆内存进行划分不同的region区域。每次使用部分区域,通过标记复制进行回收对象,并且,对region里面存活对象最少的进行复制,将存活的复制到另一个空的region。
这样不是回收的整个堆,可以实现边回收,边使用
Garbage Firest G1算法回收 JDK 7 登场
G1吸收了增量GC以及CMS的精髓,将整个jvm Heap划分为多个固定大小的region,扫描时采用Snapshot-at-the-beginning的并发marking算法(具体在后面内容详细解释)对整个heap中的region进行mark,
回收时根据region中活跃对象的bytes进行排序,首先回收活跃对象bytes小以及回收耗时短(预估出来的时间)的region,回收的方法为将此region中的活跃对象复制到另外的region中,根据指定的GC所能占用的时间来估算能回收多少region,
这点和以前版本的Full GC时得处理整个heap非常不同,这样就做到了能够尽量短时间的暂停应用,又能回收内存,由于这种策略在回收时首先回收的是垃圾对象所占空间最多的region,因此称为Garbage First。
cms收集器也是并发收集器(current mark sweep):是为了减少应用停顿时间的收集器。但是用的算法是标记清除,会产生大量的内存碎片。
G1收集器:采用的是标记整理,不会产生内存碎片,可以精确控制停顿时间。
生产环境tomcate里面jvm怎么设置参数的?怎么检查jvm运行情况?
如果是javaweb部署到timcate里面,jvm是tomcate的一个进程,你的系统是tomcate里面jvm进程运行的
需要看tomcate的一个脚本。
jvm参数包括:内存大小的分配,栈,元数据,堆(年轻(eden,surivor),老年),垃圾收集器的配置,每种垃圾回收器的特殊参数
根据每秒请求的数量,请求创建的对象,预估下大小。会在eden去创建多少对象,触发yongGc的频率,老年代的存活多少对象,压测,观察jvm运行情况。jstat工具查看jvm情况,观察:
eden对象增长情况,yongGc频率,eden存活对象多少,sur能否放下,老年代的对象增长速率,老年代fullGc的发生时间。
根据情况去调节jvm。
压测内容:
接口性能,一个系统的QPS,压测一定程度时的cpu,IO,磁盘的使用情况,jvm表现。
GC优化
知道预估,知道工具调节设置jvm参数。实际有遇到过jvm线上fullGc频繁吗,导致系统卡顿?
如果实际没有发生过,就说,是通过压测搞得。结合业务
发生oom后如何排查
首先在jvm参数的里面可以设置,发生oom异常的时候,复制一份内存使用情况的快照。可以根据快照情况去查看哪些对象(占用内存最大的对象)造成的oom异常。在找到对应的代码行,进行代码的优化。
说自己的项目业务中,什么情况下发生了报警,客户反映功能不能用。
并发导致(非代码)。
内存泄漏:代码对象无法回收。
内存溢出:超高并发时,瞬间创建大量对象,没有足够的空间存放。
jvm什么时候发生栈溢出?
1.当线程请求栈的深度大于该栈的深度时,会发生栈内存溢出。一般递归调用产生
2.栈动态扩展之后,还是不能申请足够的空间去存放,就会发生。一般是多线程调用的时候
-Xss512k 是栈的大小
Survivor的存在意义,就是减少被送到老年代的对象
每熬过一次Minor GC,年龄+1 经过15次, 若年龄超过一定限制(15),则被晋升到老年态。
JVM的配置参数
-server -Xms3800m -Xmx3800m -Xmn1500m -Xss512k
-XX:+UseConcMarkSweepGC
XX:+UseParNewGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection -XX:+CMSClassUnloadingEnabled -XX:+DisableExplicitGC -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
-Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/admin/logs -Dcloudengine_tr_min_pool_size=100
xms最大堆大小, -Xmx3800m 初始堆大小
-Xmn1500m年轻代大小 -xss 线程栈的大小
XX:+UseConcMarkSweepGC 设置老年代为并发收集器,由于老年代并发收集器会产生内存碎片,
XX:+UseParNewGC -XX:CMSFullGCsBeforeCompaction=5 并行收集器5次收集后,进行垃圾内存整理
-XX:+UseCMSCompactAtFullCollection 对老年代的压缩,影响性能,消除内存碎片
CMS的几个阶段?
1.初始标记,stop the word
2.并发标记 :与用户线程共同运行
3.预清理:
4.可被终止的预清理
5.重新标记 stop the word
6.并发清除
7.并发重置状态等待下次CMS的触发
初始标记?
标记年轻代中存活的对象引用到老年代的对象
标记老年代的GcRoot对象
GC Roots对象?
虚拟机栈(栈桢中的本地变量表)中的引用的对象
方法区中静态属性引用的对象
常量引用的对象
本地方法栈中JNI的引用的对象
并发标记
在初始标记的所有存活对象中进行标记,和用户线程一起运行.对于已经经过初始标记的对象
由于并发运行,会产生新的对象在年轻代,或者年轻代晋升到老年代的对象.这些对象有可能忘记漏标,
为了减少重复标记,会将这些对象所在的card标记一个dirty.后续处理只需要针对dirty的,不用扫描整个老年代
预清理阶段?
由于上述阶段不能完全标记出所有存活的对象.这一阶段处理上一阶段由于引用关系改变标记成card的dirty的对象.
将这些对象标记成存活对象
可终止的预处理
为了减少清理垃圾对象的负担.此阶段可以在年轻代发生Ygc,减少重新标记,减少年轻代对象引用指向老年代的对象
最大持续5s
重新标记
标记整个老年代的存活对象.重新标记的内存范围是整个堆.
标记年轻代的原因:是因为如果老年代的对象被年轻代的对象引用,将视为存活对象
年轻代的不可达对象,也会作为GcRoot对象扫描老年代. (因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作“GC ROOTS”)
重新标记前,先年轻代发生一次yGc,将存活的对象到suvovir中,在进行扫描年轻代时,就只扫描sur区就行
并发清理
清理老年代所有未标记的对象并且回收空间,
由于和用户线程并行,会产生新的垃圾对象,称未浮动垃圾