上一篇小文章总结了一下JVM,今天就来给大家捋一捋JVM里经常被问的GC机制吧,走过路过别错过,千山万水总是情,大哥大姐留个赞,谢谢。下面就进入正题吧,宏观回顾JVM内存模型:
一、JVM内存模型
从大的方面来讲,JVM内存有非堆内存和堆内存之分,也就是大家口中所说的永久区内存(permanent space)和堆内存(heap space)。
栈内存一般都不归于JVM内存模型中,因为栈内存属于线程私有。永久区内存存的是class类级对象如class本身的信息,method,field等等;堆内存存储的是对象实例和数组。
如上图所示,heap space由Old Generation(年老区)和Young Generation(年轻区)组成。Old Generation(年老区)存放的对象一般是一些存活周期比较长的对象,然后一般新创建的对象会先存放在年轻区,但是也有个例,年老区有个对象保护机制,就是当新建的对象在年轻区(该年轻区是刚经历过GC后的)放不下时,会启动临时保护机制,帮忙先存放一下新创建的大对象。JVM有-XX:PretenureSizeThreshold参数,大于这个参数值的对象将直接分配到老年代中。Young Generation(年轻区)还根据8:1:1的比例分为了Eden区、survivor0区和survivor1区,新建的对象一般会放在Eden区,当Eden区内存不足,会GC一次把Eden区的对象复制到survivor区,survivor区内存不足也会把对象复制到old区,当所有区可使用内存大小都不足以新建对象,就会抛出OOM(内存溢出)了。JVM新建对象的过程如下:
在这稍微提一下,在JAVA1.7版本之前,永久区就是我们说的方法区,永久区内存才能溢出相对少见,一般加载海量class数据,超过了非堆内存容量导致。通常是发生在web应用刚加载运行时,所以推荐web都使用预加载机制,方便在部署项目时就发现并解决问题。然而在JAVA1.8版本后,永久区被移除了,更名为元空间,也就是之前存在方法区或者说是永久区的数据会存在本地内存里,本地物理机内存会相对比jvm虚拟机内存大,,但也只是减少了OOM的出现,,过大使用内存还是会OOM的。至于栈内存溢出,更少见,不过呢,如果是递归调用方法进入死循环的话,就会出现栈内存溢出了。
堆内存优化:调整JVM启动参数-Xms -Xmx -XX:newSize -XX:MaxNewSize,如调整初始堆内存和最大堆内存 -Xms256M -Xmx512M。 或者调整初始New Generation的初始内存和最大内存 -XX:newSize=128M -XX:MaxNewSize=128M。
永久区优化:调整PermSize参数 如 -XX:PermSize=256M -XX:MaxPermSize=512M。
栈内存优化:调整每个线程的栈内存容量 如 -Xss2048K 。
总结一下,一个运行中的JVM所占的内存= 堆内存 + 永久区内存 + 所有线程所占的栈内存总和 。不过呢,有一句话还是要提一下的,不要为了调优而调优,JVM的堆内存新老年代空间分配不要随意改变,会影响一些垃圾收集器自带的回收性能。
二、垃圾回收机制
1、范围:垃圾回收区域
java虚拟机栈、本地方法栈以及PC寄存器都是线程私有的,随着线程的创建和消亡,会自动被GC机制回收的。然而方法区和java堆是GC重要回收区域,毕竟一个接口可能会有多个实现类,多个实现类会被分配的内存也不同,一个方法多个分支的执行也有被分配不同的内存,而且这些对象或者实例随时会不再被引用的,因此需要GC进行动态回收才能保证JVM不会那么容易出现OOM。
2、前提:如何判断对象可被回收
①引用计数法
通俗说法就是通过一个计数器来计算对象的引用次数,初始化数值是0,当对象被引用就+1,不被引用时就计数器-1,当GC回收时,会把数值为0的回收掉;不过此方法虽然简单,但是呢解决不了循环引用的BUG,比如对象A包含指向对象B的引用,对象B也包含指向对象A的引用,但没有引用指向A和B,运用引用计数法来回收的话,A和B的引用都是1,不会被回收,会出现内存泄露问题了。
②根搜索
根搜索又称之为可达性分析法,通过选取一个GC Root(根对象)作为起始点,然后向下搜索,如果有哪个对象是无法连接到GC Root的话,便判断为不可达对象,也就是该对象没有被引用,可以作为根的对象有:栈中变量引用的对象,类静态属性引用的对象,常量引用的对象等。因为每个线程都有一个栈,所以我们需要选取多个根对象。不过呢,被根搜索第一次找到的对象,不会立马被回收,而是被标记后丢到F-queue队列里等到finalize()方法执行,看看是否会复活对象,如果不被复活,那在第二次GC时会被回收掉了。需要注意的地方,第一,finalize()方法只会执行一次,也就是对象只有一次复活机会,第二,执行GC后,要停顿半秒等待优先级很低的finalize()执行完毕。
3、策略:GC回收算法
①标记-清除法
顾名思义,标记清除法就是两步,第一步,先找到不被引用的对象,然后进行标记,第二步就是进行清除回收对象。该算法毕竟容易实现,但是呢会产生不连续内存碎片,这样子,如果有大对象新建时,很容易会出现内存不足,然而进行又一次的GC,很消耗CPU性能哦。
②标记-复制法
一开始就是把内存分为两部分,当进行完GC后,把还存活的对象复制到另一部分空闲内存空间,该算法实现也很简单,当大部分对象都被回收时这种策略也很高效。但也有一个缺点,可用内存减少了一半。怎么办呢,聪明人做法,不按照1:1平分,而是按照8:1:1分为一个Eden区和两个survivor区,每次将Eden和Survivor中存活的对象复制到另一块空闲的Survivor中。该三个区域组成了新生代。
③标记-整理法
根据老年代的特点,采用回收掉垃圾对象后对内存进行整理的策略再合适不过,将所有存活下来的对象都向一端移动。
4、实现:垃圾收集器
上图已经很详细了,但是我简单say一下可用来回答面试官吧。
Serial,单线程串行收集器,应用于新生代垃圾回收,使用标记-复制法,执行期间需要STW暂停业务流程。
Serial Old,Serial的老年代版本,应用于老年代垃圾回收,主用于JVM的client回收,使用标记-整理法,执行期间需要STW暂停业务流程。
ParNew,多线程并行收集器,应用于新生代垃圾回收,使用标记-复制法,适用于server模式下,执行期间需要STW暂停业务流程。
Parallel Scavenge,多线程并行收集器,应用于新生代垃圾回收,使用标记-复制法,但是更注重于吞吐量,可精确控制。
Parallel Old, Parallel Scavenge的老年代版本,应用于老年代垃圾回收,使用标记-整理法,优先适用于吞吐量优先的环境下。
CMS,多线程并发收集器,应用于老年代回收,使用的是标记-清除法,优点是低停顿,官方文档说法是无论多少垃圾回收都可保证停顿时间是10ms,缺点会产生内存碎片,可以使用-XX:CMSFullGCsBeforeCompaction设置执行几次CMS回收后,跟着来一次内存碎片整理。
G1收集器,是CMS终极改进版,充分利用多CPU、多核进行并行和并发收集,大大缩短停顿时间,还可预测停顿时间避免应用雪崩现象,把年老和新生代区改变成大小一样的region来管理,内存使用更灵活,避免了内存碎片问题。
5、触发:何时GC回收
GC回收分类有三种,新生代回收称之为minor GC,老年代回收称之为major GC,System.gc的总回收称之为Full GC。Full GC触发条件有:①Old空间不足 ;②统计得到的Minor GC(默认是15次GC后)晋升到老年代的平均大小大于老年代的剩余空间;③永久区内存不足,不过该永久区不等同于方法区,只是堆内存自实现的一个方法区。值得一提,不同JVM对象的引用,GC也不同的:
强引用:就是类似 object obj = new object(),这样新建对象都是强引用,该实例没有对象引用时才会被回收。
软引用:适用于缓存场景,只有内存不够用才会被GC。
弱引用:在GC时一定会被回收。
虚引用:一般是用来确认对象是否被GC了。
三、JVM性能优化
此处有个小彩蛋,穿插一下JVM性能优化的简单总结。
到此,GC的简单入门到入魔大法已经完成了。多学多记录。