分代模型介绍
根据写代码方式的不同,采用不同的方式来创建和使用对象,其实对象的生存周期不同,所以JVM将Java堆内存划分为两个区域:年轻代、老年代
通过下面的代码,来看下方法区,Java虚拟机栈和Java堆内存的关系图
public class HelloWorld {
private static Demo1 demo1 = new Demo1();
public static void main(String[] args) throws InterruptedException {
executeDemo2();
while (true) {
executeDemo1();
Thread.sleep(6000);
}
}
public static void executeDemo2() {
Demo2 demo2 = new Demo2();
demo2.execute();
}
public static void executeDemo1() {
demo1.execute();
}
}
HelloWorld类中一个静态变量demo1引用了Demo1对象,由于静态变量会长期留存在内存中使用,则demo1对象会在年轻代中留存一会,然后最终进入老年代,此时的关系图如下:
进入main方法后,会先调用executeDemo2(),执行Demo2对象的execute()方法,在executeDemo2方法会创建Demo2对象,这个对象他是用完就会回收,所以是会放在年轻代里的,由栈帧里的局部变量来引用
一旦executeDemo2()方法执行完毕后,方法的栈帧则会出栈内栈,栈内存立即回收,对应的年代里的Demo2对象会被称为垃圾对象,等待垃圾回收机制回收
紧接着会执行while循环代码,会周期性的调用executeDemo1()方法
垃圾回收机制算法
复制算法
针对新生代的垃圾回收算法,叫做复制算法,顾名思义是将存货的对象复制到另外的地方。
这就是所谓的“复制算法“,把新生代内存划分为两块内存区域,然后只使用其中一块内存
待那块内存快满的时候,就把里面的存活对象一次性转移到另外一块内存区域,保证没有内存碎片
接着一次性回收原来那块内存区域的垃圾对象,再次空出来一块内存区域。两块内存区域就这么重复着循环使用
- 复制算法缺点
按照上面的思路,从图中可以看出,每次都需要空出一般的内存不使用,非常的浪费内存,加入给新生代1G的内存空间,但是按照上面的模型只有512MB的空间可以使用,这样的话对内存的使用率只有50%
我们的代码中不停的创建对象然后分配在新生代中,但是一般很快创建的对象没人引用,成为垃圾对象,此时被垃圾回收机制回收,绝大多数的对象都是存活周期非常短的对象,可能被创建出来1毫秒之后就没人引用了,所以在每一次新生代垃圾回收后,绝大多数数对象都被垃圾回收,只有极少个数的对象存活下来。
- 解决办法
所以JVM内存模型中,把新生代内存区域划分为三块:
1个Eden区,2个Survivor区,其中Eden区占80%内存空间,每一块Survivor区各占10%内存空间,比如说Eden区有800MB内存,每一块Survivor区就100MB内存,如下图:
平时可以使用的,就是Eden区和其中一块Survivor区,那么相当于就是有900MB的内存是可以使用的
刚开始对象都是分配在Eden区内的,如果Eden区快满了,此时就会触发垃圾回收,此时就会把Eden区中的存活对象都一次性转移到一块空着的Survivor区。接着Eden区就会被清空,然后再次分配新对象到Eden区里,此时:Eden区和一块Survivor区里是有对象的,其中Survivor区里放的是上一次Minor GC后存活的对象。
如果下次再次Eden区满,那么再次触发Minor GC,就会把Eden区和放着上一次Minor GC后存活对象的Survivor区内的存活对象,转移到另外一块Survivor区去
这么做最大的好处,就是只有10%的内存空间是被闲置的,90%的内存都被使用上了
无论是垃圾回收的性能,内存碎片的控制,还是说内存使用的效率,都非常的好
内存标记算法
被使用的内存区域中的垃圾对象进行标记,标记出哪些对象可以被垃圾回收,然后直接对内存区域中的对象进行垃圾回收,把内存空出来,在被内存使用的区域里,回收掉了大量的垃圾对象,但是保留了一些被人引用的存活对象。但是存活的对象在内存区域中东一个西一个,非常的凌乱,而且造成了大量的内存碎片
图中圈红的则为出来的内存碎片,有些内存太小则无法被其他对象使用,则会造成内存浪费
年轻代
- 大部分的正常对象,都是优先在新生代分配内存的
老年代
新生代里的对象一般在什么场景下会进入老年代?
-
躲过15次GC之后进入老年代
如果一个实例对象在新生代中,成功的在15次垃圾回收后,还没有被回收掉,则会被转移到老年代中
可以通过JVM参数“-XX:MaxTenuringThreshold”来设置,默认是15岁
-
动态对象年龄判断
此规则让对象不用等待15次GC后就进入老年代
假如当前对象的的Survivor区域里,一批对象的总大小大于了这块Survivor区域的内存大小的50%,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代了
-
大对象直接进入老年代
JVM参数:是“-XX:PretenureSizeThreshold”,可以把他的值设置为字节数,比如“1048576”字节,就是1MB,如果创建的一个大于这个参数设置的大小对象,就直接把这个大对象放到老年代去,不会经过新生代
-
Minor GC后的对象太多,无法放入Survivor区
如果在Minor GC之后发现剩余的存活对象太多了,没办法放入另外一块Survivor区怎么办
在经过GC后,Edenl区里还有150MB的对象存活,Survivor区的内存只有100MB,此时无法放入S区,则将这些对象直接转移到老年代中去
老年代空间分配担保规则
存在一个问题:如果新生代里有大量对象存活下来,确实是Survivor区装不下,必须转移到老年代中去,但是如果老年代里空间也不够放这些对象,此时JVM会怎么操作?
首先,在执行任何一次Minor GC之前,JVM会先检查一下老年代可用的可用内存空间,是否大于新生代所有对象的总大小。
**为啥检查这个呢?**因为最极端的情况下,可能新生代Minor GC过后,所有对象都存活下来了,那岂不是新生代所有对象全部要进入老年代?
如果发现老年代的内存大小是大于新生代所有对象的,就可以放心大胆的对新生代发起一次MinorGC,因为即使Minor GC之后所有对象存活,Survivor区放不下了,也可以转移到老年代去。
但 是假如执行Minor GC之前,发现老年代的可用内存已经小于了新生代的全部对象大小了
那么这个时候是不是有可能在Minor GC之后新生代的对象全部存活下来,然后全部需要转移到老年代去,但是老年代空间又不够?
所以假如Minor GC之前,发现老年代的可用内存已经小于了新生代的全部对象大小了,就会看一个“-XX:-
**HandlePromotionFailure”**的参数是否设置
下一步判断,就是看看老年代的内存大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小
举个例子,之前每次Minor GC后,平均都有10MB左右的对象会进入老年代,那么此时老年代可用内存大于10MB。
这就说明,很可能这次Minor GC过后也是差不多10MB左右的对象会进入老年代,此时老年代空间是够的
如果上面那个步骤判断失败了,或者是“-XX:-HandlePromotionFailure”参数没设置,此时就会直接触发一次“Full GC”,就是对老年代进行垃圾回收,尽量腾出来一些内存空间,然后再执行Minor GC
如果上面两个步骤都判断成功了,那么就是说可以冒点风险尝试一下Minor GC。此时进行Minor GC有几种可能
-
第一种可能,Minor GC过后,剩余的存活对象的大小,是小于Survivor区的大小的,那么此时存活对象进入Survivor区域即可。
-
第二种可能,Minor GC过后,剩余的存活对象的大小,是大于 Survivor区域的大小,但是是小于老年代可用内存大小的,此时就直接进入老年代即可。
-
第三种可能,很不幸,Minor GC过后,剩余的存活对象的大小,大于了Survivor区域的大小,也大于了老年代可用内存的大小。此时老年代都放不下这些存活对象了,就会发生“Handle Promotion Failure”的情况,这个时候就会触发一次“Full GC”。
Full GC就是对老年代进行垃圾回收,同时也一般会对新生代进行垃圾回收。
因为这个时候必须得把老年代里的没人引用的对象给回收掉,然后才可能让Minor GC过后剩余的存活对象进入老年代里面。
如果要是Full GC过后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致所谓的
“OOM”内存溢出
上面的文字描述比较绕,可参考下面的流程图查看:
老年代垃圾回收算法
触发垃圾回收的时机
-
在Minor GC之前,一通检查发现很可能Minor GC之后要进入老年代的对象太多了,老年代放不下,此时需
要提前触发Full GC然后再带着进行Minor GC
-
在Minor GC之后,发现剩余对象太多放入老年代都放不下
老年代采取的是标记整理算法
如果系统频繁出现老年代的Full GC垃圾回收,会导致系统性能被严重影响,出现频繁卡顿的情况