JVM之垃圾回收

#垃圾回收

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)

第19行不再引用ArrayList对象,所以就被垃圾回收了


创建堆使用情况的文件

四种引用



(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部分全部删掉

然后交换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、、、

老年代的清理算法是(标记+清除/整理)




没有放任何数据时新声代和老年代的内存占用情况


放了7MB的内容后,触发了一次新声代的垃圾回收


新声代的伊甸园和from马上要满了


存了8MB的内存,将新声代的内容向老年代转移,两次垃圾回收

GC分析_大对象


存入8MB

如果最开始存入的大对象,就比新声代的内存还要大,如果老年代的内存够的话就直接存入到老年代里面,就不会引起新声代的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

每个区域都是独立的e或o或s


一段时间伊甸园区满了,放到幸存区


幸存区东西比较多或者年龄很大就会放到老年代

不够年龄的会被放到另一个幸存区里,如果另一个有地方的话

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)案例

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容