参考文章:http://www.importnew.com/21463.html
https://blog.csdn.net/bruce_6/article/details/38553143
(本文中有部分信息是结合个人理解添加进去的,如果有错误还请指正!)
对于一个Java程序员来说,大多数情况下的确是无需对内存的分配、释放做太多考虑,对Jvm也无需有多么深的理解的。但是在写程序的过程中却也往往因为这样而造成了一些不容易察觉到的内存问题,并且在内存问题出现的时候,也不能很快的定位并解决。因此,了解并掌握Java的内存管理是一个合格的Java程序员必需的技能,也只有这样才能写出更好的程序,更好地优化程序的性能。
虽然最近这些年很多言论都号称java已死或者不久即死,但是Java的语言应用占有率一直居高不下。与高性能的C/C++相比,Java具有gc机制,并且没有那让人望而生畏的指针,上手门槛相对较低;而与上手成本更低的PHP、Ruby等脚本语言来说,又比这些脚本语言有性能上的优势(这里暂时忽略FB自己开发的HHVM)。
对于Java来说,最终是要依靠字节码运行在jvm上的。目前,常见的jvm有以下几种:
其中以HotSpot应用最广泛。目前sun jdk的最新版本已经到了8,但鉴于新版的jdk使用并未普及,因此本文仅仅针对HotSpot虚拟机的jdk6来讲。
2.1java运行时内存区
Java的运行时内存组成如下图所示:
其中,对于这各个部分有一些是线程私有的,其他则是线程共享的。
线程私有的有:
线程共享的有:
2.2对象访问
java是面向对象的一种编程语言,那么如何通过引用来访问对象呢?一般有如下两种方法。
1.通过句柄访问:
java堆中分成两部分,一部分存放实例对象数据的实例池,另一部分存放到实例对象以及方法区中的类信息(类类型数据)的指针的句柄池。方法执行的时候,会在栈中开辟一个内存空间用来存储方法运行时出现的一些信息,包括局部变量表(基本数据类型)、对象的引用(reference)等等。而这里的reference存储的就是指向句柄池的信息(可以理解为指向句柄池的指针)。方法在执行的过程中不断地堆栈中的信息进行处理(进栈和出栈),当涉及到的reference出栈时,就会去找到句柄池中的指针,然后执行对象数据。
2.直接指针访问
直接指针访问相比于句柄访问来说少了句柄池这一个中间环节,原本句柄池中存放的指针数据现在分别存放如下:实例对象数据的引用(指针)存放于栈中的reference中,对象类型数据(类类型数据)的引用(指针)存放于实例对象中。也就是说在方法执行的过程中,栈中的reference出栈时,首先访问的是实例对象的数据,实例对象中的处理过程根据是否需要引用到对象类型数据而选择是否去方法区中寻找对象类型数据来执行。也就是说实例对象接管了一部分句柄池的功能(保存对象类型数据的引用),而另外一部分句柄池的功能则直接有栈来接管(保存了实例对象的引用)。
2.3内存溢出
在JVM申请内存的过程中,会遇到无法申请到足够内存,从而导致内存溢出的情况。一般有以下几种情况:
在通常情况下,我们掌握java的内存管理就是为了应对网站/服务访问慢,慢的原因一般有以下几点:
其中,垃圾收集对性能的影响一般有以下几个:
垃圾收集的一些基本概念
吞吐量与响应时间
牵扯到垃圾收集,还需要搞清楚吞吐量与响应时间的含义
吞吐量与访问时间的关系很复杂,有时可能以响应时间为代价而得到较高的吞吐量,而有时候又要以吞吐量为代价得到较好的响应时间。而在其他情况下,一个单独的更改可能对两者都有提高。通常,平均响应时间越短,系统吞吐量越大;平均响应时间越长,系统吞吐量越小; 但是,系统吞吐量越大, 未必平均响应时间越短;因为在某些情况(例如,不增加任何硬件配置)吞吐量的增大,有时会把平均响应时间作为牺牲,来换取一段时间处理更多的请求。
针对于Java的垃圾回收来说,不同的垃圾回收器会不同程度地影响这两个指标。例如:并行的垃圾收集器,其保证的是吞吐量,会在一定程度上牺牲响应时间。而并发的收集器,则主要保证的是请求的响应时间。
GC的流程
GC算法
其中,Copy对比Mark-sweep
分代收集
分代收集是目前比较先进的垃圾回收方案。有以下几个相关理论
HotSpot虚拟机的分代收集,分为一个Eden区、两个Survivor去以及Old Generation/Tenured区,其中Eden以及Survivor共同组成New Generatiton/Young space。通常将对New Generation进行的回收称为Minor GC;对Old Generation进行的回收称为Major GC,但由于Major GC除并发GC外均需对整个堆以及Permanent Generation进行扫描和回收,因此又称为Full GC。
分代收集中典型的垃圾收集算法组合描述如下:
那么何时进行Minor GC、何时进行Major GC? 一般的过程如下:
但这个具体还要看JVM是采用的哪种GC方案。
New Generation的GC有以下三种:
对于上述三种GC方案均是在Eden Space分配不下时,触发GC。
Old Generation的GC有以下四种:
对于Serial Old, Parallel Old而言触发机制为
对于CMS而言触发机制为:
3.2 HotSpot垃圾收集器
上图即为HotSpot虚拟机的垃圾收集器组成。
Serial收集器:串行收集器
一个单线程的收集器,但它的单线程的意义不仅仅是说明它只会使用一个CPU或一条线程去完成垃圾回收,更重要的是在垃圾收集时,必须暂停其他所有的工作线程(“Stop the World”),直到收集结束。
Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单 线程收集效率。
此收集器的一个工作流程如下如所示:
收集前:
收集后:
ParNew收集器(并行GC)
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中一个与性能无关的重要原因是,除了Serial收集器外,目前只有ParNew收集器能与CMS收集器(一款并发的老年代收集器)配合工作。
对比Serial收集器如下图所示:
Parallel Scavenge收集器(并行回收GC)--吞吐量优先
Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。它是 以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化。
吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快的完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Serial Old收集器(串行GC)收集器
Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。
如果在Server模式下,它主要有两大用途:一是在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用,另一个就是作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure的时候使用。
Parallel Old收集器(并行GC)收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。老年代版本吞吐量优先收集器,使用多线程和标记-整理算法,JVM 1.6提供,在此之前,新生代使用了PS收集器的话,老年代除Serial Old外别无选择,因为PS无法与CMS收集器配合工作。
CMS收集器 并发低停顿收集器
CMS 是一种以最短停顿时间为目标的收集器,使用CMS并不能达到GC效率最高(总体GC时间最小),但它能尽可能降低GC时服务的停顿时间,这一点对于实时或 者高交互性应用(譬如证券交易)来说至关重要,这类应用对于长时间STW一般是不可容忍的。CMS收集器使用的是标记-清除算法,也就是说它在运行期间会 产生空间碎片,所以虚拟机提供了参数开启CMS收集结束后再进行一次内存压缩
四个步骤:
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
对比serial old收集器如下图所示:
CMS有以下的缺点:
CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美,器主要有三个显著缺点:
CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量+3) / 4。
CMS 收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure“,失败后而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数 -XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。
最后一个缺点,CMS是基于“标记 -清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。
G1收集器
G1算法在Java6中还是试验性质的,在Java7中正式引入,但还未被广泛运用到生产环境中。它的特点如下:
使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小不等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
在G1收集器中,Region之间的对象引用以及其他收集器的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中,如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不会对全堆扫描也不会有遗漏。
G1收集器的运作步骤:
初始标记阶段只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
并发标记阶段:从GC Roots开始对堆中对象进行可达性分析,找出存活对象,这个阶段耗时较长,但可与用户程序并发执行。
最终标记阶段:为了修正在并发标记阶段因用户程序运作而导致标记变化的那一部分标记记录,虚拟机将这部分记录记录在Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs里的数据合并到Remembered Set中,这阶段需要停顿线程,但可与用户程序并发执行。
筛选回收阶段:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划。这个阶段可与用户程序并发执行,因为只回收一部分Region,时间是用户可控制的。
总结如下: