关于JVM,也许你听过这些术语:年轻代(新生代)、老年代、永久代、minor gc(young gc)、major gc、full gc
不要急,先上图,这是jvm 堆内存结构图
仔细的你发现了 图中有些分数8/10和1/10,这是默认配置下各个代内存分配比例。
举个栗子:
假如总heap max分配1200M,那么年轻代占用1/3就是400M,老年代占2/3就是800M。
Eden占年轻代的8/10就是320M。Survivor占年轻代的2/10就是80M,from和to各占40M。
年轻代
也叫新生代,顾名思义,主要是用来存放新生的对象。新生代又细分为 Eden区、SurvivorFrom区、SurvivorTo区。
新创建的对象都会被分配到Eden区(如果该对象占用内存非常大,则直接分配到老年代区), 当Eden区内存不够的时候就会触发MinorGC(Survivor满不会引发MinorGC,而是将对象移动到老年代中),
在Minor GC开始的时候,对象只会存在于Eden区和Survivor from区,Survivor to区是空的。
Minor GC操作后,Eden区如果仍然存活(判断的标准是被引用了,通过GC root进行可达性判断)的对象,将会被移到Survivor To区。而From区中,对象在Survivor区中每熬过一次Minor GC,年龄就会+1岁,当年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置,默认是15)的对象会被移动到年老代中,否则对象会被复制到“To”区。经过这次GC后,Eden区和From区已经被清空。
“From”区和“To”区互换角色,原Survivor To成为下一次GC时的Survivor From区, 总之,GC后,都会保证Survivor To区是空的。
奇怪为什么有 From和To,2块区域?
这就要说到新生代Minor GC的算法了:复制算法
把内存区域分为两块,每次使用一块,GC的时候把一块中的内容移动到另一块中,原始内存中的对象就可以被回收了,
优点是避免内存碎片。
老年代
随着Minor GC的持续进行,老年代中对象也会持续增长,导致老年代的空间也会不够用,最终会执行Major GC(MajorGC 的速度比 Minor GC 慢很多很多,据说10倍左右)。Major GC使用的算法是:标记清除(回收)算法或者标记压缩算法。
标记清除(回收):1. 首先会从GC root进行遍历,把可达对象(存过的对象)打标记
2. 再从GC root二次遍历,将没有被打上标记的对象清除掉。
优点:老年代对象一般是比较稳定的,相比复制算法,不需要复制大量对象。之所以将所有对象扫描2次,看似比较消耗时间,其实不然,是节省了时间。举个栗子,数组 1,2,3,4,5,6。删除2,3,4,如果每次删除一个数字,那么5,6要移动3次,如果删除1次,那么5,6只需移动1次。
缺点:这种方式需要中断其他线程(STW),相比复制算法,可能产生内存碎片。
标记压缩:和标记清除算法基本相同,不同的就是,在清除完成之后,会把存活的对象向内存的一边进行压缩,这样就可以解决内存碎片问题。
当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。
永久代(元空间)
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间,Metaspace)的区域所取代。
值得注意的是:元空间并不在虚拟机中,而是使用本地内存(之前,永久代是在jvm中)。这样,解决了以前永久代的OOM问题,元数据和class对象存在永久代中,容易出现性能问题和内存溢出,毕竟是和老年代共享堆空间。java8后,永久代升级为元空间独立后,也降低了老年代GC的复杂度。
Visual GC插件
自己不妨写点代码,测试下上面说过的GC过程,通过Visual GC插件
Java VisualVM安装Visual GC插件
https://blog.csdn.net/yujianping_123/article/details/99549194
上面说到了minor gc 和major gc,那么看下full gc
Full GC
是清理整个堆空间—包括年轻代和老年代。
什么时候触发:
1. 调用System.gc
2. 方法区空间不足
2.老年代空间不足,包括:
- 新创建的对象都会被分配到Eden区,如果该对象占用内存非常大,则直接分配到老年代区,此时老年代空间不足
- 做minor gc操作前,发现要移动的空间(Eden区、From区向To区复制时,To区的内存空间不足)比老年代剩余空间要大,则触发full gc,而不是minor gc
- 等等
GC优化的本质,也是为什么分代的原因:减少GC次数和GC时间,避免全区扫描。