目录

对于一个Java程序员来讲,大多数状况下的确是无需对内存的分配、释放作太多考虑,对Jvm也无需有多么深的理解的。可是在写程序的过程当中却也每每由于这样而形成了一些不容易察觉到的内存问题,而且在内存问题出现的时候,也不能很快的定位并解决。所以,了解并掌握Java的内存管理是一个合格的Java程序员必需的技能,也只有这样才能写出更好的程序,更好地优化程序的性能。php
一. 背景知识
根据网络能够找到的资料以及笔者可以打听到的消息,目前国内外著名的几个大型互联网公司的语言选型归纳以下:java
- Google: C/C++ Go Python Java JavaScript,不得不提的是Google贡献给java社区的guava包质量很是高,很是值得学习和使用。
- Youtube、豆瓣: Python
- Fackbook、Yahoo、Flickr、新浪:php(优化过的php vm)
- 网易、阿里、搜狐: Java、PHP、Node.js
- Twitter: Ruby->Java,之因此如此就在于与Jvm相比,Ruby的runtime是很是慢的。而且Ruby的应用比起Java仍是比较小众的。不过最近twitter有往scala上迁移的趋势。
可见,虽然最近这些年不少言论都号称java已死或者不久即死,可是Java的语言应用占有率一直居高不下。与高性能的C/C++相比,Java具备gc机制,而且没有那让人望而生畏的指针,上手门槛相对较低;而与上手成本更低的PHP、Ruby等脚本语言来讲,又比这些脚本语言有性能上的优点(这里暂时忽略FB本身开发的HHVM)。程序员
对于Java来讲,最终是要依靠字节码运行在jvm上的。目前,常见的jvm有如下几种:算法
- Sun HotSpot
- BEA Jrockit
- IBM J9
- Dalvik(Android)
其中以HotSpot应用最普遍。目前sun jdk的最新版本已经到了8,但鉴于新版的jdk使用并未普及,所以本文仅仅针对HotSpot虚拟机的jdk6来说。编程
二. Jvm虚拟机内存简介
2.1 Java运行时内存区
Java的运行时内存组成以下图所示:数组

其中,对于这各个部分有一些是线程私有的,其余则是线程共享的。服务器
线程私有的以下:网络
线程共享的以下:
-
Java堆
存放对象实例,几乎全部的对象实例以及其属性都在这里分配内存。
-
方法区
存储已经被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等数据。
-
运行时常量池
方法区的一部分。用于存放编译期生成的各类字面量和符号引用。
-
直接内存
NIO、Native函数直接分配的堆外内存。DirectBuffer引用也会使用此部份内存。
2.2 对象访问
Java是面向对象的一种编程语言,那么如何经过引用来访问对象呢?通常有两种方式:
-
经过句柄访问

-
直接指针

此种方式也是HotSpot虚拟机采用的方式。
2.3 内存溢出
在JVM申请内存的过程当中,会遇到没法申请到足够内存,从而致使内存溢出的状况。通常有如下几种状况:
- 虚拟机栈和本地方法栈溢出
- StackOverflowError: 线程请求的栈深度大于虚拟机所容许的最大深度(循环递归)
- OutOfMemoryError: 虚拟机在扩展栈是没法申请到足够的内存空间,通常能够经过不停地建立线程引发此种状况
- Java堆溢出: 当建立大量对象而且对象生命周期都很长的状况下,会引起OutOfMemoryError
- 运行时常量区溢出:OutOfMemoryError:PermGen space,这里一个典型的例子就是String的intern方法,当大量字符串使用intern时,会触发此内存溢出
- 方法区溢出:方法区存放Class等元数据信息,若是产生大量的类(使用cglib),那么就会引起此内存溢出,OutOfMemoryError:PermGen space,在使用Hibernate等框架时会容易引发此种状况。
三. 垃圾收集
3.1 理论基础
在一般状况下,咱们掌握java的内存管理就是为了应对网站/服务访问慢,慢的缘由通常有如下几点:
- 内存:垃圾收集占用cpu;放入了太多数据,形成内存泄露(java也是有这种问题的^_^)
- 线程死锁
- I/O速度太慢
- 依赖的其余服务响应太慢
- 复杂的业务逻辑或者算法形成响应的缓慢
其中,垃圾收集对性能的影响通常有如下几个:
- 内存泄露
- 程序暂停
- 程序吞吐量显著降低
- 响应时间变慢
垃圾收集的一些基本概念
- Concurrent Collector:收集的同时可运行其余的工做进程
- Parallel Collector: 使用多CPU进行垃圾收集
- Stop-the-word(STW):收集时必须暂停其余全部的工做进程
- Sticky-reference-count:对于使用“引用计数”(reference count)算法的GC,若是对象的计数器溢出,则起不到标记某个对象是垃圾的做用了,这种错误称为sticky-reference-count problem,一般能够增长计数器的bit数来减小出现这个问题的概率,可是那样会占用更多空间。通常若是GC算法能迅速清理完对象,也不容易出现这个问题。
- Mutator:mutate的中文是变异,在GC中便是指一种JVM程序,专门更新对象的状态的,也就是让对象“变异”成为另外一种类型,好比变为垃圾。
- On-the-fly:用来描述某个GC的类型:on-the-fly reference count garbage collector。此GC不用标记而是经过引用计数来识别垃圾。
- Generational gc:这是一种相对于传统的“标记-清理”技术来讲,比较先进的gc,特色是把对象分红不一样的generation,即分红几代人,有年轻的,有年老的。这类gc主要是利用计算机程序的一个特色,即“越年轻的对象越容易死亡”,也就是存活的越久的对象越有机会存活下去(姜是老的辣)。
吞吐量与响应时间
牵扯到垃圾收集,还须要搞清楚吞吐量与响应时间的含义
- 吞吐量是对单位时间内完成的工做量的量度。如:每分钟的 Web 服务器请求数量
- 响应时间是提交请求和返回该请求的响应之间使用的时间。如:访问Web页面花费的时间
吞吐量与访问时间的关系很复杂,有时可能以响应时间为代价而获得较高的吞吐量,而有时候又要以吞吐量为代价获得较好的响应时间。而在其余状况下,一个单独的更改可能对二者都有提升。一般,平均响应时间越短,系统吞吐量越大;平均响应时间越长,系统吞吐量越小; 可是,系统吞吐量越大, 未必平均响应时间越短;由于在某些状况(例如,不增长任何硬件配置)吞吐量的增大,有时会把平均响应时间做为牺牲,来换取一段时间处理更多的请求。
针对于Java的垃圾回收来讲,不一样的垃圾回收器会不一样程度地影响这两个指标。例如:并行的垃圾收集器,其保证的是吞吐量,会在必定程度上牺牲响应时间。而并发的收集器,则主要保证的是请求的响应时间。
GC的流程
- 找出堆中活着的对象
- 释放死对象占用的资源
- 按期调整活对象的位置
GC算法
- Mark-Sweep 标记-清除
- Mark-Sweep-Compact 标记-整理
-
Copying Collector 复制算法
-
Mark-标记
从”GC roots”开始扫描(这里的roots包括线程栈、静态常量等),给可以沿着roots到达的对象标记为”live”,最终全部可以到达的对象都被标记为”live”,而没法到达的对象则为”dead”。效率和存活对象的数量是线性相关的。
-
Sweep-清除
扫描堆,定位到全部”dead”对象,并清理掉。效率和堆的大小是线性相关的。
-
Compact-压缩
对于对象的清除,会产生一些内存碎片,这时候就须要对这些内存进行压缩、整理。包括:relocate(将存货的对象移动到一块儿,从而释放出连续的可用内存)、remap(收集全部的对象引用指向新的对象地址)。效率和存活对象的数量是线性相关的。
-
Copy-复制
将内存分为”from”和”to”两个区域,垃圾回收时,将from区域的存活对象总体复制到to区域中。效率和存活对象的数量是线性相关的。
其中,Copy对比Mark-sweep
- 内存消耗:copy须要两倍的最大live set内存;mark-sweep则只须要一倍。
- 效率上:copy与live set成线性相关,效率高;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。

- Eden区是分配对象的区域。
- Survivor是minor/younger gc后存储存活对象的区域。
- Tenured区域存储长时间存活的对象。
分代收集中典型的垃圾收集算法组合描述以下:
- 年青代一般使用Copy算法收集,会stop the world
- 老年代收集通常采用Mark-sweep-compact, 有可能会stop the world,也能够是concurrent或者部分concurrent。
那么什么时候进行Minor GC、什么时候进行Major GC? 通常的过程以下:
- 对象在Eden Space完成内存分配
- 当Eden Space满了,再建立对象,会由于申请不到空间,触发Minor GC,进行New(Eden + S0 或 Eden S1) Generation进行垃圾回收
- Minor GC时,Eden Space不能被回收的对象被放入到空的Survivor(S0或S1,Eden确定会被清空),另外一个Survivor里不能被GC回收的对象也会被放入这个Survivor,始终保证一个Survivor是空的
- 在Step3时,若是发现Survivor区满了,则这些对象被copy到old区,或者Survivor并无满,可是有些对象已经足够Old,也被放入Old Space。
- 当Old Space被放满以后,进行Full GC
但这个具体还要看JVM是采用的哪一种GC方案。
New Generation的GC有如下三种:
- Serial
- ParallelScavenge
- ParNew
对于上述三种GC方案均是在Eden Space分配不下时,触发GC。
Old Generation的GC有如下四种:
对于Serial Old, Parallel Old而言触发机制为
- Old Generation空间不足
- Permanent Generation空间不足
- Minor GC时的悲观策略
- Minor GC后在Eden上分配内存仍然失败
- 执行Heap Dump时
- 外部调用System.gc,可经过-XX:+DisableExplicitGC来禁止,。这里须要注意的是禁用System.gc()会引发使用NIO时的OOM,因此此选项慎重使用。具体可见:http://hllvm.group.iteye.com/group/topic/27945。
对于CMS而言触发机制为:
- 当Old Generation空间使用到必定比率时触发,HopSpot V1.6中默认是92%,可经过PrintCMSInitiationStatistics(此参数在V1.5中不能用)来查看这个值究竟是多少,经过CMSInitiatingOccupancyFaction来强制指定。默认值是根据以下公式计算出来的:((100 -MinHeapFreeRatio) +(double)(CMSTriggerRatio* MinHeapFreeRatio) / 100.0)/ 100.0,MinHeapFreeRatio默认值为40,CMSTriggerRatio默认值为80。
- 当Permanent Generation采用CMS收集且空间使用到必定比率触发,Permanent Generation采用CMS收集需设置:-XX:+CMSClassUnloadingEnabled。 Hotspot V1.6中默认为92%,可经过CMSInitiatingPermOccupancyFraction来强制指定。一样,它是根据以下公式计算出来的:((100 -MinHeapFreeRatio) +(double)(CMSTriggerPermRatio* MinHeapFreeRatio) / 100.0)/ 100.0,MinHeapFreeRatio默认值为40,CMSTriggerPermRatio默认值为80。
- Hotspot根据成本计算决定是否须要执行CMS GC,可经过-XX:+UseCmsInitiatingOccupancyOnly来去掉这个动态执行的策略。
- 外部调用System.gc,且设置了ExplicitGCIInvokesConcurrent或者ExplicitGCInvokesConcurrentAndUnloadsClasses。
3.2 HotSpot垃圾收集器

上图即为HotSpot虚拟机的垃圾收集器组成。
Serial收集器
- -XX:+UserSerialGC参数打开此收集器
- Client模式下新生代默认的收集器。
- 较长的stop the world时间
- 简单而高效
此收集器的一个工做流程以下如所示:
收集前:

收集后:

ParNew收集器
- -XX:+UserParNewGC
- +UseConcuMarkSweepGC时默认开启
- Serial收集器的多线程版本
- 默认线程数与CPU数目相同
- -XX:ParrallelGCThreads指定线程数目
对比Serial收集器以下图所示:

Parallel Scavenge收集器
- 新生代并行收集器
- 采用Copy算法
- 主要关注的是达到可控制的吞吐量,“吞吐量优先”
- -XX:MaxGCPauseMillis -XX:GCTimeRAtion两个参数精确控制吞吐量
- -XX:UseAdaptiveSizePolicy GC自适应调节策略
- Server模式的默认新生代收集器
Serial Old收集器
- Serial的老年代版本
- Client模式的默认老年代收集器
- CMS收集器的后备预案,Concurrent Mode Failure时使用
- -XX:+UseSerialGC开启此收集器
Parallel Old收集器
- -XX:+UseParallelGC -XX:+UseParallelOldGC启用此收集器
- Server模式的默认老年代收集器
- Parallel Scavenge的老年代版本,使用多线程和”mark-sweep”算法
- 关注点在吞吐量以及CPU资源敏感的场合使用
- 通常使用Parallel Scavenge + Parallel Old能够达到最大吞吐量保证
CMS收集器
并发低停顿收集器
- -XX:UseConcMarkSweepGC 开启CMS收集器,(默认使用ParNew做为年轻代收集器,SerialOld做为收集失败的垃圾收集器)
- 以获取最短回收停顿时间为目标的收集器,重视响应速度,但愿系统停顿时间最短,会和互联网应用。
四个步骤:
- 初始标记 Stop the world: 只标记GC roots能直接关联到的对象,速度很快。
- 并发标记:进行GC roots tracing,与用户线程并发进行
- 从新标记 Stop the world:修正并发标记期间因程序继续运行致使变更的标记记录
- 并发清除
对比serial old收集器以下图所示:

CMS有如下的缺点:
- CMS是惟一不进行compact的垃圾收集器,当cms释放了垃圾对象占用的内存后,它不会把活动对象移动到老年代的一端
- 对CPU资源很是敏感。不会致使线程停顿,但会致使程序变慢,总吞吐量下降。CPU核越多越不明显
- 没法处理浮动垃圾。可能出现“concurrent Mode Failure”失败, 致使另外一次full GC ,能够经过调整-XX:CMSInitiatingOccupancyFraction来控制内存占用达到多少时触发gc
- 大量空间碎片。这个能够经过设置-XX:UseCMSCompacAtFullCollection(是否在full gc时开启compact)以及-XX:CMSFullGCsBeforeCompaction(在进行compact前full gc的次数)
G1收集器
G1算法在Java6中仍是试验性质的,在Java7中正式引入,但还未被普遍运用到生产环境中。它的特色以下:
- 使用标记-清理算法
- 不会产生碎片
- 可预测的停顿时间
- 化整为零:将整个Java堆划分为多个大小相等的独立区域
- -XX:+UseG1GC能够打开此垃圾回收器
- -XX:MaxGCPauseMillis=200能够设置最大GC停顿时间,固然JVM并不保证必定可以达到,只是尽力。

3.3 调优经验
- 须要打开gc日志并读懂gc日志:-XX:PrintHeapAtGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamp -Xloggc:$CATALINA_BASE/logs/gc.log
- 垃圾回收的最佳状态是只有young gc,也就是避免生命周期很长的对象的存在。
- 从young gc开始,尽可能给年青代大点的内存,避免full gc
- 注意Survivor大小
- 注意内存墙:4G~5G
GC日志简介
1403682.561: [GC [PSYoungGen: 1375104K->11376K(1386176K)] 4145665K->2782002K(4182400K), 0.0174410 secs] [Times: user=0.27 sys=0.00, real=0.02 secs]
- 1403682.561:发生的时间点,JVM运行的时间长度,以度为单位,也能够格式化成固定的时间格式(使用-XX:+PrintGCDateStamps)
- PSYoungGen:发生了何种类型的GC,此处表明发生了年轻代的GC
- 1375104K:回收前的大小
- 11376K:回收后的大小
- 1386176K:YOUNG代的大小
- 4145665 K:回收前总的占用大小
- 2782002K:回收后的占用大小
- 4182400K:总占用大小
- 0.0174410:垃圾收集停顿时间
- 0.27和0.00:表明在用户态(user)和系统状(sys)的CPU运行时间
- 0.02 secs:表明实际的GC的运行时间
注:上面实际GC的运行时间小于用户态和系统态的时间总和,是因为前者仅指CPU的运行时间,包括等待或IO阻塞的时间,而如今的GC是采用多线程收集的,同时机器也是多个CPU,所以,大部分是两者之和要比前面的值大。若是是采用串形化收集器的话,两者时间几乎相差很少。
老年代使用建议
- Parallel GC(-XX:+UseParallel[Old]GC)
- Parallel GC的minor GC时间是最快的, CMS的young gc要比parallel慢, 由于内存碎片
- 能够保证最大的吞吐量
- 确实有必要才改为CMS或G1(for old gen collections)
开发建议
- 小对象allocate的代价很小,一般10个CPU指令;收集掉新对象也很是廉价;不用担忧活的很短的小对象
- 大对象分配的代价以及初始化的代价很大;不一样大小的大对象可能致使java堆碎片,尤为是CMS, ParallelGC 或 G1还好;尽可能避免分配大对象
- 避免改变数据结构大小,如避免改变数组或array backed collections / containers的大小;对象构建(初始化)时最好显式批量定数组大小;改变大小致使没必要要的对象分配,可能致使java堆碎片
- 对象池可能潜在的问题
- 增长了活对象的数量,可能增长GC时间
- 访问(多线程)对象池须要锁,可能带来可扩展性的问题
- 当心过于频繁的对象池访问
GC的庞氏骗局
虽然GC在大多数状况下仍是正常的,但有时候JVM也会发生欺骗你的场景, JVM不停的在垃圾回收,但是每次回收完后堆却仍是满的,很明显程序内存被使用完了,已经没法正常工做了,但JVM就是不抛出OutOfMemoryError(OOM)这个异常来告诉程序员内部发出了什么,只是不停的作老好人尝试帮咱们作垃圾回收,把服务器的资源耗光了。
出现这种现象的一种典型状况就是GC的GCTimeLimit和GCHeapFreeLimit参数设置不合适。GCTimeLimit的默认值是98%,也就是说若是大于等于98%的时间都用花在GC上,则会抛出OutOfMemoryError。GCHeapFreeLimit是回收后可用堆的大小,默认值是2%,也就是说只要有多余2%的内存可用就认为这次gc是成功的。若是GCTimeLimit设置过大或者GCHeapFreeLimit设置太小那么就会形成GC的庞式骗局,不停地进行垃圾回收。
四. Java七、8带来的一些变化
- Java7带来的内存方面的一个很大的改变就是String常量池从Perm区移动到了Heap中。调用String的intern方法时,若是存在堆中的对象,则会直接保存对象的引用,而不会从新建立对象。
- Java7正式引入G1垃圾收集器用于替换CMS。
- Java8中,取消掉了方法区(永久代),使用“元空间”替代,元空间只与系统内存相关。
- Java 8 update 20所引入的一个很棒的优化就是G1回收器中的字符串去重(String deduplication)。因为字符串(包括它们内部的char[]数组)占用了大多数的堆空间,这项新的优化旨在使得G1回收器能识别出堆中那些重复出现的字符串并将它们指向同一个内部的char[]数组,以免同一个字符串的多份拷贝,那样堆的使用效率会变得很低。可使用-XX:+UseStringDedup