JVM Memory Managment

内存空间

尊重原创 本文转载自https://blog.csdn.net/xiaoddt/article/details/55050215

内存空间

在JVM规范中,将内存空间分为:方法区、堆、本地方法栈、PC寄存器、及JVM方法栈。如图:

这里写图片描述

方法区

方法区主要保存了类加载的信息(名称、修饰符等)、类的静态变量、类中定义了为final类型的常量,类中的field信息、类中的方法信息。当开发人员在程序中通过Class对象的getName、isInstance等方法来获取信息时,这些数据都来源于方法区域。方法区域是全局共享的。在一定条件下也会被GC。当方法区域要使用的内存大小超过其允许的大小是,会抛出OutOfMemory异常。
在Sun JDK中,这块区域对应Permanet Generation,又称持久带,默认最小值为16MB,最大值为64MB,可以通过

堆用于存储对象实例及数组值,可以认为,java中所有通过new创建的对象的内存都在此分配。Heap中对象所占用的内存由GC回收,在32位操作系统中最大为2GB,64为操作系统则没有限制,其大小可以通过-Xms和-Xms来控制,-Xms为JVM启动时申请的最小Heap内存,默认为物理内存的1/64但小于1GB,-Xmx为JVM可申请的最大Heap内存。默认为物理内存的1/4但小于1GB。默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小。可通过-XX:MinHeapFreeRatio=”比例”来指定这个比例;当空余堆内存大于70%时,JVM会减小到-Xms制定的大小,可以通过
-XX:MaxHeapFreeRatio=”比例”来指定这个值。对于运行系统而言。为了避免频繁的调整Heap的大小。通常将-Xms和-Xmx大小设为一致。
为了让内存回收更高效,SunJDK从1.2开始 对堆采用了分代管理。
这里写图片描述

New Generation

大多数情况下Java程序中新建的对象都是从新生代分配内存的。新生代由Eden Space和两块相同大小的Survivor Space(通常又称为S0和S1或From和To)构成。可以通过-Xmm参数来指定新生代的大小。也可以通过-XX:SurvivorRatio来调整Eden Space及Survivor Space的大小。

Old Generation

用于存放新生代中经过多次垃圾回收仍然存在的对象,例如缓存,新建的对象也有可能在旧生代上直接分配内存。主要有两种情况(由不同的GC实现决定):一种为大对象,可以通过在启动参数上设置-XX:PretenureSizeThreshold=1024(单位为字节,默认值为0)设置当前对象超过多少为大对象。内存在旧生代中直接分配。次参数在新生代采用了Paraller Scavenge GC时无效。它会根据运行时状况决定什么时候在那个地方创建对象。

本地方法栈

本地方法栈用于支持native方法的执行。存储了每个native 方法调用的状态。在SunJDK的实现中,本地方法栈和JVM栈是同一个

PC寄存器和JVM方法栈

每个线程都会创建PC寄存器和JVM方法栈。PC寄存器占用的可能为CPU寄存器或操作系统内存。JVM方法栈为线程私有。其在内存分配上非常高效。当方法运行完毕时,其对应的栈帧也会被释放。
当JVM方法栈空间不足时,会抛出StackOverflowError。在SunJDK中可以通过-Xss来指定其大小。

内存分配

Java对象所占用的内存 主要是从堆中进行分配。堆是所有线程共享的。因此在堆上分配内存时需要加锁,这导致了创建对象的开销比较大。当堆空间不足时,会触发GC。如果GC后空间仍然不足,则会抛出OutOfMemory错误信息。
SunJDK为了提升内存分配效率。会为每个新创建的线程在新生代的Eden Space上分配到一块独立的空间,这块空间称为TLAB,其大小由JVM根据运行情况计算而得。可以通过-XX:TLABWasteTargetPercent来设置TLAB可占用的Eden Space的百分比。默认为1%。JVM根据这个比例、线程数量、线程是否频繁创建对象来给每个线程分配合适大小的TLAB空间。在TLAB空间上分配内存时不用加锁。因此JVM在给线程中的对象分配内存时,会尽量在TLAB上分配。如果对象过大,或者TLAB空间已用完,则仍然在堆上进行分配。因此在编写Java程序时,通常多个小对象比大对象分配来的高效。可通过启动参数:-XX:+PrintTLAB来查看TLAB空间的使用情况。

内存回收

收集器

JVM通过GC来回收堆和方法区的内存。GC的基本原理会首先找到程序中不再被使用的对象,然后回收这些对象所占用的内存。通过采用收集器的方式实现GC,主要的收集器有引用计数收集器和跟踪收集器

引用计数收集器

引用计数收集器采用分散式的管理方式,通过计数器记录对象是否被引用。当计数器为0时,说明此对象不再被使用,可以进行回收
这里写图片描述
上图中,当ObjectA释放对ObjectB的引用后,ObjectB的引用计数器值为0,此时可以回收ObjectB所占用的内存。
引用计数器需要在每次对象赋值时进行计数器的增减,它有一定消耗。另外,引用计数器对于循环引用的场景没有办法实现回收,如果上图中,ObjectA和ObjectB互相引用。那么即使ObjectA释放了对ObjectB、ObjectC的引用,也无法回收ObjectB、ObjectC。对于JAVA这种面向对象的对形成负责引用关系的语言来说,引用计数器作为GC不是很合适,SunJDK也未采用此方式。

跟踪收集器

跟踪收集器采用集中式的管理方式。全局记录数据的引用状态。再基于一定条件触发(例如定时、空间不足时),执行时需要从跟集合来扫描对象的引用关系。这可能会造成应用程序暂停。主要有复制、标记-清除、标记-压缩三种实现算法。

复制

复制采用的方式为从跟集合扫描出存活的对象,并将找到的存活对象复制到一块新的完全未使用的空间中。
这里写图片描述
复制收集器方式仅需要从根集合扫描所有的存货对象,当要回收的空间中存活的对象较少时,复制算法会比较高效。其带来的成本是要增加一块空的内存空间及进行对象的移动

标记-清除

标记-清除采用的是从根集合开始扫描。对存活的对象进行标记。标记完毕后,再扫描整个空间未被标记的对象,并进行回收
这里写图片描述
标记-清除动作不需要对对象进行移动,且仅对其不存活的对象进行处理。在空间中存活的对象较多的情况下较为高效。但是此方式是直接回收不存活对象所占用的内存 会造成内存碎片。

标记-压缩

标记-压缩采用标记-清除的方式对存活对象进行标记,但在清除时则不同,在回收不存活对象所占用的内存空间后,会将其他所有的存活对象的左端空闲的空间进行移动,并跟新引用其对象的指针。
这里写图片描述
此方法成本比标记-清除高,好处是不产生内存碎片。

SunJDK中可用的GC

以上的三种跟踪收集器各有优缺点,SunJDK根据运行的Java内存分析,认为程序中大部分对象的存活时间都是较短的,少部分对象是长期存活的。基于这个分析,SunJDK将JVM的堆分为了新生代和旧生代,并提供了不同的GC实现:
这里写图片描述

新生代可用GC

SunJDK认为新生代的对象通常存活时间较短,因此选择了上述的复制算法来实现对象的回收。根据上述复制算法的介绍,在执行复制时,需要一块未使用的内存空间来存放存活的对象。这是新生代又被划分为Eden、S0和S1三块空间的原因,Eden Space存放新创建的对象,S0和S1其中的一块用于在Minor GC触发时作为复制的目标空间。当其中一块为复制的目标空间,另一块中的内容则会被清空。因此通常由将S0、S1称为From Space和To Space,SunJDK提供了串行GC、并行回收GC和并行GC三种方式来回收新生代对象所占用的内存。对新生代对象所占用内存进行回收通常又称为Minor GC

1、串行GC(Serial GC)
当采用串行GC时,SurvivorRatio的值对应eden space/surivivor space,SurvivorRatio默认值为,例如放-Xmn设置为10M时,采用串行GC,eden space 的占用空间为8M,两个surivivor space各占1M.新生代分配内存采用的为空闲指针的方式,指针保持最后一个分配的对象在新生代内存区间的位置。当有新的对象要分配内存时,只需要检查剩余的空间是否足够存放新的对象,够则更新指针并创建对象,不够则出发Minor GC。
2、并行回收GC(Paraller Scavenge)
并行回收GC也是采用的复制算法,但是在扫面和复制时都是采用多线程实现。并且并行回收GC对大的新生代回收做了很多优化,例如动态调整eden、s0、s1的大小,在多CPU上回收时间会比串行短。适合于多CPU、暂停时间要求短的应用上。并行回收是server级别(CPU核数超过2切物理内存超过2GB)上默认采用的回收方式。
3、并行GC(ParNew)
并行GC在基于SurvivorRadio的值划分eden space和两块survivor space的方式上和串行GC一样
并行GC和并行回收GC的区别是并行GC需配合旧生代使用CMS GC,CMS GC在进行旧生代GC时,有些过程是并发进行的。如果此时发生Minor GC,需要进行处理,而并行回收GC是没有做这些处理的,也正是因为这些处理,ParNew和并行的旧生代GC无法同时使用。

旧生代和持久代可用GC

JDK提供了串行、并行、及并发三种GC来对旧生代和持久代占用的内存进行回收
1、串行
串行时和新生代内存分配相同
串行基于标记-清除-压缩实现,分为三个阶段:

  • 从根集合对象开始扫描,按照三色着色的方式对对象进行标记
  • 遍历整个旧生代,找出其中未标识的对象并进行回收
  • 执行滑动压缩,将存活的对象像旧生代空间的开始处进行滑动,最终留出一块连续的到结尾处的空间

串行执行的整个过程需暂停应用,且采用单线程模式,消耗时间较长。
2、并行
并行采用标记-压缩实现,内存分配和串行相同
其过程和串行相似,只是在多个线程上执行。
这种方式也需要耗费暂停时间,只是较上中方式可以缩短暂停时间。
3、并发(CMS:Cocurrent Mark-Sweep GC)
Mark-Sweep 方式要对整个空间的对象扫描并标记,这个过程会造成较长时间的应用暂停。有些应用对响应时间有很高的要求。因此,SunJDK提供了CMS GC,好处为GC的大部分动作均与应用并发进行.因此可以大大缩短应用暂停的时间.
CMS采用的是Mark-Sweep方式,其在回收完毕后可能会形成多个空闲空间,于是CMS采用free list的方式来记录旧生代空间中哪些部分是空闲的。当有对象需要内存时,就先要去free list寻找有哪个部分是可以放下这个内存的。多数情况下旧生代分配内存的请求都来源于Minor GC阶段,CMS这种分配旧生代内存的方式会导致Minor GC速度下降
另外,由于CMS执行过程中大部分时候适合应用并发进行的,分配内存的动作有可能和回收内存的动作同时进行,这是会造成free list激烈竞争,CMS为了避免这现象,引入了Mutual exclusion locks,以JVM分配内存为优先
CMS 执行扫面、着色和清除步骤如下:
1)、第一次标记(initial Marking)
该步骤需要暂停整个应用,扫描从根部集合对象到旧生代中可访问的对象,并对这些对象进行着色。对于着色对象,CMS采用一个外部的bit数组来进行
2)、并发标记
在初始化标记完成后,CMS恢复所有应用的线程,同时开始并发对之前着色过的对象进行轮询,以标记这些对象为可访问对象
3)、重新标记
该步骤会暂停整个应用,在2中应用可能会修改对象的引用关系或创建新对象因此要对这些改变或新建对象进行扫描,并重新着色
4)、并发收集
完成3后,恢复所有线程,这一步主要负责将没有标记的对象回收。由于内存碎片,可能会造成每次分出去的内存比回收的小,为了避免这种现象,在进行Sweeping的时候,CMS会尽量将相邻的空闲内存组成一块。采用的方法是先从free list里面删去,然后重新加入

FULL GC

除了CMS GC外,在对旧生代和持久代触发GC时,其实是对新生代、旧生代和持久代都进行GC,因此通常又称为FULL GC。当Full GC 被触发时,首先对新生代进行GC(在新生代采用的是PS GC时,可以通过-XX:-ScavgengeBeforeFullGC来禁止Full GC对新生代进行GC),然后对旧生
代GC、持久代GC。
除了直接调用System.gc外,触发Full GC执行情况有下面四种:
1)、旧生代空间不足
2)、Permanet Genatation满
Permanet Genatation存放一些class信息等,当系统中要加载的类、反射的类、和调用的方法较多时Permanet Genatation可能会被占满
3)、CMS GC出现promotion failed和concurrent mode failure
4)、统计得到的Minor GC晋升到旧生代平局大学大于旧生代GC

Garbage First GC

除了以上所说的GC外,为了能控制某个片段内GC所占用的最大暂停时间,例如100s里面最多暂停1s,以满足对响应时间有很高要求的应用,SunJDK 6 update 1以上的版本和JDK1.7中增加了一种Garbage First 的GC(G1),在此就不再详述。

对JVM内存状况的查看有很多工具,诸如GC Portal /JConsole/JVisual VM/JMap/JHat/Jstat/Eclipse Memory Analyzer