目录
一、JVM分类
目前主流的 Java 虚拟机有哪些? - 知乎Wikipedia那个Comparison of Java virtual machines页面给JVM实现分得还挺细。利益相关:Azul System的员…https://www.zhihu.com/question/29265430/answer/43818804JVM不同,分区、对象分代、垃圾回收机制等都会略有不同,下文以最主流的HotSpot VM为例整理。
二、JVM中对象分代
1、为什么要分代?
优化垃圾回收机制的性能,对不同代实现分治。(垃圾回收机制后续会讲解)
2、对象分代
虚拟机中共划分为三个代:年轻代(Young Generation)、老年代(Old Generation)和持久代(Permanent Generation)。垃圾回收机制主要是对年轻代和老年代进行回收。
- 年轻代:主要是用来存放新生的对象。所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。(特点:回收频繁)
- 老年代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。(特点:回收频率低)
- 持久代:用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响。
3、持久代存在哪里?
有的说持久代是对 方法区 的实现;有的说 堆 被分为了:年轻代、老年代和持久代。所以持久代到底存放在哪里呢?
持久代在物理层面,是在堆空间的;而在逻辑层面,是在方法区的;因为方法区其实物理上也是在堆中的,但是由于功能和作用的区别,逻辑上方法区是独立于堆的。当然jdk8以后就没有方法区了,只有元空间,所以jdk8以后,持久代逻辑上是在元空间的。
4、持久代和元空间(Metaspace)的区别
Java8内存模型—永久代(PermGen)和元空间(Metaspace) - liuxiaopeng - 博客园一、JVM 内存模型 根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。 1、虚拟机栈:每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈https://www.cnblogs.com/paddix/p/5309550.html移除持久代的工作从JDK1.7就开始了,JDK1.7中,存储在持久代的部分数据就已经转移到了Java Heap或者是Native Heap,但持久代仍存在于JDK1.7中,并没完全移除。到JDK1.8时,HotSpot 已经完全没有“PermGen space”这个区间了,取而代之是一个叫做Metaspace(元空间)的东西。
也就是JDK1.8之前的持久代溢出异常 "java.lang.OutOfMemoryError: PermGen space",会被堆内存溢出 "java.lang.OutOfMemoryError: Java heap space"取代。
元空间的本质和持久代类似,都是对JVM规范中方法区的实现。不过元空间与持久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小。
5、为什么会出现元空间?
- 字符串存在持久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于持久代的大小指定比较困难,太小容易出现持久代溢出,太大则容易导致老年代溢出(占用了老年代的空间)。
- 持久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
三、Java垃圾回收机制
1、如何确定“垃圾”?
对于堆中的对象,有如下两种方式:
- 引用计数器法:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用。缺陷:两个对象互相引用着对方,导致它们的引用计数都不为零,引用计数算法也就无法回收它们。
- 可达性分析算法:该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径(引用链),则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。这是Java使用的判断可回收对象的算法。
对于方法区(持久代),垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型,
- 对于常量:某个常量不再被对象引用,则被判定为“废弃的常量”,在必要时会被回收。
- 对于类:要判定为不再使用的类,条件比较苛刻,需要同时满足下面三个条件:
- 该类所有的实例(堆中的)都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
- 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
当然,前文已经讲过,对方法区(持久代)的回收效率低,对于该区域的垃圾回收情况很少。
2、经典的垃圾回收算法
- Mark-Sweep(标记-清除)算法:会产生内存碎片
- Copying(复制)算法:不会产生内存碎片
- Mark-Compact(标记-整理)算法:不会产生内存碎片
- Generational Collection(分代收集)算法:新生代用 复制算法 和 老年代用 标记整理算法
算法性能对比:
- 效率:复制算法>标记/整理算法>标记/清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
- 内存整齐度:复制算法=标记/整理算法>标记/清除算法。
- 内存利用率:标记/整理算法=标记/清除算法>复制算法。
3、垃圾回收过程
新创建的对象一般会被分配在新生代中,常用的新生代的垃圾回收器是 ParNew 垃圾回收器,它按照 8:1:1 将新生代分成 Eden 区,以及两个 Survivor 区。某一时刻,创建的对象将 Eden 区全部挤满,这个对象就是挤满新生代的最后一个对象。此时,Minor GC 就触发了。
垃圾回收器内容补充:
- ParNew:是Serial收集器的多线程版本,使用多个线程进行垃圾收集。
- Serial/Serial Old:Serial/Serial Old收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。
- 还有一些其他收集器,详见上面链接。
Minor GC 前,检查工作
在正式 Minor GC 前,JVM 会先检查新生代中对象,是比老年代中剩余空间大还是小。为什么要做这样的检查呢?原因很简单,假如 Minor GC 之后 Survivor 区放不下剩余对象,这些对象就要进入到老年代,所以要提前检查老年代是不是够用。这样就有两种情况:
- 老年代剩余空间大于新生代中的对象大小,那就直接Minor GC,GC完survivor不够放,老年代也绝对够放;
- 老年代剩余空间小于新生代中的对象大小,这个时候就要查看是否启用了“老年代空间分配担保规则”,具体来说就是看 -XX:-HandlePromotionFailure 参数是否设置了,未启用的话直接此时进行Full GC。
老年代空间分配担保规则:如果老年代中剩余空间大小,大于历次 Minor GC 之后剩余对象的大小,那就允许进行 Minor GC。因为从概率上来说,以前的放的下,这次的也应该放的下。那就有两种情况:
- 老年代中剩余空间大小,大于历次Minor GC之后剩余对象的大小,进行Minor GC;
- 老年代中剩余空间大小,小于历次Minor GC之后剩余对象的大小,进行Full GC,把老年代空出来再检查。
开启老年代空间分配担保规则只能说是大概率上来说,Minor GC 剩余后的对象够放到老年代,所以当然也会有万一,Minor GC 后会有这样三种情况:
- Minor GC 之后的对象足够放到 Survivor 区,皆大欢喜,GC 结束;
- Minor GC 之后的对象不够放到 Survivor 区,接着进入到老年代,老年代能放下,那也可以,GC 结束;
- Minor GC 之后的对象不够放到 Survivor 区,老年代也放不下,那就只能 Full GC。
前面都是成功 GC 的例子,还有 3 中情况,会导致 GC 失败,报 OOM(OutOfMemoryError):
- 紧接上一节 Full GC 之后,老年代任然放不下剩余对象,就只能 OOM;
- 未开启老年代分配担保机制,且一次 Full GC 后,老年代任然放不下剩余对象,也只能 OOM;
- 开启老年代分配担保机制,但是担保不通过,一次 Full GC 后,老年代任然放不下剩余对象,也是能 OOM。
经过检查后,具体 Minor GC 处理过程
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向,对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置,默认15)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为“To”的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
注:
一般来说,大对象会被直接分配到老年代,所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组,比如:byte[] data = new byte[4*1024*1024]。
4、Minor GC 和 Full GC
Minor GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Minor GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区,然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。
Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:
- 年老代被写满
- 持久代被写满(元空间出现后,持久代被写满的可能降低)
- System.gc()被显示调用
- 上一次GC之后Heap的各域分配策略动态变化
Minor GC 和 Full GC 有什么区别?
Minor GC:收集生命周期短的区域(Young area)。
Full GC(或Major GC):收集生命周期短的区域(Young area)和生命周期比较长的区域(Old area),对整个堆进行垃圾收集。
补充:
面试中常会问到,哪些收集器回收年轻代,哪些回收老年代?
参考