#垃圾回收
1、如何判断对象可以垃圾回收
引用计数法 (记录被引用的次数,当一个变量引用次数为0时,就可以当作一个垃圾被回收)
缺点:循环引用问题,虽然不被使用,但一直互相引用,所以不能被垃圾回收
可达性分析算法(确定一系列根对象GC Root(一定不能被回收的对象))
工具:Memory Analyzer(MAT)堆分析器,帮助你找到内存的泄漏和减少内存的消耗、、
工具的使用:首先先使用jps知道进程ID,jmap -dump:format=b,live,file=1.bin 进程ID
1.bin是文件的名字
打开MAT,在MAT中打开生成的.bin文件,打开Java Basic的GC Roots就可以查看当前抓取的快照中有哪些根对象了、、、
根对象:System Class(java启动时生成的类),Native Stack(操作系统调用的Java对象),Thread(一些活动线程所使用的对象,每次方法调用都会产生一个栈帧,栈帧使用时会产生一些东西(局部变量等)都可以作为根对象,调用的方法的参数也是根对象),Busy Monitor(正在加锁的对象syn)
创建堆使用情况的文件
四种引用
(1)强引用
new了一个对象,然后把这个对象赋值给一个变量,这就是强引用
(2)软引用
内存不够时,就会释放软引用对象、、、
(3)弱引用
只要发生垃圾回收就会把其释放掉、、
当软、弱对象都被回收了之后,软、弱对象本身就是一个对象,就会被(也可以不放进去)放到引用队列中、方便对这两个对象进行空间释放
(4)虚引用(Cleaner线程中的clean方法,根据之前记录的直接内存的地址(OS的内存),调用UnsAre.freeMemory将其清除)
虚引用对象会被分配一块byteBuffer和直接内存,当使用结束后ByteBuffer可以自己进行垃圾回收,但是它被分配直接内存不被Java所管理,所以,当ByteBuffer被回收的时候,虚引用对象就会被放到引用队列,
(5)终结器引用
Object父类,会有一个finallize(),当对象A重写了finallize(),垃圾回收时,就会把终结器引用对象放入到引用队列,再由一个优先级很低的线程对这个队列进行清理,清理时,会调用对象A的finallize()方法,把A清理掉,然后再把终结器引用对象进行回收、、
效率比较低,先入队,而且线程优先级很低,就导致很久才会释放内存、、
-Xms设置的是JVM启动时的堆内存大小,-Xmx设置的是JVM堆内存的最大值、、、
最后两个对象必须配合着引用队列使用、、
这个例子Xmx设置为了20m,20M
-Xmx20m -Xx:+PrintGCDetails -verbose:gc
第四次,新声代和老年代都回收了,但是都无效,没回收多少,此时就会进行软引用的对象进行回收、、前三个都清理了,第四个进去
所以最后就只有第四个对象的内存、、
在这个Queue中存放软引用对象,然后把它里面的对象都删掉了,再去遍历list的时候就不会再输出null了、、、
2、垃圾回收算法
(1)标记清除
扫描堆里面的所有对象,先把没有根对象进行引用的对象进行标记,第二步再把标记的对象进行清理(将其地址记录在空闲地址列表里,下次使用时在列表里面去取就好了)、、、
缺点:内存碎片
(2)标记整理
标记和上一个一样,清除改成了整理,避免了内存碎片的问题
缺点:速度比较慢,一旦在整理过程中,引用被整理的对象就会有很多的问题,变量的引用地址等内容会出现问题、、
(3)复制
标记
第二步复制到另一区域,也有整理的作用
然后交换from和to的位置
优点:没有碎片,且占用双倍的内存空间
3、分代垃圾回收
以上的三种是一起结合着使用的、、
长时间使用的对象放在老年代中,用完就可以丢弃的对象可以放在新声代里面、、、
(Java堆的分配)老年代垃圾回收很少,新声代垃圾回收比较频繁、、
永久代是一种方法区的实现方法、、永久代和元空间是不同jdk版本对方法区的实现、、
先往新声代的伊甸园里面放对象,如果东西多了快到幸存区了,那么就执行一次标记复制(Minor GC),将标记后剩下的部分放到幸存区To里面,在To里面的对象寿命+1(对象寿命初始值是0),然后交换From和To的名字,对伊甸园的对象进行清理
然后再放到伊甸园,如果再满了,就放到To里面,From里面的对象如果和GC Root对象有关联那么还是放到To里面,然后寿命+1,交换From和To的名称,以此类推、、
Minor GC会引发一次stop the world,会让所有的用户线程暂停,只执行垃圾回收的线程、、
如果某个对象的寿命超过一定的阈值(最大是15次,这个数据是放在对象头中,4bits,所以最大是15,不同的垃圾回收器这个值设定的都不一样),那么就放到老年代里面,等到老年代和新声代都满了,就会触发Full GC、、、
老年代的清理算法是(标记+清除/整理)
GC分析_大对象
如果最开始存入的大对象,就比新声代的内存还要大,如果老年代的内存够的话就直接存入到老年代里面,就不会引起新声代的GC了
就会造成OOM、、、在OOM之前还会先做一次GC,再做一次Full GC、、
这个是强引用,所以就会直接OOM
如果在一个子线程中直接这段代码,OOM异常、、然后主线程是正常执行的、、
(这块讲的不是很清晰,后期要自己去补充、、)
4、垃圾回收器
(1)串行
单线程的垃圾回收器,当进行垃圾回收时,将其他的线程都暂时暂停、、
适用场景:堆内存较小,适合个人电脑(核数较小的情况)
-XX:+UseSerialGC = Serial + SerialOld
新声代:复制 老年代:标签+整理
(2)吞吐量优先
多线程的
堆内存较大,且多核CPU
单位时间内,STW的时间最短,平均每秒的垃圾回收时间最短,一个小时两次
0.2,0.2 总共0.4
垃圾回收时间占程序运行时间的占比,占比越低,吞吐量越高
第一行第一个新声代的复制算法,第二个标记整理算法
还是用户线程暂停,暂停后是多个垃圾回收线程在执行,等到这些执行完,用户线程再继续执行、、、
4核CPU——》4个垃圾回收线程、、
最后一行是对垃圾回收的线程数进行规定的
第二行是自适应的新声代大小
第三行是根据用户设定的吞吐量目标去设定内存大小,1/(1+ratio),ratio=99=》0.01 ,代表着100分钟内只有1分钟用于垃圾回收,但一般ratio设置为19代表着100分钟内有五分钟进行垃圾回收
第四行是暂停的毫秒数,默认的是200毫秒、、、
(3)相应时间优先
多线程,用户线程和垃圾回收线程并发执行、、
堆内存较大,且多核CPU
让stop the world尽可能短STW,单次时间最短,0.1,0.1,0.1,0.1,0.1,总共0.5
总时间0.4优于0.5
第三个是执行垃圾时候的。内存占比、、因为用户线程一边执行,垃圾回收也正在执行,所以在这个过程中还是会产生新的垃圾,所以要规定内存到多少了就执行一下垃圾回收,不能满了再回收,会有问题、、默认值65%
第一行,标记清除算法的垃圾回收器,并发的,第一个工作在老年代,第二个,新声代复制算法的垃圾回收器,如果并发失败,就会变为串行的(标记整理)垃圾回收、、第二个工作在新生代
如果JVM的垃圾回收参数设置为第一行的三个,那就意味着,在新生代中使用
-XX:UseParNewGC~SerialOld意味着在新生代中使用并行的回收方法(复制),如果并行失败,那么就改成串行回收
在老年代中会使用CMS进行回收
如果没有标记第二个,那么新老都使用CMS进行回收
第二行,并行的垃圾回收线程数,一般和CPU的核数是一样的 ;第二个,并发的线程数,一般设置为并行的线程数的1/4
第四行,新生代可能会引用老年代的对象(即使是找到了,新生代的也会被回收掉),所以先对新生代进行垃圾回收,避免引用的时候扫描整个堆、、 +就是打开,-就是关闭
CMS,标记清除算法会产生比较多的内存碎片,会造成内存不足,并发失败、、CMS这个垃圾回收器就不能工作了、、、就会退化为SerialOld(串行的标记整理),这样会很损耗性能、、
(4)G1 Garbage First/One
取代了CMS,同时注重吞吐量和低延迟、、
- XX:MaxGCPauseMillis=time 默认延迟目标是200毫秒,如果想增大吞吐量,可以将这个数值提高些
超大堆内存,会把堆分为多个大小相等的Region、、1/2/4/8 M
- XX:G1HeapRegionSize=size这个命令可以设置堆Region的大小
- XX:+UseG1GC
整体是使用标记+整理算法,两个区域之间是复制算法
1)G1垃圾回收阶段
新生代垃圾回收——》老年代内存到一定的阈值,新生代回收+并发标记——》混合收集(新老、幸存区都会收集)
2)Young Collection
不够年龄的会被放到另一个幸存区里,如果另一个有地方的话
3)Young Collection + CM
CM 并发标记
在Young GC时会进行GC Root的初始标记(找到那些根对象,新生代GC),并发标记是指顺着根对象找到其他的一些对象(在老年代才会进行并发标记,不会影响用户线程的进行)
老年代占用堆空间比例到达阈值时,进行并发标记(不会STW),由上图的JVM参数决定
并发标记是标记出哪些老年代的Region是可以被回收的
4)Mixed Collection
老年代使用了复制算法,新生代的和之前是一样的、、
但是老年代只是复制回收了一部分,使用-XX:MaxGCPauseMillis=ms决定,数值小就回收的少,数值大就回收的多、、调回收价值高的老年代Region(回收后释放的空间更多的Region)
最终标记,是为了并发标记(因为其它用户线程也在工作)可能漏掉了一些对象,
5)Full GC
CMS与G1是垃圾回收速度赶不上产生速度才是Full GC
前两个老年代内存不足触发的垃圾回收就可以成为Full GC了
6)Young Collection跨代引用
根对象伊甸园,可达性分析,放到幸存区
那么怎么找根对象呢?一部分在老年代,红色的为脏卡,只扫描老年代的脏卡就可以获得了,而不用全部扫描
伊甸园的Remember Set记录都有哪些脏卡、、
7)Remark
CMS和G1共有的过程:
并发标记阶段,重新标记阶段(Remark)
这个图是并发标记阶段时,对象处理的一个状态
黑色的代表:已经完成的,且有引用,保留下来的
灰色的代表:正在处理中
白色的代表:还没有被处理
有引用的最终都会存活,没有引用的最后就清理掉了
并发标记是还有用户线程还在使用C,所以可能灰色B最开始时有引用,快结束时又被用户线程取消了这种引用,这个时候就认为C是没有引用的白色,但在此之后,可能又被其它用户线程给引用了C,如果按照原来的思路把C给清理了就会出现问题
也就是说C的引用发生了变化,写屏障的代码就会执行,写屏障:把C放入到新的队列里面,且把C变成灰色,等到并发标记结束之后,就会进入重新标记阶段,重新标记会STW,就会从队列中把对象一个一个拿出来,如果是灰色的,那就变成黑色,这样就避免了之前的问题、、、
8)JDK 8u20字符串去重
9)JDK 8u40并发标记类卸载
一个类加载器和他们的实例全部没有引用了就可以卸载了(只是自定义的类加载器、、例如一些框架使用的类加载器、、)
10)JDK 8u60回收巨型对象
巨型对象,老年代的引用为0时就可以被回收了
11)JDK 9并发标记起始时间的调整
12)JDK 9更高效的回收
5、垃圾回收调优
Javase官网去看Java命令的所有参数
(1)调优领域
GC调优主要影响网络延迟,STW的影响很明显
1)内存
2)锁竞争
3)cpu占用
4)io
(2)确定目标
【低延迟】还是【高吞吐量】,选择合适的回收器
互联网项目:低延迟
低延迟:CMS(JDK8),G1(JDK9推荐),ZGC(JDK12推荐)
高吞吐量的选择:ParallelGC
(3)最快的GC是不发生GC
·查看FullGC前后内存占用,考虑以下几个问题:
·数据是不是太多了 (代码问题)
·数据表示是否太臃肿
·对象图 (User只用ID那就只查ID,不要整个User对象都查出来)
·对象大小 (16 包装对象,Integer一个对象头就是16个字节,内容4个字节,外加上对齐就是24个字节了,int只有4个字节)
·是否存在内存泄漏
·static Map map= 一直往里面放数据,不删除
·软引用
· 弱引用
· 三方的缓存实现
mybatis的懒加载查询?????这是啥
(4)新生代调优
·新生代的特点
·所有的new操作的内存分配非常廉价
new一个对象时首先在伊甸园中分配,这个速度是非常快的,因为每个线程都会被在伊甸园中分配一块私有的区域,tlab,每线程个都分配是因为线程安全问题
·TLAB thread-local allocation buffer
·新生代中死亡对象的回收代价是零
伊甸园中被标记的那个对象的回收
·大部分的对象用过即死
·Minor GC的时间远远低于Full GC
新生代很大,老年代就会比较小,新生代向老年代copy时,老年代可能就会放不下,触发Full GC,时间会更长
新生代很小就会频繁触发Minor GC
建议:新生代大小为【heap*1/4,heap*1/2】
8:1:1
新生代主要是在复制上(位置迁移外加上改变其它对象的引用地址比较废时),标记时间比较小
新生代能容纳所有【并发量*(请求·响应)】
e.g.一次请求响应512K内存,并发量1千,大约是512M,那么将新生代设置为512M比较好,这样可以不触发/较少触发 新生代的垃圾回收
##幸存区大到能保留【当前活跃对象+需要晋升的对象】
第一个是现在还在引用,但是之后就不用了,会被回收
第二个是会被晋升到老年区的对象
如果幸存区比较小,就会把阈值调小,之前是15晋升,现在可能就是5晋升、、等到老年代内存不足才会被回收、、
·晋升阈值配置得当,让长时间存活的对象尽快晋升
第二行是打印晋升阈值信息、、、age2的total是age1和2总共占用的空间大小
第一行是调整最大的晋升阈值
(5)老年代调优
(6)案例