
1、前言
对于Java虚拟机在内存分配与回收的学习,若是读者大学时代没有偷懒的话,操做系统和计算机组成原理这两门功课学的比较好的话,理解起来JVM是比较容易的,只要底子还在,不少东西均可以举一反三。javascript
1.1 计算机==>操做系统==>JVM
JVM全称为Java Virtual Machine,译为Java虚拟机,读者会问,虚拟机虚拟的是谁呢?即虚拟是对什么东西的虚拟,即实体是什么,是如何虚拟的?下面让咱们来看看“虚拟与实体”。java
一图解析计算机、操做系统、JVM三者关系linux

1.1.1 虚拟与实体(对上图的结构层次分析)
JVM之因此称为之虚拟机,是由于它是实现了计算机的虚拟化。下表展现JVM位于操做系统堆内存中,分别实现的了对操做系统和计算机的虚拟化。程序员

操做系统栈对应JVM栈,操做系统堆对应JVM堆,计算机磁盘对应JVM方法区,存放字节码对象,计算机PC寄存器对应JVM程序计数器(注意:计算机PC寄存器是下一条指令地址,JVM程序计数器是当前指令的地址),惟一不一样的是,整个计算机(内存(操做系统栈+操做系统堆) +磁盘+PC计数器)对应JVM占用的整个内存(JVM栈+JVM堆+JVM方法区+JVM程序计数器)。web
1.1.2 Java程序执行(对上图的箭头流程分析)
上图中不只是结构图,展现JVM的虚拟和实体的关系,也是一个流程图,上图中的箭头展现JVM对一个对象的编译执行。算法
程序员写好的类加载到虚拟机执行的过程是:当一个classLoder启动的时候,classLoader的生存地点在JVM中的堆,首先它会去主机硬盘上将Test.class装载到JVM的方法区,方法区中的这个字节文件会被虚拟机拿来new Test字节码(),而后在堆内存生成了一个Test字节码的对象,最后Test字节码这个内存文件有两个引用一个指向Test的class对象,一个指向加载本身的classLoader。整个过程上图用箭头表示,这里作说明。数组
就像本文开始时说过的,有了计算机组成原理和操做系统两门课的底子,学起JVM的时候会容易许多,由于JVM本质上就是对计算机和操做系统的虚拟,就是一个虚拟机。tomcat
Java正是有了这一套虚拟机的支持,才成就了跨平台(一次编译,永久运行)的优点。安全
这样一来,前言部分咱们成功引入JVM,接下来,本文要讲述的重点是JVM自动内存管理,先给出总述:服务器
JVM自动内存管理=分配内存(指给对象分配内存)+回收内存(回收分配给对象的内存)
上面公式告诉咱们,JVM自动内存管理分为两块,分配内存和回收内存
2、JVM内存空间与参数设置
2.1 运行时数据区
JVM在执行Java程序的过程当中会把它所管理的内存划分为若干个不一样的运行时数据区域。这些运行时数据区包括方法区、堆、虚拟栈、本地方法栈、程序计数器,如图:

让咱们一步步介绍,对于运行时数据区,不少博客都是使用顺序介绍的方式,不利于读者对比比较学习,这里笔者以表格的方式呈现:

让咱们对上表继续深刻,讲述上表中的StackOverflowError和OutOfMemoryError。
2.2 关于StackOverflowError和OutOfMemoryError
2.2.1 StackOverflowError
运行时数据区中,抛出栈溢出的就是虚拟机栈和本地方法栈,
产生缘由:线程请求的栈深度大于虚拟机所容许的深度。由于JVM栈深度是有限的而不是无限的,可是通常的方法调用都不会超过JVM的栈深度,若是出现栈溢出,基本上都是代码层面的缘由,如递归调用没有设置出口或者无限循环调用。
解决方法:程序员检查代码是否有无限循环便可。
2.2.2 OutOfMemoryError
容易发生OutOfMemoryError内存溢出问题的内存空间包括:Permanent Generation space和Heap space。
一、第一种java.lang.OutOfMemoryError:PermGen space(方法区抛出)
产生缘由:发生这种问题的原意是程序中使用了大量的jar或class,使java虚拟机装载类的空间不够,与Permanent Generation space有关。因此,根本缘由在于jar或class太多,方法区堆溢出,则解决方法有两个种,要么增大方法区,要么减小jar、class文件,且看解决方法。
解决方法:
1. 从增大方法区方面入手:
增长java虚拟机中的XX:PermSize和XX:MaxPermSize参数的大小,其中XX:PermSize是初始永久保存区域大小,XX:MaxPermSize是最大永久保存区域大小。
如web应用中,针对tomcat应用服务器,在catalina.sh 或catalina.bat文件中一系列环境变量名说明结束处增长一行:
JAVA_OPTS=" -XX:PermSize=64M -XX:MaxPermSize=128m"
可有效解决web项目的tomcat服务器常常宕机的问题。
2. 从减小jar、class文件入手:
清理应用程序中web-inf/lib下的jar,若是tomcat部署了多个应用,不少应用都使用了相同的jar,能够将共同的jar移到tomcat共同的lib下,减小类的重复加载。
二、第二种OutOfMemoryError:Java heap space(堆抛出)
产生缘由:发生这种问题的缘由是java虚拟机建立的对象太多,在进行垃圾回收之间,虚拟机分配的到堆内存空间已经用满了,与Heap space有关。因此,根本缘由在于对象实例太多,Java堆溢出,则解决方法有两个种,要么增大堆内存,要么减小对象示例,且看解决方法。
解决方法:
1.从增大堆内存方面入手:
增长Java虚拟机中Xms(初始堆大小)和Xmx(最大堆大小)参数的大小。如:set JAVA_OPTS= -Xms256m -Xmx1024m
2.从减小对象实例入手:
通常来讲,正常程序的对象,堆内存时绝对够用的,出现堆内存溢出通常是死循环中建立大量对象,检查程序,看是否有死循环或没必要要地重复建立大量对象。找到缘由后,修改程序和算法。
三、第三种OutOfMemoryError:unable to create new native thread(Java虚拟机栈、本地方法栈抛出)
产生缘由:这个异常问题本质缘由是咱们建立了太多的线程,而能建立的线程数是有限制的,致使了异常的发生。能建立的线程数的具体计算公式以下:
(MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) = Number of threads
注意:MaxProcessMemory 表示一个进程的最大内存,JVMMemory 表示JVM内存, ReservedOsMemory 表示保留的操做系统内存,ThreadStackSize 表示线程栈的大小。
在java语言里, 当你建立一个线程的时候,虚拟机会在JVM内存建立一个Thread对象同时建立一个操做系统线程,而这个系统线程的内存用的不是JVMMemory,而是系统中剩下的内存(MaxProcessMemory - JVMMemory - ReservedOsMemory)。由公式得出结论:你给JVM内存越多,那么你能建立的线程越少,越容易发生 java.lang.OutOfMemoryError: unable to create new native thread。
解决方法:
1.若是程序中有bug,致使建立大量不须要的线程或者线程没有及时回收,那么必须解决这个bug,修改参数是不能解决问题的。
2.若是程序确实须要大量的线程,现有的设置不能达到要求,那么能够经过修改MaxProcessMemory,JVMMemory,ThreadStackSize这三个因素,来增长能建立的线程数:MaxProcessMemory 表示使用64位操做系统,VMMemory 表示减小 JVMMemory 的分配
ThreadStackSize 表示减少单个线程的栈大小。
2.3 JVM堆内存和非堆内存
2.3.1 堆内存和非堆内存
JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)。
年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,而后根据GC算法回收。
非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,好比类的元数据、方法、常量、属性等。
在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上相似,都是方法区的实现,他们最大区别是:永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制。在后面的实践中,由于笔者使用的是JDK8,因此打印出的GC日志里面就有MetaSpace。
2.3.2 JVM堆内部构型(新生代和老年代)
Jdk8中已经去掉永久区,这里为了与时俱进,再也不赘余。

上图演示Java堆内存空间,分为新生代和老年代,分别占Java堆1/3和2/3的空间,新生代中又分为Eden区、Survivor0区、Survivor1区,分别占新生代8/十、1/十、1/10空间。
问题1:什么是Java堆?
回答1:JVM规范中说到:”全部的对象实例以及数组都要在堆上分配”。Java堆是垃圾回收器管理的主要区域,百分之九十九的垃圾回收发生在Java堆,另外百分之一发生在方法区,所以又称之为”GC堆”。根据JVM规范规定的内容,Java堆能够处于物理上不连续的内存空间中。
问题2:为何Java堆要分为新生代和老年代?
回答2:当前JVM对于堆的垃圾回收,采用分代收集的策略。根据堆中对象的存活周期将堆内存分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少许存活。而老年代中存放的对象存活率高。这样划分的目的是为了使 JVM 可以更好的管理堆内存中的对象,包括内存的分配以及回收。
问题3:为何新生代要分为Eden区、Survivor0区、Survivor1区?
回答3:这是结构与策略相适应的原则,新生代垃圾收集使用的是复制算法(一种垃圾收集算法,Serial收集器、ParNew收集器、Parallel scavenge收集器都是用这种算法),复制算法能够很好的解决垃圾收集的内存碎片问题,可是有一个自然的缺陷,就是要牺牲一半的内存(即任意时刻只有一半内存用于工做),这对于宝贵的内存资源来讲是极度奢侈的。新生代在使用复制算法做为其垃圾收集算法的时候,对其作了优化,拿出2/10的新生代的内存做为交换区,称为Survivor0区和Survivor1区。
值得注意的是,有的博客上称为From Survivor Space和To Survivor Space,这样阐述也是对的,可是容易对初学者造成误导,由于在复制算法中,复制是双向的,没有固定的From和To,这一次是由这一边到另外一边,下次就是从另外一边到这一边,使用From Survivor Space和To Survivor Space容易让后来学习者误觉得复制只能从一边到另外一边,固然有的博客中会附加无论从哪边到哪边,起始就是From,终点就是To,即From Survivor Space和To Survivor Space所对应的区循环对调,可是读者不必定想的明白。因此笔者这里使用Survivor0、Survivor1,减小误解。
因此说,新生代在结构上分为Eden区、Survivor0区、Survivor1区,是与其使用的垃圾收集算法(复制算法)相适应的结果。
问题4:关于永久区Permanent Space?
回答4:因为Jdk8中取消了永久区Permanent Space,本文为与时俱进,再也不讲述Permanent Space。
2.4 JVM堆参数设置
这些都是和堆内存分配有关的参数,因此咱们放在第二部分了,和垃圾收集器有关的参数放在第四部分。
举例:java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m
2.4.1 JVM重要参数
由于整个堆大小=年轻代大小(新生代大小) + 年老代大小 + 持久代大小,
-Xmn2g:表示年轻代大小为2G。持久代通常固定大小为64m,因此增大年轻代后,将会减少年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。这里设置为4,表示年轻代与年老代所占比值为1:4,又由于上面设置年轻代为2G,则老年代大小为8G
-XX:SurvivorRatio=8:设置年轻代中Eden区与Survivor区的大小比值。这里设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
则Eden:Survivor0:Survivor1=8:1:1
-XX:MaxPermSize=16m:设置持久代大小为16m。
全部整个堆大小=年轻代大小 + 年老代大小 + 持久代大小= 2G+ 8G+ 16M=10G+6M=10246MB
2.4.2 JVM其余参数
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM促使内存为3550m,此值能够设置与-Xmx相同。
-Xss128k:设置每一个线程的堆栈大小。JDK5.0之后每一个线程堆栈大小为1M,之前每一个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减少这个值能生成更多的线程。可是操做系统对一个进程内的线程数仍是有限制的,不能无限生成,经验值在3000~5000左右。
关于为何-xmx与-xms的大小设置为同样的?
首先,在Java堆内存分配中,-xmx用于指定JVM最大分配的内存,-xms用于指定JVM初始分配的内存,因此,-xmx与-xms相等表示JVM初次分配的内存的时候就把全部能够分配的最大内存分配给它(指JVM),这样的作的好处是:
1. 避免JVM在运行过程当中、每次垃圾回收完成后向OS申请内存:由于全部的能够分配的最大内存第一个就给它(JVM)了。
2. 延后启动后首次GC的发生时机、减小启动初期的GC次数:由于第一次给它分配了最大的;
3. 尽量避免使用swap space:swap space为交换空间,当web项目部署到linux上时,有一条调优原则就是“尽量使用内存而不是交换空间”
4.设置堆内存为不可扩展和收缩,避免在每次GC 后调整堆的大小
影响堆内存扩展与收缩的两个参数

由上表可知,堆内存默认是自动扩展和收缩的,可是有一个前提条件,就是到xmx比xms大的时候,当咱们将xms设置为和xmx同样大,堆内存就不可扩展和收缩了,即整个堆内存被设置为一个固定值,避免在每次GC 后调整堆的大小。
附加:在Java非堆内存分配中,通常是用永久区内存分配:
JVM 使用 -XX:PermSize 设置非堆内存初始值,由 -XX:MaxPermSize 设置最大非堆内存的大小。
2.5 从日志看JVM(开发实践)

这里了设置GC日志关联的类和将GC日志打印

如程序所述,申请了10MB的空间,allocation1 2MB+allocation2 2MB+allocation3 2MB+allocation4 4MB=10MB
接下来咱们开始阅读GC日志,这里笔者以本身电脑上打印的GC日志为例,讲述阅读GC日志的方法:
heap表示堆,即下面的日志是对JVM堆内存的打印;
由于使用的是jdk8,因此默认使用ParallelGC收集器,也就是在新生代使用Parallel Scavenge收集器,老年代使用ParallelOld收集器
PSYoungGen 表示使用Parallel scavenge收集器做为年轻代收集器,ParOldGen表示使用Parallel old收集器做为老年代收集器,即笔者电脑上默认是使用Parallel scavenge+Parallel old收集器组合。
其中,PSYoungGen总共38400K(37.5MB),被使用了13568K(13.25MB),PSYoungGen又分为Eden Space 33280K(32.5MB) 被使用了40% 13MB,from space 5120K(5MB)和to space 5120K(5MB),这就是一个eden区和两个survivor区。
此处注意,由于使用的是jdk8,因此没有永久区了,只有MetaSpace,见上图。
3、HotSpot VM
3.1 HotSpot VM相关知识
问题一:什么是HotSpot虚拟机?HotSpot VM的前世此生?
回答一:HotSpot VM是由一家名为“Longview Technologies”的公司设计的一款虚拟机,Sun公司收购Longview Technologies公司后,HotSpot VM成为Sun主要支持的VM产品,Oracle公司收购Sun公司后,即在HotSpot的基础上,移植JRockit的优秀特性,将HotSpot VM与JRockit VM整合到一块儿。
问题二:HotSpot VM有何优势?
回答二:HotSpot VM的热点代码探测能力能够经过执行计数器找出最具备编译价值的代码,而后通知JIT编译器以方法为单位进行编译。若是一个方法被频繁调用,或方法中有效循环次数不少,将会分别触发标准编译和OSR(栈上替换)编译动做。经过编译器与解释器恰当地协同工做,能够在最优化的程序响应时间与最佳执行性能中取得平衡,并且无须等待本地代码输出才能执行程序,即时编译的时间压力也相对减少,这样有助于引入更多的代码优化技术,输出质量更高的本地代码。
问题三:HotSpot VM与JVM是什么关系?
回答三:今天的HotSpot VM,是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。
3.2 HotSpot VM的两个实现与查看本机HotSpot
HotSpot VM包括两个实现,不一样的实现适合不一样的场景:
Java HotSpot Client VM:经过减小应用程序启动时间和内存占用,在客户端环境中运行应用程序时能够得到最佳性能。此通过专门调整,可缩短应用程序启动时间和内存占用,使其特别适合客户端环境。此jvm实现比较适合咱们平时用做本地开发,平时的开发不须要很大的内存。
Java HotSpot Server VM:旨在最大程度地提升服务器环境中运行的应用程序的执行速度。此jvm实现通过专门调整,多是特别调整堆大小、垃圾回收器、编译器那些。用于长时间运行的服务器程序,这些服务器程序须要尽量快的运行速度,而不是快速启动时间。
只要电脑上安装jdk,咱们就能够看到hotspot的具体实现:

4、JVM内存回收
咱们知道,Java中是没有析构函数的,既然没有析构函数,那么如何回收对象呢,答案是自动垃圾回收。Java语言的自动回收机制可使程序员不用再操心对象回收问题,一切都交给JVM就行了。那么JVM又是如何作到自动回收垃圾的呢,且看本节,本节分为两个部分——垃圾收集算法和垃圾收集器,其中,收集算法是内存回收的理论,而垃圾回收器是内存回收的实践。
4.1 垃圾收集算法(内存回收理论)
4.1.1 标记-清除算法
标记-清除算法分为两个阶段,“标记”和“清除”,
标记:首先标记出全部须要回收的对象;
清除:在标记完成后统一回收全部被标记的对象。
“标记-清除”算法的不足:第一,效率问题,标记和清除两个过程的效率都不会过高;第二,空间问题,标记清除后产生大量不连续的内存碎片,这些内存空间碎片可能会致使之后程序运行过程当中须要分配较大对象时,没法找到足够的连续内存而不得不提早触发一次垃圾收集动做,若是很容易出现这样的空间碎片多、没法找到大的连续空间的状况,垃圾收集就会较为频繁。
4.1.2 复制算法
为了解决“标记-清除算法”的效率问题,一种复制算法产生了,它将当前可用内存按容量划分为大小相等的两块,每次只使用其中一块。当一块的内存用完了,就将还活着的对象复制到另外一块上面,而后再把已使用的内存空间一次清除掉。这样使得每次都对整个半区进行内存回收,内存分配时就不用考虑内存碎片等复杂状况,只要移动堆顶指针,按顺序分配内存便可。
这种算法处理内存碎片的核心在于将整个半块中活的的对象复制到另外一整个半块上面去,因此称为复制算法。
附:关于复制算法的改进
复制算法合理的解决了内存碎片问题,可是却要以牺牲一半的宝贵内存为代价,这是很是让人心疼的。使人愉快地是,现代虚拟机中,早就有了关于复制算法的改进:
对于Java堆中新生代中的对象来讲,99%的对象都是“朝升夕死”的,就是说不少的对象在建立出来后不久就会死掉了,全部咱们能够大胆一点,不须要按照1:1的比例来划份内存空间,而是将新生代的内存划分为一块较大的Eden区(通常占新生代8/10的大小)和两块较小的Survivor区(用于复制,通常每块占新生代1/10的大小,两块占新生代2/10的大小)。当回收时,将Eden区和Survivor里面当前还活着的对象所有都复制到另外一块Survivor中(关于另外一个块Survivor是否会溢出的问题,答案是不会,这里将新生代90%的容量里的对象复制到10%的容量里面,确实是有风险的,可是JVM有一种内存的分配担保机制,即当目的Survivor空间不够,会将多出来的对象放到老年代中,由于老年代是足够大的),最后清理Eden区和源Survivor区的空间。这样一来,每次新生代可用内存空间为整个新生代90%,只有10%的内存被浪费掉,
正是由于这一特性,现代虚拟机中采用复制算法来回收新生代,如Serial收集器、ParNew收集器、Parallel scavenge收集器均是如此。
4.1.3 标志-整理算法(复制算法变动后在老年代的应用)
对于新生代来讲,因为具备“99%的对象都是朝生夕死的”这一特色,因此咱们能够大胆的使用10%的内存去存放90%的内存中活着的对象,即便是目的Survivor的容量不够,也能够将多余的存放到老年代中(担保机制),全部对于新生代,咱们使用复制算法是比较好的(Serial收集器、ParNew收集器、Parallel scavenge收集器)。
可是对于老年代,没有大多数对象朝生夕死这一特色,若是使用复制算法就要浪费一半的宝贵内存,全部咱们用另外一种办法来处理它(指老年代)——标志-整理算法。
标记-整理算法分为两个阶段,“标记”和“整理”,
标记:首先标记出全部须要回收的对象(和标记-清除算法同样);
整理:在标记完成后让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存(向一端移动相似复制算法)。
4.1.4 分代收集算法
当前商业虚拟机都是的垃圾收集都使用“分代收集”算法,这种算法并无什么新的思想,只是根据对象存活周期的不一样将内存划分为几块。通常是把Java堆分为新生代和老年代,这样就能够根据各个年代的特色采起最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少许对象存活,就是使用复制算法,这样只须要付出少许存活对象的复制成本就能够完成收集。而老年代中由于对象的存活率高、没有额外空间对其分配担保(新生代复制算法若是目的Survivor容量不够会将多余对象放到老年代中,这就是老年代对新生代的分配担保),必须使用“标记-清除算法”或“标记-整理算法”来回收。
四种经常使用算法优缺点比较、用途比较

4.2 垃圾收集器(内存回收实践)
有了上面的垃圾回收算法,就有了不少的垃圾回收器。对于垃圾回收器,不多有表格对比,笔者以表格对比的方式呈现:



注意:G1收集器的收集算法加粗了,这里作出说明,G1收集器从总体上来看是基于“标记-整理”算法实现的收集器,从局部(两个region之间)上看来是基于“复制”算法实现的。
从上表能够获得的收集经常使用组合包括:
经常使用组合1:Serial + serial old 新生代和老年代都是单线程,简单
经常使用组合2:ParNew+ serial old 新生代多线程,老年代单线程,简单
经常使用组合3:Parallel scavenge + Parallel old 该组合完成吞吐量优先虚拟机,适用于后台计算
经常使用组合4:cms收集器 完成响应时间短虚拟机,适用于用户交互
经常使用组合5:G1收集器 面向服务端的垃圾回收器
4.2.1 经常使用组合1:Serial + serial old 新生代和老年代都是单线程,简单

附:图上有一个safepoint,译为安全点(有的博客上写成了savepoint,是错误的,至少是不许确的),这个safepoint干什么的呢?如何肯定这个safepoint的位置?
这个safepoint是干什么的?
safepoint的定义是“A point in program where the state of execution is known by the VM”,译为程序中一个点就是虚拟机所知道的一个执行状态。
JVM中safepoint有两种,分别为GC safepoint、Deoptimization safepoint:
GC safepoint:用在垃圾收集操做中,若是要执行一次GC,那么JVM里全部须要执行GC的Java线程都要在到达GC safepoint以后才能够开始GC;
Deoptimization safepoint:若是要执行一次deoptimization
,那么JVM里全部须要执行deoptimization的Java线程都要在到达deoptimization safepoint以后才能够开始deoptimize
咱们上图中的safepoint天然是GC safepoint,因此上图中的两个safepoint都是指执行GC线程前的状态。
对于上图的理解是(不少博客上都有这种运行示意图,可是没有加上解释,笔者这里加上):
一、多个用户线程(图中是四个)要开始执行新生代GC操做,因此都要达到GC safepoint点,先到的要等待晚到的,图中都达到了;
二、四个线程都执行新生代的GC操做,由于使用的是Serial收集器,因此是基于复制算法的单线程GC,并且要Stop the world,因此只有GC线程在执行,四个用户线程都中止了。
三、新生代GC操做完成,四个线程继续执行,过了一下子,要开始执行老年代的GC操做了,因此四个线程都要再次达到GC safepoint点,先到的要等待晚到的,图中都达到了;
四、四个线程都执行老年代的GC操做,由于使用的是Serial Old收集器,因此是基于标志-整理算法的单线程GC,并且要Stop the world,因此只有GC线程在执行,四个用户线程都中止了。
五、老年代GC操做完成,四个线程继续执行。
4.2.2 经常使用组合2:ParNew+ serial old 新生代多线程,老年代单线程,简单

该组合中新生代ParNew收集器仅仅是Serial收集器的多线程版本,全部该组合相对于Serial + serial old 只是新生代是多线程而已,其他不变
对于上图的理解是(不少博客上都有这种运行示意图,可是没有加上解释,笔者这里加上):
一、多个用户线程(图中是四个)要开始执行新生代GC操做,因此都要达到GC safepoint点,先到的要等待晚到的,图中都达到了;
二、四个线程都执行新生代的GC操做,由于使用的是Parnew收集器,因此是基于复制算法的多线程GC(注意:这里的多线程GC,是指多个GC线程并发,用户线程仍是要中止的)因此仍是要Stop the world,因此只有GC线程在执行,四个用户线程都中止了。
三、新生代GC操做完成,四个线程继续执行,过了一下子,要开始执行老年代的GC操做了,因此四个线程都要再次达到GC safepoint点,先到的要等待晚到的,图中都达到了;
四、四个线程都执行老年代的GC操做,由于使用的是Serial Old收集器,因此是基于标志-整理算法的单线程GC,并且要Stop the world,因此只有GC线程在执行,四个用户线程都中止了。
五、老年代GC操做完成,四个线程继续执行。
4.2.3 经常使用组合3:Parallel scavenge + Parallel old 新生代和老年代都是多线程,该组合完成吞吐量优先虚拟机,适用于后台计算

对于上图的理解是:
一、多个用户线程(图中是四个)要开始执行新生代GC操做,因此都要达到GC safepoint点,先到的要等待晚到的,图中都达到了;
二、四个线程都执行新生代的GC操做,由于使用的是Parallel scavenge收集器,因此是基于复制算法的多线程GC(注意,这里的多线程GC,是指多个GC线程并发,用户线程仍是要中止的)因此只有GC线程在执行,四个用户线程都中止了。
三、新生代GC操做完成,四个线程继续执行,过了一下子,要开始执行老年代的GC操做了,因此四个线程都要再次达到GC safepoint点,先到的要等待晚到的,图中都达到了;
四、四个线程都执行老年代的GC操做,由于使用的是Parallel Old收集器,因此是基于标志-整理算法的多线程GC,(注意,这里的多线程GC,是指多个GC线程并发,用户线程仍是要中止的)因此只有GC线程在执行,四个用户线程都中止了。
五、老年代GC操做完成,四个线程继续执行。
4.2.4 经常使用组合4:cms收集器 多线程,完成响应时间短虚拟机,适用于用户交互

对于上图的理解是:
CMS收集包括四个步骤:初始标记、并发标记、从新标记、并发清除(CMS做为标记-清除收集器,三个标记一个清除)
|
是否须要stop the world,中止用户线程 |
单个GC线程运行or多个GC线程运行 |
初始标记 |
须要 |
单个GC线程运行 |
并发标记 |
不须要 |
多个GC线程运行 |
从新标记 |
须要 |
多个GC线程运行 |
并发清除 |
不须要 |
多个GC线程运行 |
一、多个用户线程(图中是四个)要开始执行新生代GC操做,因此都要达到GC safepoint点,先到的要等待晚到的,图中都达到了;
二、四个线程都执行GC操做,由于使用的是CMS收集器,第一步骤是初始标记,初始标记仅仅只是标记一下GC Roots能直接关联到的对象,GC的标记阶段须要stop the world,让全部Java线程挂起,这样JVM才能够安全地来标记对象。因此只有“初始标记”在执行,四个用户线程都中止了。初始标记完成后,达到第二个GC safepoint,图中达到了;
三、开始执行并发标记,并发标记是GCRoot开始对堆中的对象进行可达性分析,找出存活的对象,并发标记能够与用户线程一块儿执行,并发标记完成后,全部线程达到下一个GC safepoint,图中达到了;
四、开始执行从新标记,从新标记是为了修正在并发标记期间因用户程序继续运做而致使标记产生变更的那部分标记记录,
从新标记完成后,全部线程达到下一个GC safepoint,图中达到了;
五、开始执行并发清理,并发清理能够与用户线程一块儿执行,并发清理完成后,全部线程达到下一个GC safepoint,图中达到了;
六、开始重置线程,就是对刚才并发标记操做的对象,图中是线程3(注意:重置线程针对的是并发标记的线程,没有被并发标记的线程不须要重置线程操做),重置操做线程3的时候,与其余三个用户线程无关,它们能够一块儿执行。
CMS为何是多线程收集器?
由于CMS收集器整个过程当中耗时最长的第二并发标记和第四并发清除过程当中,GC线程均可以与用户线程一块儿工做,初始标记和从新标记时间忽略不计,因此,从整体上来讲,cms收集器的内存回收过程与用户线程是并发执行的,因此上表中CMS为多线程收集器。
4.2.5 经常使用组合5:G1收集器 多线程,面向服务端的垃圾回收器
一、什么是G1?
G1就是Gabage-First,它将整个Java堆划分为多个大小相等的独立区域,即Region,虽然还保留新生代和老年代的概念,但新生代和老年代已再也不物理隔离,它们都是一部分Region的集合。
G1收集器的底层原理:G1跟踪各个Region里面的垃圾堆积的价值大小(回收所得到的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次依据容许的收集时间,优先收集回收价值最大的Region。正是这种使用Region划份内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内能够获取尽量高的效率。
G1收集器运行示意图以下:

对于上图的理解是:
G1收集包括四个步骤:初始标记、并发标记、最终筛选、筛选回收
一、多个用户线程(图中是四个)要开始执行新生代GC操做,因此都要达到GC safepoint点,先到的要等待晚到的,图中都达到了;
二、开始执行初始标记,初始标记仅仅只是标记一下GC Roots能直接关联到的对象,而且修改TAMS(Next Top at Mark Start)的值,让下一个阶段用户程序并发标记时,能在正确可用的Region上建立新对象,整个标记阶段须要stop the world,让全部Java线程挂起,这样JVM才能够安全地来标记对象。因此只有“初始标记”在执行,四个用户线程都中止了。初始标记完成后,达到第二个GC safepoint,图中达到了;
三、开始执行并发标记,并发标记是GCRoot开始对堆中的对象进行可达性分析,找出存活的对象,并发标记能够与用户线程一块儿执行,并发标记完成后,全部线程(GC线程、用户线程)达到下一个GC safepoint,图中达到了;
四、开始执行最终标记,最终标记是为了修正在并发标记期间因用户程序继续运做而致使标记产生变更的那部分标记记录,最终标记完成后,全部线程达到下一个GC safepoint,图中达到了;
五、开始执行筛选回收,筛选回归首先对各个Region的回收价值和成本排序, 根据用户期待的GC停顿时间来制定回收计划,筛选回收过程当中,由于停顿用户线程将大幅提升收集效率,因此通常筛选回归是中止用户线程的,筛选回归完成后,全部线程达到下一个GC safepoint,图中达到了;
六、G1收集器收集结束,继续并发执行用户线程。
4.3 垃圾收集器经常使用参数
(笔者这里加上idea上如何使用这些参数,这些是垃圾收集器的参数,因此这里放到第四部分,在本文第五部份内存分配咱们会用到)
参数 |
idea中使用方式 |
描述 |
UseSerialGC |
VM Options: -XX:+UseSerialGC |
虚拟机运行在Client模式下的默认值,打开此开关以后,使用Serial+Serial Old的收集器组合进行内存回收 |
UseParNewGC |
VM Options: -XX:+UseParNewGC |
打开此开关以后,使用ParNew+ Serial Old的收集器组合进行内存回收 |
UseConcMarkSweepGC |
VM Options: -XX:+UseConcMarkSweepGC |
打开此开关以后,使用ParNew + CMS+ Serial Old的收集器组合进行内存回收。Serial Old收集器将做为CMS收集器出现Concurrent Mode Failure失败后的后备收集器使用 |
UseParallelGC |
VM Options: -XX:+UseParallelGC |
虚拟机运行在Server模式下的默认值,打开此开关以后,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器组合进行内存回收 |
UseParallelOldGC |
VM Options: -XX:UseParallelOldGC |
打开此开关后,使用Parallel Scavenge + Parallel Old 的收集器组合进行内存回收 |
SurvivorRatio |
VM Options: -XX:SurvivorRatio=8 |
新生代中Eden区域与Survivor区域的容量比值,默认为8,表明Eden:Survivor=8:1 |
PretenureSizeThreshold |
VM Options: -XX:PretenureSizeThreshold=3145728 表示大于3MB都到老年代中去 |
直接晋升到老年代的对象大小,设置这个参数后,这个参数以字节B为单位大于这个参数的对象将直接在老年代中分配 |
MaxTenuringThreshold |
VM Options: -XX:MaxTenuringThreshold=2 表示经历两次Minor GC,就到老年代中去 |
晋升到老年代的对象年龄,每一个对象在坚持过一次Minor GC以后,年龄就增长1,当超过这个参数值就进入到老年代 |
UseAdaptiveSizePolicy |
VM Options: -XX:+UseAdaptiveSizePolicy |
动态调整Java堆中各个区域的大小以及进入老年代的年龄 |
HandlePromotionFailure |
jdk1.8下,HandlePromotionFailure会报错,Unrecongnized VM option |
是否容许分配担保失败,即老年代的剩余空间不足应应对新生代的整个Eden区和Survivor区的全部对象存活的极端状况 |
ParallelGCThreads |
VM Options: -XX:ParallelGCThreads=10 |
设置并行GC时进入内存回收线程数 |
GCTimeRadio |
VM Options: -XX:GCTimeRadio=99 |
GC占总时间的比率,默认值是99,即容许1%的GC时间,仅在使用Parallel Scavenge收集器时生效 |
MaxGCPauseMillis |
VM Options: -XX:MaxGCPauseMillis=100 |
设置GC的最大停顿时间,仅在使用Parallel Scavenge收集器时生效 |
CMSInitiatingOccupanyFraction |
VM Options: -XX:CMSInitiatingOccupanyFraction=68 |
设置CMS收集器在老年代空间被使用多少后触发垃圾收集,默认值68%,仅在使用CMS收集器时生效 |
UseCMSCompactAtFullCollection |
VM Options: -XX:+UseCMSCompactAtFullCollection |
设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理,仅在使用CMS收集器时生效 |
CMSFullGCsBeforeCompaction |
VM Options: -XX:CCMSFullGCsBeforeCompaction=10 |
设置CMS收集在进行若干次垃圾收集后再启动一次内存碎片整理,仅在使用CMS收集器时生效 |
5、JVM内存分配
这部分的内容借鉴《深刻理解Java虚拟机》一书,有改动。
附:What is minorGC? What is Major GC(Full GC)?
新生代GC(Minor GC):发生在新生代的垃圾收集动做,由于Java对象大多具备朝生夕灭的特性,全部Minor GC很是频繁,通常回收速度较快。
老年代GC(Major GC/Full GC):发生在老年代的GC,出现了major GC,常常会伴随一个MinorGC(可是不绝对),Major GC速度通常比Minor GC慢10倍。
5.1 对象优先在Eden上分配
5.1.1 设置VM Options
-XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC
5.1.2 程序输出(给出附加解释)
第一步:能够看到,当分配6M内存时,所有都在Eden区,没有任何问题,说明JVM优先在Eden区上分配对象

第二步:由于年轻代只有9M,剩下1M是给To Survivor用的,已经使用了6M,如今申请4M, 就会触发Minor GC,将6M的存活的对象放到目的survivor中去,可是放不下,由于目的survivor只有1M空间,因此分配担保到老年代中去,而后将4M对象放到Eden区中。因此,最后的结果是 Eden区域使用了4096KB 4M 老年代中使用了6M 这里form space占用57%能够忽略不计。

5.2 大对象直接进入老年代(使用-XX:PretenureSizeThreshold参数设置)
5.2.1 设置VM Options
-XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728
5.2.2 程序输出(给出附加解释)

5.3 长期存活的对象应该进入老年代(使用-XX:MaxTenuringThreshold参数设置)
5.3.1 设置VM Options
-XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1
5.3.2 程序输出(给出附加解释)
第一步骤:只分配allocation1 allocation2,不会产生任何Minor GC,对象都在Eden区中

第二步骤:分配allocation3,产生Minor GC,allocation2移入老年区

第三步骤:allocation3再次分配,allocation1也被送入老年区,老年区里有allocation1 allocation2

6、尾声
本文讲述JVM自动内存管理(包括内存回收和内存),前言部分从操做系统引入JVM,第二部分介绍JVM空间结构(运行时数据区、堆内存和非堆内存),第三部分介绍HotSpot虚拟机,第四部分和第五部分分别介绍自动内存回收和自动内存分配的原理实现。
每天打码,每天进步!