不论是BAT面试,还是工作实际中的JVM调优以及参数设置,或者内存溢出检测等,都须要波及到Java虚拟机的内存模型、内存调配,以及回收算法机制等,这些都是必考、必会技能。

JVM内存模型

JVM内存模型能够分为两个局部,如下图所示,堆和办法区是所有线程共有的,而虚拟机栈,本地办法栈和程序计数器则是线程公有的。


1. 堆(Heap)
堆内存是所有线程共有的,能够分为两个局部:年老代和老年代。下图中的Perm代表的是永恒代,然而留神永恒代并不属于堆内存中的一部分,同时jdk1.8之后永恒代也将被移除。


堆是java虚拟机所治理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,该内存区域寄存了对象实例及数组(但不是所有的对象实例都在堆中)。

其大小通过-Xms(最小值)和-Xmx(最大值)参数设置(最大最小值都要小于1G),前者为启动时申请的最小内存,默认为操作系统物理内存的1/64,后者为JVM可申请的最大内存,默认为物理内存的1/4,默认当空余堆内存小于40%时,JVM会增大堆内存到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小堆内存的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,当然为了防止在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。堆内存 = 新生代+老生代+长久代。

在咱们垃圾回收的时候,咱们往往将堆内存分成新生代和老生代(大小比例1:2),新生代中由Eden和Survivor0,Survivor1组成,三者的比例是8:1:1,新生代的回收机制采纳复制算法,在Minor GC的时候,咱们都留一个存活区用来寄存存活的对象,真正进行的区域是Eden+其中一个存活区,当咱们的对象时长超过肯定年龄时(默认15,能够通过参数设置),将会把对象放入老生代,当然大的对象会间接进入老生代。老生代采纳的回收算法是标记整顿算法。

2. 办法区(Method Area)
办法区也称”永恒代“,它用于存储虚拟机加载的类信息、常量、动态变量、是各个线程共享的内存区域。默认最小值为16MB,最大值为64MB(64位JVM因为指针收缩,默认是85M),能够通过-XX:PermSize 和 -XX:MaxPermSize 参数限度办法区的大小。

它是一片间断的堆空间,永恒代的垃圾收集是和老年代(old generation)捆绑在一起的,因而无论谁满了,都会触发永恒代和老年代的垃圾收集。不过,一个显著的问题是,当JVM加载的类信息容量超过了参数-XX:MaxPermSize设定的值时,利用将会报OOM的谬误。参数是通过-XX:PermSize和-XX:MaxPermSize来设定的。

3.虚拟机栈(JVM Stack)
形容的是java办法执行的内存模型:每个办法被执行的时候都会创立一个”栈帧”,用于存储局部变量表(包含参数)、操作栈、办法进口等信息。每个办法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

申明周期与线程雷同,是线程公有的。栈帧由三局部组成:局部变量区、操作数栈、帧数据区。局部变量区被组织为以一个字长为单位、从0开始计数的数组,和局部变量区一样,操作数栈也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来拜访的,而是通过入栈和出栈来拜访的,能够看作为长期数据的存储区域。

除了局部变量区和操作数栈外,java栈帧还须要一些数据来反对常量池解析、失常办法返回以及异样派发机制。这些数据都保留在java栈帧的帧数据区中。

局部变量表: 寄存了编译器可知的各种根本数据类型、对象援用(援用指针,并非对象自身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。

局部变量表所需的内存空间在编译期间实现调配,当进入一个办法时,这个办法须要在栈帧中调配多大的局部变量是齐全确定的,在运行期间栈帧不会扭转局部变量表的大小空间。

4.本地办法栈(Native Stack)
与虚拟机栈根本相似,区别在于虚拟机栈为虚拟机执行的java办法服务,而本地办法栈则是为Native办法服务。(栈的空间大小远远小于堆)

5.程序计数器(PC Register)
是最小的一块内存区域,它的作用是以后线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过扭转这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、异样解决、线程复原等根底性能都须要依赖计数器实现。

6.间接内存
间接内存并不是虚拟机内存的一部分,也不是Java虚拟机标准中定义的内存区域。jdk1.4中新退出的NIO,引入了通道与缓冲区的IO形式,它能够调用Native办法间接调配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小.

JVM垃圾回收算法

1.标记革除

原理:

从根汇合节点进行扫描,标记出所有的存活对象,最初扫描整个内存空间并革除没有标记的对象(即死亡对象)
实用场合:

存活对象较多的状况下比拟高效
实用于年轻代(即旧生代)
毛病:

标记革除算法带来的一个问题是会存在大量的空间碎片,因为回收后的空间是不间断的,这样给大对象分配内存的时候可能会提前触发full gc。

2.复制算法

原理:

从根汇合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)下来,之后将原来的那一块儿内存(图中上边的那一块儿内存)全副回收掉
实用场合:

存活对象较少的状况下比拟高效
扫描了整个空间一次(标记存活对象并复制挪动)
实用于年老代(即新生代):基本上98%的对象是”朝生夕死”的,存活下来的会很少
毛病:

须要一块儿空的内存空间
须要复制挪动对象

3.标记整顿

原理:

从根汇合节点进行扫描,标记出所有的存活对象,最初扫描整个内存空间并革除没有标记的对象(即死亡对象)(能够发现前边这些就是标记-革除算法的原理),革除完之后,将所有的存活对象左移到一起。
实用场合:

用于年轻代(即旧生代)
毛病:

须要挪动对象,若对象十分多而且标记回收后的内存十分不残缺,可能挪动这个动作也会消耗肯定工夫
扫描了整个空间两次(第一次:标记存活对象;第二次:革除没有标记的对象)
长处:

不会产生内存碎片

4.分代收集算法
以后商业虚拟机的垃圾收集都采纳“分代收集”(Generational Collection)算法,这种算法并没有什么新的思维,只是依据对象存活周期的不同将内存划分为几块。个别是把Java堆分为新生代和老年代,这样就能够依据各个年代的特点采纳最适当的收集算法。

专门钻研表明,新生代中的对象98%是“朝生夕死”的,所以并不需要依照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次应用Eden和其中一块Survivor[1]。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最初清理掉Eden和方才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“节约”。当然,98%的对象可回收只是个别场景下的数据,咱们没有方法保障每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,须要依赖其余内存(这里指老年代)进行调配担保(Handle Promotion)。

在新生代中,每次垃圾收集时都发现有少量对象死去,只有大量存活,那就选用复制算法,只须要付出大量存活对象的复制老本就能够实现收集。而老年代中因为对象存活率高、没有额定空间对它进行调配担保,就必须应用“标记—清理”或者“标记—整顿”算法来进行回收。

垃圾回收器

1.Serial收集器
Serial收集器是最古老的收集器,它的毛病是当Serial收集器想进行垃圾回收的时候,必须暂停用户的所有过程,即stop the world。到当初为止,它仍然是虚拟机运行在client模式下的默认新生代收集器,与其余收集器相比,对于限定在单个CPU的运行环境来说,Serial收集器因为没有线程交互的开销,分心做垃圾回收天然能够取得最高的单线程收集效率。

2.ParNew收集器
ParNew收集器是Serial收集器新生代的多线程实现,留神在进行垃圾回收的时候仍然会stop the world,只是相比拟Serial收集器而言它会运行多条过程进行垃圾回收。

ParNew收集器在单CPU的环境中相对不会有比Serial收集器更好的成果,甚至因为存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百的保障能超过Serial收集器。当然,随着能够应用的CPU的数量减少,它对于GC时系统资源的利用还是很有益处的。它默认开启的收集线程数与CPU的数量雷同,在CPU十分多(譬如32个,当初CPU动辄4核加超线程,服务器超过32个逻辑CPU的状况越来越多了)的环境下,能够应用-XX:ParallelGCThreads参数来限度垃圾收集的线程数。

3.Parallel Scavenge收集器
Parallel是采纳复制算法的多线程新生代垃圾回收器,仿佛和ParNew收集器有很多的类似的中央。然而Parallel Scanvenge收集器的一个特点是它所关注的指标是吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的工夫与CPU总耗费工夫的比值,即吞吐量=运行用户代码工夫 / (运行用户代码工夫 + 垃圾收集工夫)。进展工夫越短就越适宜须要与用户交互的程序,良好的响应速度可能晋升用户的体验;而高吞吐量则能够最高效率地利用CPU工夫,尽快地实现程序的运算工作,次要适宜在后盾运算而不须要太多交互的工作。

4.CMS收集器
CMS(Concurrent Mark Swep)收集器是一个比拟重要的回收器,当初利用十分宽泛,咱们重点来看一下,CMS一种获取最短回收进展工夫为指标的收集器,这使得它很适宜用于和用户交互的业务。从名字(Mark Swep)就能够看出,CMS收集器是基于标记革除算法实现的。它的收集过程分为四个步骤:

  • 初始标记(initial mark)
  • 并发标记(concurrent mark)
  • 从新标记(remark)
  • 并发革除(concurrent sweep)

留神初始标记和从新标记还是会stop the world,然而在消耗工夫更长的并发标记和并发革除两个阶段都能够和用户过程同时工作。

不过因为CMS收集器是基于标记革除算法实现的,会导致有大量的空间碎片产生,在为大对象分配内存的时候,往往会呈现老年代还有很大的空间残余,然而无奈找到足够大的间断空间来调配以后对象,不得不提前开启一次Full GC。

为了解决这个问题,CMS收集器默认提供了一个-XX:+UseCMSCompactAtFullCollection收集开关参数(默认就是开启的),用于在CMS收集器进行FullGC完开启内存碎片的合并整顿过程,内存整理的过程是无奈并发的,这样内存碎片问题倒是没有了,不过进展工夫不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction参数用于设置执行多少次不压缩的FULL GC后跟着来一次带压缩的(默认值为0,示意每次进入Full GC时都进行碎片整顿)。

可怜的是,它作为老年代的收集器,却无奈与jdk1.4中曾经存在的新生代收集器Parallel Scavenge配合工作,所以在jdk1.5中应用cms来收集老年代的时候,新生代只能抉择ParNew或Serial收集器中的一个。

ParNew收集器是应用-XX:+UseConcMarkSweepGC选项启用CMS收集器之后的默认新生代收集器,也能够应用-XX:+UseParNewGC选项来强制指定它。

5.G1收集器
G1收集器是一款面向服务端利用的垃圾收集器。HotSpot团队赋予它的使命是在将来替换掉JDK1.5中公布的CMS收集器。与其余GC收集器相比,G1具备如下特点:

并行与并发:G1能更充沛的利用CPU,多核环境下的硬件劣势来缩短stop the world的进展工夫。
分代收集:和其余收集器一样,分代的概念在G1中仍然存在,不过G1不须要其余的垃圾回收器的配合就能够单独治理整个GC堆。
空间整合:G1收集器有利于程序长时间运行,调配大对象时不会无奈失去间断的空间而提前触发一次GC。
可预测的非进展:这是G1绝对于CMS的另一大劣势,升高进展工夫是G1和CMS独特的关注点,能让使用者明确指定在一个长度为M毫秒的工夫片段内,耗费在垃圾收集上的工夫不得超过N毫秒。
在应用G1收集器时,Java堆的内存布局和其余收集器有很大的差异,它将这个Java堆分为多个大小相等的独立区域,尽管还保留新生代和老年代的概念,然而新生代和老年代不再是物理隔离的了,它们都是一部分Region(不须要间断)的汇合。

尽管G1看起来有很多长处,实际上CMS还是支流。

与GC相干的罕用参数
除了下面提及的一些参数,上面补充一些和GC相干的罕用参数:

-Xmx: 设置堆内存的最大值。
-Xms: 设置堆内存的初始值。
-Xmn: 设置新生代的大小。
-Xss: 设置栈的大小。
-PretenureSizeThreshold: 间接降职到老年代的对象大小,设置这个参数后,大于这个参数的对象将间接在老年代调配。
-MaxTenuringThrehold: 降职到老年代的对象年龄。每个对象在保持过一次Minor GC之后,年龄就会加1,当超过这个参数值时就进入老年代。
-UseAdaptiveSizePolicy: 在这种模式下,新生代的大小、eden 和 survivor 的比例、降职老年代的对象年龄等参数会被主动调整,以达到在堆大小、吞吐量和进展工夫之间的平衡点。在手工调优比拟艰难的场合,能够间接应用这种自适应的形式,仅指定虚拟机的最大堆、指标的吞吐量 (GCTimeRatio) 和进展工夫 (MaxGCPauseMills),让虚拟机本人实现调优工作。
-SurvivorRattio: 新生代Eden区域与Survivor区域的容量比值,默认为8,代表Eden: Suvivor= 8: 1。
-XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常状况下能够和 CPU 数量相等。但在 CPU 数量比拟多的状况下,设置绝对较小的数值也是正当的。
-XX:MaxGCPauseMills:设置最大垃圾收集进展工夫。它的值是一个大于 0 的整数。收集器在工作时,会调整 Java 堆大小或者其余一些参数,尽可能地把进展工夫管制在 MaxGCPauseMills 以内。
-XX:GCTimeRatio:设置吞吐量大小,它的值是一个 0-100 之间的整数。假如 GCTimeRatio 的值为 n,那么零碎将破费不超过 1/(1+n) 的工夫用于垃圾收集。

对于作者:mikechen,十余年BAT架构教训,资深技术专家,曾任职阿里、淘宝、百度。

关注集体公众号:mikechen的互联网架构,十余年BAT架构教训倾囊相授!
在公众号菜单栏对话框回复【架构】关键词,即可查看我原创的300期+BAT架构技术系列文章与1000+大厂面试题答案合集