垃圾回收算法
复制算法(针对新生代的)
把新生代的内存区域分为2部分,然后只使用其中一块,待内存快满的时候,就把里面存活的对象转移到另一块内存区域,保证没有内存碎片,接着回收原来那块内存区域的垃圾对象,再次空出来一块内存区域,两块内存区域重复循环使用
缺点:内存使用率太低,每次只使用其中一半
复制算法优化
1个Eden区 80% 2个Survivor区 各10%
流程
1.刚开始对象分配都是在Eden区,如果Eden区快满了,此时就会触发垃圾回收。
2.此时就会把Eden区存活的对象一次性转移到空着的S区,接着清空Eden区,然后再次分配对象到Eden区
3.如果下次Eden又快满了,再次触发minor gc,就会把Eden区存活的对象和上一次Minor gc存活对象的S区的存活对象,转移到另一块S区
优点
1.只有10%的内存闲置了,90%的内存都使用上了
2.垃圾回收的性能和内存碎片的控制,还有内存使用的效率都非常好
标记清除
标记哪些对象是可以被垃圾回收的,直接对那块内存区域的对象进行回收
缺点:存活对象再内存区域一个东一个西,产生大量的内存碎片,内存浪费
产生的问题
可能因为内存碎片太多的缘故,虽然所有的内存碎片加起来有很多的内存,但是这些内存都是零散的,导致没有完整的一块内存区域来分配新的对象
标记整理 (针对老年代的)
1.首先标记出来老年代存活的对象,这些对象可能东一个西一个
2.把这个存活的对象再内存中移动,尽量挪到一边去,让存活对象紧凑的靠在一起,避免垃圾回收后产生内存碎片
系统对内存使用压力的估算方法
1.,每秒钟系统会使用多少内存空间,然后多长时间会触发一次垃圾回收?
2.垃圾回收之后,你们系统内大体会有多少对象存活下来?为什么?
3.然后都有哪些对象会存活下来?存活下来的对象会占多少内存空间?
如何进入老年代
1.躲过15次GC
对象每次再新生代里躲过一次GC被转移到一块S区,此时他的年龄就增长一岁,默认的设置下,当对象的年龄为15的时候就会进入老年代 jvm参数设置-XX:MaxTenuringThreshold
2.动态对象年龄判断
就是当前放对象的S区,一批对象的总大小大于了这块S区域的内存大小的50%,那么此时大于等于这批对象的年龄的对象就进入老年代
规则:年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区<br>域的50%,此时就会把年龄n以上的对象都放入老年代。
一次达到Survivor 100%不会立马触发动态年龄判定机制,需要下一次GC的时候看你还是超过Survivor 50%,才会进行动态年龄判定
实战:为了避免动态年龄判断把S区的对象直接进入老年代,如果新生代的内存有限,可以调整jvm参数"-<br>XX:SurvivorRatio=8" 默认Eden区80%,也可以降低Eden区的内存大小,给S区更多的内存,然后让每次Minor gc存活下来的对象进入S区
3.大对象直接进入老年代
jvm参数
是“-XX:PretenureSizeThreshold”,可以把他的值设置为字节数,比如“1048576”字节,就是1MB。如果你创建的对象大小大于这个值,就直接进入老年代,根本不经过年轻代
好处:避免再新生代出现大对象,屡次躲过GC,还得再S区来回复制多次才能进入老年代,
4.Minor GC后的对象太多无法放入Survivor区怎么办?
假设Minor Gc后,eden区有150M的存活对象,S区放不下,就直接进入老年代
5.老年代空间分配担保规则
原因
minor gc之后,S区放不下,有可能老年代也放不下
流程
再执行minor gc之前,jvm都会检查老年代的可用内存大小是否大于新生代所有的对象总大小。如果jvm参数设置了,就看看老年代的可用内存大小是否大于之前历代每一次Minor gc后进入老年代的对象的平均大小。如果参数没有设置,就会直接触发一次full gc对老年代进行回收,尽量回收一些内存,然后再执行Minor gc
jvm参数设置
-XX:-HandlePromotionFailure”的参数是否设置了
对老年代触发垃圾回收时机
1.再minor gc 之前,很可能Minor gc后要进入老年代的对象太多了,老年代放不下,此时需要提前进行full gc,然后再进行minor gc
2.再minor gc之后,存活的对象老年代放不下了
3.老年代可以设置一个阈值,一旦老年代内存使用达到这阈值,就会触发full gc
minor gc触发时机
当新生代的Eden区和其中一个Survivor区空间不足时。
Minor gc后的几种情况
1.存活对象 < S区的大小,直接进入S区
2.存活对象 > S区的大小 但是 <老年代可用内存的大小, 进入老年代
3. 存活对象 > S区的大小 同时 > 老年代可用内存大小,此时老年代都放不下这些对象了,就会出现 “Handle Promotion Failure 的情况,触发一次full gc。full gc是对老年代回收,同时也一般回收新生代的垃圾,
4.如果再3的基础上,老年代还是没有足够的内存空间存放Minor gc存活的对象,就会发生OOM,内存溢出
垃圾回收器
Serial 和 Serial Old
Serial回收年轻代,Seraial Old回收老年代
原理:单线程运行,垃圾回收的时候会停止我们系统的其他线程的工作,让我们的系统直接卡死,然后让他们垃圾回收,我们几乎不用
ParNew
回收新生代,采用的是复制算法
流程:把Eden区的存活对象标记出来,全部转移到S1区,然后一次性清空Eden区的垃圾对象,当Eden区再次满的时候,触发minor gc, 去标记Eden区和S1区的存活对象,一次性转移到S2区,然后清空Eden区和S1区。
CMS
回收老年代,采用的是标记清除算法,会占用CPU资源
Concurrent Mode Failure问题
再并发清理阶段,CMS只清理之前标记过的垃圾对象。但是这个阶段系统程序一直再运行,可能随着系统运行有些对象进入老年代,同时还变成垃圾对象,这些垃圾对象就是浮动垃圾。虽然这些对象是垃圾对象,但CMS只能回收之前标记好的垃圾对象,只能等到下一次GC的时候回收。所以为了保证CMS再垃圾回收期间,还有预留内存空间让一些对象进入老年代
jvm参数:“-XX:CMSInitiatingOccupancyFaction”参数可以用来设置老年代占用多少比例的时候触发CMS垃圾回收,JDK 1.6里面默认的值是92%。老年代占用了92%空间了,就自动进行CMS垃圾回收,预留8%的空间给并发回收期间,系统程序把一些新对象放入老年代中。
如果再CMS垃圾回收期间,系统要放入的对象大于老年代可用内存,会发生concurrent mode failure问题,就是说并发垃圾回收失败了,我一边清理 你一边把对象放入老年代,内存都不够了。此时就会用Serial Old代替CMS,直接强行把系统程序 stop the world,重新进行长时间的Gc Roots追踪,标记全部的垃圾对象,不允许创建新对象,然后一次性回收垃圾对象,恢复系统运行
内存碎片问题
老年代的CMS采用的是标记-清除算法,每次都是标记出来垃圾对象,然后一次性回收。这样会导致大量的内存碎片。如果内存碎片太多,会导致后续进入老年代的对象找不到可用的连续内存空间,然后触发full gc。CMS有一个参数是“-XX:+UseCMSCompactAtFullCollection”,默认就打开了。再full gc后需要再次进行 stop the world.停止工作线程,进行内存碎片整理。把存活的对象挪到一边,空出来大片的连续内存,避免内存碎片。还有一个参数是“-XX:CMSFullGCsBeforeCompaction”,这个意思是执行多少次Full GC之后再执行一次内存碎片整理的工作,默认<br>是0,意思就是每次Full GC之后都会进行一次内存整理。
流程
1.初始标记
标记出来被GC ROOTS直接引用的对象,会造成stop the world, 但其实影响不大, 因为速度很快
2.并发标记
可以让系统线程随意创建新的对象,继续运行,再运行期间可能创建新的存活对象,也可能部分存活的对象失去引用,变成垃圾对象。这个阶段是对老年代所有对象进行Gc roots跟踪,是最耗时的,但是跟系统并发运行的,不会对系统造成影响
3.重新标记
再第第二阶段,你一边标记存活对象和垃圾对象,一边系统再不停的创建新的对象,让老对象变成垃圾对象。再第二阶段结束后,肯定有存活对象和垃圾对象是第二阶段没有标记出来的。所以此时进入第三阶段,要继续让系统程序停止运行,再次进入stop the world.然后重新标记再第二阶段创建的对象还有一些已有对象可能失去引用变成垃圾的。运行速度很快
4.并发清理
这个阶段系统程序可以随意运行,清理标记的垃圾对象。这个阶段比较耗时,因为需要进行对象的清理,但是和系统程序并发运行的,不影响系统程序
G1
可以同时回收新生代和老年代,算法性能更好 , 特点就是把java堆内存拆分成多个大小相等的region,最大的特点就是设置垃圾回收预期停顿时间。其实我们对内存合理分配和参数优化就是为了减少minor gc 和full gc,尽量减少gc带来的系统停顿,避免影响系统请求
parNew和cms的痛点:stop the world
默认新生代最多只能占据堆内存60%的Region
GC时机
根据你设定的gc停顿时间给你的新生代不停分配更多Region然后到一定程度,感觉差不多了,就会触发新生代gc,保证新生代gc的时候导致的系统停顿时间在你预设范围内。
在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2MB,只要一个大对象超过了1MB,就会被放入大对象专门的Region中。而且一个大对象如果太大,可能会横跨多个Region来存放
核心设计思路
G1可以设定垃圾回收对系统的影响,他自己通过把内存拆分成大量小region,以及追踪每个region可以回收的对象大小和预估时间,最后再垃圾回收的时候,尽量把gc对系统的影响控制再你指定的时间范围内,同时再有限的时间内回收更多的垃圾
流程
1.初始标记
首先触发一个初始标记操作,这个过程是需要 stop the world的,仅仅只是标记一下gc roots能直接引用的对象,速度很快
2.并发标记
这个阶段系统程序可以运行,同时进行gc roots跟踪所有存活对象
3.最终标记
这个阶段系统程序再次进入stop the world,会根据并发标记阶段记录了那些对象修改,最终标记哪些是存活对象,哪些是垃圾对象
4.混合回收
因为我们设定了对GC停顿时间的目标,所以说他会从新生代、老年代、大对象里各自挑选一些Region,保证用指定<br>的时间(比如200ms)回收尽可能多的垃圾,这就是所谓的混合回收,
Stop the World问题
再垃圾回收的时候,尽可能的让垃圾回收器专心致志的工作,不能随便让我们的系统再创建新的对象,所以此时jvm会再后台进入 stop the world 状态,这样的话我们系统暂停运行,不再创建新的对象,同时就是让垃圾回收线程尽快完成垃圾回收工作,一旦垃圾回收完毕,就可以继续恢复我们系统的运行了
类加载的流程
加载
首先你的代码中包含“main()”方法的主类一定会在JVM进程启动之后被加载到内存,开始执行你的“main()”方法中<br>的代码。接着遇到你使用了别的类,比如“ReplicaManager”,此时就会从对应的“.class”字节码文件加载对应的类到内存里来。
验证
就是根据java虚拟机的规范,来校验你加载进来的class文件内容是否符合指定的规范。如果你的class文件被人篡改,jvm是没法执行的,所以加载.class文件到内存后,必须先验证一下,然后才能交给jvm运行
准备
其实就是给这个ReplicaManager分配内存,然后给static修饰的变量(类变量)赋默认的初始值
初始化
再这个阶段,就会执行类的初始化代码,准备的阶段的时候只是分配的内存和给个初始值0,初始化阶段是完成配置项的读取,然后在赋值。如果发现父类还没加载和初始化,先加载父类和初始化父类然后再初始化子类
使用
卸载
类加载器
bootstrap classloader(启动类加载器)
它负责加载我们机器安装的java目录下的核心类,一旦jvm启动,首先会依托启动类加载器去加载你java目录下的lib目录中的核心类库
extension classloader(扩展类加载器)
你的java目录有一个lib\ext目录,里面有写类是需要这个加载器加载的,支撑你的系统运行
application classloader(应用类加载器)
这个加载器是负责加载指定classpth环境变量所指定的路径中的类。大概就是去加载你写好的代码,这个加载器就负责把你写好的代码加载到内存中
双亲委派机制
jvm的加载器是有亲子层级的,第一层是启动类加载器,第二层是扩展类加载器,第三层是应用类加载器,自定义加载器再最下层
流程:
比如你的应用类程序加载器需要加载一个类,首先他会委派他的父类加载器去加载,最终会传导到顶层的类加载器去加载。如果父类加载器的负责的加载范围内没有找到这个类,会下推加载权利给自己的子类加载器
好处:
先找父类加载,不行的话再去找子类加载,这样就避免了多层级的类加载器重复加载的问题
JVM的内存区域
java虚拟机栈
保存每个方法的局部变量。如果线程执行了一个方法,就会对这个方法创建一个栈帧。栈帧包括局部变量表,操作数栈,动态链接,方法出口等等
流程:
1.jvm启动,先加载你的kafka类到内存,然后有个main方法,开始执行你的main方法,main线程的关联了程序计数器的,会记录你执行到哪一行指令
2.main线程再执行main方法的时候,会再main线程关联的java虚拟机栈里,会压入一个main方法的栈帧,接着发现需要创建ReplicaManger类的实例对象,会加载这个类到内存中。然后对象实例分配再堆内存中,并再main方法的栈帧的局部变量表引入一个replicaManager变量。让他引用ReplicaManager这个对象再堆内存中的地址
3.main线程就执行ReplicaManager中的方法,会依次把执行到的方法对应的栈帧压入自己的虚拟机栈,执行完之后再把对应方法的栈帧从java虚拟机栈中出栈
JVM核心参数
-XX:InitialHeapSize
初始化堆大小
-Xmn
java堆内存的新生代大小,扣除新生代的就是老年代的大小
-XX:PermSize
永久代大小(方法区)1.8以后 -XX:MetaspaceSize
-XX:MaxPermSize:
永久代最大大小 1.8以后 和-XX:MaxMetaspaceSize
-XX:+PrintGCDetils:
打印详细的gc日志
-XX:+PrintGCTimeStamps
打印每次gc发生的时间
“-XX:+UseG1GC
指定使用G1垃圾回收器,此时会自动用堆大小除以2048
XX:MaxGCPauseMills”
最大停顿时间 默认值是200ms
-XX:CMSInitiatingOccupancyFaction
老年代使用了多少比例的内存,就开始full gc, 默认92%
-XX:UseCMSCompactAtFullCollection
每次full gc之后,都需要内存碎片整理
-XX:CMSFullGCsBeforeCompaction
执行多少次Full GC之后再执行一次内存碎片整理的工作,默认<br>是0,意思就是每次Full GC之后都会进行一次内存整理。
-XX:MaxTenuringThreshold
最大分代年龄 ,默认15
-XX:PretenureSizeThreshold
大对象直接进入老年代
“-XX:G1HeapWastePercent
默认值是5%
空闲出来的Region数量达到了堆内存的5%,此时就会 立即停止混合回收
-XX:G1MixedGCLiveThresholdPercent
他的默认值是85%,意思就是确定要回收的Region的时候,必须是存<br>活对象低于85%的Region才可以进行回收
-XX:+CMSParallelinitialMarkEnabled
再CMS垃圾回收的初始标记阶段开启多线程并发执行,减少stop the world时间
-XX:+CMSScavengeBeforeRemark
再CMS的重新标记阶段之前,先尽量执行一次young gc
好处:先执行一次young gc就会回收掉一些年轻代没有人引用的对象,那么再CMS重新标记阶段就可以少扫描一些对象,提示CMS重新标记阶段的性能,减少他的耗时
-XX:+DisableExplicitGC
禁止显示执行gc .比如System.gc()
格式
-Xms512M -Xmx512M -Xmn256M -Xss1M -XX:PermSize=128M -XX:MaxPermSize=128M
模板
“-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -<br>XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -<br>XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSCompactAtFullCollection<br>-XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSParallelinitialMarkEnabled -XX:+CMSScavengeBeforeRemark”
jvm的四种引用
强引用
1个变量引用一个对象,只要是强引用对象,垃圾回收的时候绝对不会回收这个对象
软引用
1个对象被“SoftReference”软引用类型的对象包裹起来了。正常垃圾回收是不会回收软引用的对象的,但是垃圾回收之后发现内存空间不够存放新的对象,内存快要溢出了,就会把这些软引用到的对象回收。也就是能存活到下一次垃圾回收的时候
弱引用
弱引用就和没引用一起,垃圾回收的时候就会把这个对象回收
young gc日志查看
GC (Allocation Failure
对象分配失<br>败。
ParNew: 4030K->512K(4608K), 0.0015734 secs
1.指定的ParNew垃圾回收器执行GC
2. 4608K也就是4.5M,Eden+1个S的大小
3. 4030K->512K 对年轻代执行了一次GC,GC之前使用了4030K,但是GC之后只有512K的对象存活下来
4. 0.0015734 secs 本次Gc耗费的时间,大概1.5ms,仅仅回收3M的对象
4030K->574K(9728K), 0.0017518 secs (描述整个堆内存的情况)
9728K(9.5M) 堆内存的总可用空间, 其实就是年轻代4.5M+老年代5M,然后GC之前整个堆内存使用了4030K,GC后java堆内存使用了574K
par new generation total 4608K, used 2601K
ParNew垃圾回收器负责的年轻代总共有4608k(4.5M)可用内存,目前使用了2601K(2.5M)
eden space 4096K, 51% used
Eden区有4M的内存,被使用了51%
from space 512K, 100% used
from survivor区,512K是100%使用率
to space 512K, 0% used
to survivor区, 512K是0%使用率
concurrent mark-sweep generation total 5120K, used 62K
concurrent Mark Sweep(CMS)垃圾回收器,管理老年代的内存空间一共是5M,此时使用了62K
CMS: 8194K->6836K(10240K), 0.0049920 secs 老年代gc
老年代从8m对象的占用变成了6M 的对象占用
jvm分析
jstat -gc PID
S0U
from survivor区当前使用的内存大小
S1C
to survivor区当前使用的内存大小
MU
方法区(永久代,元数据区)当前使用内存的大小
YGC
系统运行迄今为止的young gc 的次数
新生代对象增长的速率,young gc触发频率, young gc的耗时,每次young gc后有多少对象是否存活下来,每次young gc过后有多少对象进入了老年代,老年代对象的增长速率, full gc的触发频率,full gc的耗时
新生代对象增长的速率
你只要再线上linux机器上运行:jstat -gc pid 10 每隔1秒更新出来最新的一行jstat统计信息,一共执行10次jstat统计 jstat -gc PID 1000 1000 每隔1秒钟打印一次,连续打印1000次
例子:比如第一秒显示eden区使用了200M内存,第二秒显示出来的统计信息里发现Eden区使用了205M,第三秒发现Eden区使用了209M内存,依次类推,推断出来这个系统每秒新增5M左右的对象
young gc的触发频率和每次耗时
比如Eden有800M内存,发现高峰期每秒新增5M对象,大概高峰期3分钟会触发一个young gc. 日常期每秒新增0.5M对象,那么日常期大概需要半个小时才会触发一次young gc
jstat会告诉你迄今为止系统发生了多少次young gc和这些young c的耗时。 比如系统运行24小时发生了260次young gc ,总耗时20s.那么平均下来每次young gc大概就耗时几十毫秒
每次young gc有多少对象是存活和进入老年代的
之前我们推算出来高峰期的时候多久发生异常young gc,比如3分钟会有一次,那么我们执行 jstat -gc pid 180000 10.就相当于每隔3分钟执行一次统计,连续执行10次。此时大家可以观察,每隔3分钟之后发生了一次young gc,此时Eden,Surivivor 老年代的对象变化。正常情况下,老年代的增长不可能快速的,如果你发现每次young gc后,每次老年代新增几十M,很有可能young gc存活的对象太多了。
full gc的触发时机和耗时
比如老年代的总内存800M,每隔3分钟新增50M对象,大概1个小时就会触发full gc。可以根据jstact可以知道full gc的次数以及总耗时,比如full gc执行了10次,总共耗时30s,大概每次full gc的时间3s左右
jmap和jhat
jmap -heap PID
会打印堆内存的各个区域的情况
jmap -histo PID
按照各种对象占用内存空间的大小降序排列,把占用内存最多的对象放在最上面
使用jmap生成堆内存转储快照
jmap -dump:live,format=b,file-dump.hprof PID
这个命令会再当前目录下生成一个dump.hprof文件,需要用jhat在浏览器中分析堆快照
jhat -port 8000 dump.hprof
访问http://ip:8000
优化思路:
尽量让每次young gc后的存活对象小于Survivor区域的50%, 都留存在年轻代里,尽量别让对象进入老年代,尽量减少full gc的频率, 避免频繁full gc对jvm性能的影响
堆内存溢出
老年代放了过多的对象,而且大多数都是存活的,即使GC过后还是大部分都存活,所以要继续放入更多的对象已经不可能了,只能引发内存溢出问题