分享者:锐哥
JVM在执行java程序的过程当中会把它所管理的内存划分红若干个不一样的数据区域。java
(1)程序计数器算法
程序计数器(Program Counter Register)是一块比较小的内存区域,它能够看做是当前线程所执行的字节码指令的行号计数器。在虚拟机的概念模型里,字节码解释器工做时就是经过改变这个计数器的值来选取下一条要执行的字节码指令。 c#
因为java虚拟机的多线程是经过线程的轮流切换并分配CPU时间片来实现的,在任何一个肯定的时刻,一个核只会执行一条线程中的指令,为了线程切换后能恢复到正确的执行位置,每一条线程都须要有一个独立的程序计数器,所以,程序计数器是线程私有的内存。 数组
(2)虚拟机栈安全
虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每一个方法在执行的同时都会建立一个称为栈帧(Stack Frame)的东西,用于存储局部变量表、操做数栈、动态连接、方法出口等信息。每一个方法从调用开始直至执行完成,都对应着一个栈帧在虚拟机栈中入栈和出栈的过程。 多线程
若是线程请求的栈深度超过了虚拟机的最大深度,那么就会抛出StackOverFlowError异常;若是虚拟机能够动态拓展而且在拓展时没法申请到足够的内存,将抛出OutOfMemoryError异常。
并发
(3)本地方法栈布局
本地方法栈(Native Method Stack)和虚拟机栈同样,都是线程私有的,只不过虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为虚拟机执行native方法服务。
性能
(4)Java堆spa
对大多数应用程序来讲,java堆(Java Heap)都是java虚拟机所管理的内存区域中最大的一块。java堆是被全部线程所共享的一块内存区域,在虚拟机启动时建立。此内存区域存在的惟一目的就是存放对象实例,几乎全部的对象都在此内存区域上进行分配。
根据java虚拟机规范的规定,java堆能够处于物理上不连续的内存空间,只要逻辑上是连续的便可。在实现时,能够实现成固定大小的,也能够实现成可拓展的。当拓展时,若是没法申请到足够的内存,将抛出OutOfMemoryError异常。
(5)方法区
方法区(Method Area)也是被各个线程所共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据。
对于习惯在HotSpot虚拟机上开发、部署程序的的开发者来讲,更习惯于把方法区称为”永久代“,但本质上二者并不等价。
(6)运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各类字面量和符号引用,这部份内容将在类加载之后进入方法区的运行时常量池中存放。
符号引用用一组符号来描述所引用的目标,符号能够是任何形式的字面量,只要使用时可以无歧义地定位到目标便可。符号引用与虚拟机的内存布局无关,引用的目标不必定加载到内存中。例如org.simple.People类引用了org.simple.Language类,在编译时,People类并不知道Language类的实际内存地址,所以只能使用符号来代替,这就是符号引用。
HotSpot虚拟机中,对象在内存中存储的布局能够分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
实例数据部分是对象真正存储的有效信息。这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。
使用句柄访问方式的话,java堆中将会划分出一块内存来做为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例和类型数据各自具体的地址信息。
使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference自己不须要修改。使用直接指针访问的最大好处是速度更快,节省了一次指针定位的时间开销。HotSpot虚拟机采用的是直接指针方式。
对于第一个问题,哪些内存须要回收,就是哪些对象是不可用的。第二个问题,何时回收,一句话概述就是内存不够用的时候进行回收。第三个问题,就涉及到回收的具体实现上了。
(1)引用计数算法
(2)可达性分析
在Java中,能够做为GC Roots的对象包括下面几种:
a. 虚拟机栈(栈帧中的本地变量表)中引用的对象
b. 方法区中类静态变量所引用的对象
d. 本地方法栈中JNI(即Native方法)引用的对象
主流的商用程序语言(java、c#等)都是采用的可达性分析算法来断定对象是否存活。
(1)标记-清除算法
这种算法存在两个缺点:
a. 效率问题。”标记”和”清除”两个阶段的效率都不高
(2)复制算法
这种算法的缺点是内存利用率只有50%
(3)标记-整理算法
标记-整理算法(Mark-Compact)是针对老年代的特色(对象存活率较高、没有额外空间进行分配担保)而提出的。这种算法分为标记和整理两个阶段,标记阶段和标记-清除算法的标记阶段一致,可是整理阶段不是直接对标记对象进行回收,而是让还存活的对象向一端移动,而后一次性清理掉端边界之外的内存。
(4)分代收集
新生代中,对象的存活率较低,就采用复制算法,只须要付出少许存活对象的复制成本就能够完成收集;老年代中由于对象存活率高,也没有额外空间进行分配担保,因此采用标记-清除算法或者标记-整理算法。
若是说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
(1)Serial收集器
它是虚拟机运行在client模式下的新生代默认的收集器:由于它简单而高效,对于限定单个CPU的环境来讲,Serial收集器由于没有线程交互的开销,专心作垃圾收集天然能够得到最高的单线程收集效率。
(2)ParNew收集器
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,一个很重要可是与性能无关的缘由是:除了Serial收集器外,只有它能与CMS收集器配合工做。
(3)Parallel Scavenge收集器
停顿时间越短,就越适合须要与用户交互的程序,良好的响应速度可以提高用户体验,而高吞吐量则能够高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不须要太多交互的任务。
除上述两个参数以外,Parallel Scavenge还提供一个参数 -XX:UseAdaptiveSizePolicy。若是开启了这个参数,就不须要手动指定新生代的大小(-Xmn)、Eden与Survivor的比率(-XX:SurvivorRatio)、晋升老年代对象的年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行状况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为 GC自适应调节策略。
(4)Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,使用”标记-整理”算法。这个收集器的主要意义也是给client模式下的虚拟机使用。若是在Server模式下,它还有另外两个做用:一种用途是在JDK 1.5及以前的版本中与Parallel Scavenge收集器搭配使用,另外一种用途就是做为CMS收集器的后备预案,在CMS收集器发生Concurrent Mode Failer时使用。
(5)Parallel Old收集器
(6)CMS收集器
CMS收集器(Concurrent Mark Sweep,并发标记清除)是一种以获取最短停顿时间为目标的收集器。基于“标记-清除”算法实现,它的运做过程大体分为4个步骤:
a. 初始标记
c. 从新标记
其中,初始标记和从新标记过程仍然须要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。从新标记阶段就是为了修正并发标记阶段因用户程序继续运行而致使标记产生变更的那一部分对象的标记记录。
这个收集器存在3个明显的缺点:
b. CMS没法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failer”失败而致使另外一次Full GC的产生。并发清理阶段,用户程序还在运行着,伴随着就会产生新的垃圾,这一部分的垃圾出如今标记以后,没法在当次收集中处理掉,只好等待下一次收集时处理,这一部分垃圾称之为“浮动垃圾”。也是由于并发清除阶段,用户程序还在运行,那也就必需要预留一部份内存空间给并发收集时用户程序使用。在JDK1.5的默认设置下,CMS收集器在当老年代使用了68%的空间后就会被激活,在JDK1.6种,这个阈值被提高到了92%。要是CMS运行期间预留的内存没法知足程序须要,就会出现一次“Concurrent Mode Failer”失败,这时虚拟机将启动后背预案:临时启用Serial Old收集器来从新进行老年代的垃圾收集。
(7)G1收集器
G1(Garbage First)收集器是一款面向服务端应用的垃圾收集器,JDK 1.7中才出现。相比其余的垃圾收集器,G1具备更加明显的优点:
a. G1中虽然还保留分代的概念,可是G1能独立管理整个Java堆,再也不须要像之前那样要多个收集器配合。由于G1将整个堆划分红多个大小相等的独立区域(Region),新生代和老年代的概念虽然保留着,可是再也不是物理隔离的了,他们都是一部分Region(不须要连续)的集合。
b. 空间整合。G1从总体上来看是基于标记-整理算法实现,从局部(两个Region之间)来看是基于复制算法实现,避免了内存碎片的问题。
c. 可预测的停顿。这是G1相对于CMS的另外一大优点。G1除了追求低停顿之外,还能创建可预测的停顿时间模型,能让使用者指明在一个长度为M毫秒的时间片断内,消耗在垃圾收集上的时间不得超过N毫秒。
G1之因此能创建可预测的停顿时间模型,是由于它能够有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪每一个Region里面的垃圾堆积的价值大小(回收这块Region所得到的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据容许的收集时间,优先回收价值最大的Region(这也是Garbage First名称的由来)。
Java技术体系中所讲的自动内存管理最终能够归结为自动化的解决了两个问题:给对象分配内存以及回收分配给对象的内存。内存的回收上面已经说过原理,下面看一下内存分配。
对象在新生代Eden区中分配,当Eden区中没有足够的内存进行分配时,虚拟机将发起一次Minor GC。
虚拟机提供了一个参数 -XX:PretenureSizeThreshold ,大于这个参数设置值的对象将不会在Eden区域上进行分配,而是直接在老年代进行分配。这样作的目的是为了不在Eden区以及两个Survivor区之间发生大量的内存复制(当大对象的存活率比较高时)。
前面在说对象的内存布局时提到对象头中有一部分存储对象自身运行时所须要的数据,例如哈希码、GC分代年龄,这里面的GC分代年龄指的就是对象在新生代中熬过的GC次数,每熬过一次Minor GC,对象的年龄就增长一岁,当它的年龄增长到必定程度(默认是15岁)就会被晋升到老年代。这个晋升老年代的阈值能够经过参数 -XX:MaxTenuringThreshold 来设置。
对象的年龄不必定非要达到 MaxTenuringThreshold 设置的值才能晋升老年代。若是在Survivor中相同年龄的对象的大小之和超过了Survivor空间大小的一半,那么年龄大于或等于该年龄的对象将直接进入老年代,无需等到 MaxTenuringThreshold 要求的年龄。
在新生代进行Minor GC以前,虚拟机会作以下的事情:
(1)先检查老年代最大可用的连续内存空间是否大于新生代全部对象总空间,若是大于,那么Minor GC就是安全的,由于不会有对象进入老年代。不然进行步骤(2)
(2)查看 HandlePromotionFailure设置值是否容许分配担保失败,若是不容许,那么也要进行一次Full GC。不然进行步骤(3)
(3)检查老年代最大可用连续空间是否大于历次晋升老年代的平均大小,若是大于,会尝试进行一次Minor GC,可是可能会有风险。不然进行步骤(4)
(4)也要进行一次Full GC。
步骤(3)中提到会尝试进行一次Minor GC,但此次GC会存在风险,这句话是什么意思呢?前面在讲复制算法时提到,新生代在进行GC时,若是Eden空间和From Survivor空间中存活的对象大小之和超过了To Survivor空间的大小,那么这些存活对象将经过分配担保机制进入老年代。由于尚未进行Minor GC,因此虚拟机并不知道本次GC中存活对象的大小,因此只好采用历次晋升老年代对象的平均值来做为经验值,若是老年代可用的最大连续内存空间大于经验值,那么颇有可能说明老年代可用的最大连续内存空间也大于本次Minor GC存活的对象大小之和,因此会尝试进行Minor GC。可是若是在进行Minor GC后发现存活对象大小之和大于老年代最大可用连续内存空间,那么这时老年代将没法存放这部分存活对象,这就是风险所在,此时老年代就要来一次Full GC以腾出空间。