JVM-3.内存

目录
一、运行时数据区
二、内存使用细节:以HotSpot的堆为例
三、实战:OutOfMemoryError异常
四、垃圾收集器(堆+方法区)与内存分配策略
 
 
 
一、运行时数据区
1、程序计数器(线程私有)
(1)存放内容:如果线程正在执行Java方法,计时器记录正在执行的虚拟机字节码指令的地址,如果是native方法,值为空。
 
2、Java虚拟机栈(线程私有)
(1)存放内容:每个方法在执行时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用开始到执行结束,对应一个栈帧在虚拟机栈里入栈和出栈的过程。
(2)其他
局部变量表存放基本数据类型、对象引用和returnAddress类型。其中long和double占两个slot,其他占1个。局部变量表所需内存空间在编译器确定。
在概念模型的讨论中,认为虚拟机栈的每个栈帧所需内存大小在编译期就可以确定;但是有时运行期JIT编译器可能进行一些优化。
 
3、本地方法栈(线程私有):与虚拟机栈类似,为native方法服务。
 
4、堆(线程共享)
(1)存放内容:所有对象实例和数组在堆上分配(一些新技术的发展导致这一点不绝对,略)
(2)其他
在虚拟机启动时创建;
一般是最大的一块;
堆是GC管理的主要区域,因此也称为GC堆;
堆的划分:新生代和老生代(回收角度);TLAB:线程私有的分配缓冲区;
堆空间可连续可不连续(逻辑上连续),可选择固定大小或可扩展。
 
5、方法区(线程共享)
(1)存放内容:存储已加载的类信息、常量、静态变量、即时编译器编译后的代码等;JVM将方法区描述为堆的一个逻辑部分。
(2)其他
HotSpot曾将方法区实现为永久代,便于管理内存,但是遇到一些问题(如更容易出现内存溢出,略),逐步将放弃这种实现。
方法区可连续可不连续(逻辑上连续),可选择固定大小或可扩展,可选择不收集;该区域内存回收的主要目标是对常量池的回收和对类型的卸载。
 
6、运行时常量池(线程共享)
(1)存放内容:方法区的一部分;存放Class文件常量池(编译期生成的字面量和符号引用),一般也存放符号引用翻译成的直接引用。此外,运行时常量池具有动态性,可以在运行期加入新的常量(如String.intern())
 
7、直接内存:不是VM运行时数据区的一部分;NIO中使用native函数分配的堆外内存。
 
 
二、内存使用细节:以HotSpot的堆为例
1、对象创建过程(Java对象,不包括数组和Class对象)
(1)检查:当虚拟机遇到new指令时,首先检查指令后的参数能够定位到常量池中的一个符号引用;并检查符号引用所代表的类,是否已经加载解析初始化,如果没有,则执行类加载。
(2)堆中分配内存
大小:所需内存大小在加载完成后确定。
分配方式:分配内存的过程即在堆中划出一块区域来,主要有指针碰撞和空闲列表两种。如果堆内存工整(使用和空闲的分开),移动分界点指针位置即可,这是指针碰撞;如果不工整(使用和空闲的交错),则会有一个列表维护记录使用情况,这是空闲列表。Java堆是否工整,取决于使用的GC算法是否有压缩整理功能;因此使用Serial、ParNew等带Compact的GC时,指针碰撞;而使用CMS这种基于Mark-Sweep的GC,空闲列表。
(内存分配层面的)线程安全:同步技术(如CAS+失败重试);缓存技术(TLAB),即为不同线程预先分配一小块缓存。
(3)初始化:零值(不包括对象头)。
(4)设置对象头:包括对象是哪个类的实例,如何找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息。
(5)初始化:此时对象已经创建完成;这个初始化并不是new,而是new后的invokespecial(一般都有)。
(6)注意:程序中的new一般至少对应字节码指令中的new和invokespecial;创建对象应该还有一个将引用入栈的步骤,同样发生在指令的new中(而不仅是程序的new中)。
以下是函数和字节码的对应关系:
public class Test1 {
	public void f(){
		new String();
	}
	public void g(){
		String s = new String();
	}
}

JVM-3.内存

 
2、对象在内存中的分布
(1)对象头:包括运行时数据(Mark Word)和类型指针。
Mark Word包括哈希码,GC分代年龄、锁状态标志、线程持有的锁、偏向线程的id、偏向时间戳等;数据长度与虚拟机位数相同(32或64),为实现这个目的采用空间复用(如何复用在《JVM》系列中有详述)。
如果对象是数组,对象头还包含数组长度;因为通过Java对象的元数据信息可以获得对象大小,却不能通过数组对象的元数据信息获得数组的大小。
(2)实例数据
排列顺序:默认策略是long/double、int、short/char、byte、boolean、reference;在此基础上,父类在前。策略可改变,略。
(3)对齐填充:对象起始地址要求是8字节整数倍。
 
3、对象的访问(栈上的reference)
(1)句柄:Java堆中划分出句柄池;栈中reference指向对象的句柄位置,句柄中包含了实例数据和类型数据的地址(对象头呢?)。
优势:如果对象移动(GC时常常发生),只需要改变句柄的值,不用改变reference。
JVM-3.内存
JVM-3.内存
(2)直接指针:如下所示。优势:速度快,少一次定位。HotSpot使用这种定位方式。
JVM-3.内存
 JVM-3.内存
 
 
三、实战:OutOfMemoryError异常
1、所有内存区域中,只有程序计数器不会出现OutOfMemoryError(OOM);虚拟机栈和本地方法栈还可能出现*Error。
2、略
 
 
 
四、垃圾收集器(堆+方法区)与内存分配策略
1、找出不可用对象
(1)引用计数算法:实现简单,效率高;难以解决循环引用的问题,主流JVM都未使用。
(2)可达性分析算法:通过一系列称为GC Roots的对象为起点向下搜索,搜索所走过的路径称为引用链;若一个对象到GC Roots没有引用链相连,则该对象不可用。
GC Roots包括:
虚拟机栈(各个帧栈中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即native方法)引用的对象
Remembered Set【我自己加的】
(3)finalize的最后一搏
注意,垃圾回收器要回收一个对象要进行两次标记过程:第1次,如果通过GC Roots不可达,则进行第一次标记,并进行一次筛选,条件是该对象是否要执行finalize()方法;如果该对象没有覆盖finalize()或已经执行过一次finalize(),则没有必要执行。如果该对象要进行finalize(),则放入F-Queue队列中,由一个虚拟机自动建立的、低优先级的Finalizer线程执行;但虚拟机只是出发这个方法,不承诺等待它运行结束(运行过慢或死循环可能导致F-Queue中的其他对象永久等待,GC崩溃)。第2次,GC对F-Queue中的对象进行第2次小规模标记,如果仍然不可达,就要真的回收了。
 
2、方法区的回收
(1)JVM规范没有规定方法区一定要回收;方法区回收性价比很低,在堆中,尤其是新生代中,一般应用一次垃圾回收可以回收75%-90%的空间,但方法区效率低很多。Hotspot实现中(目前是),方法区实现为永久代。
(2)回收内容:废弃常量和无用的类。
常量:包括字符串、类/接口/方法/字段等的符号引用;回收与堆类似。
无用的类:必须满足以下3个条件;满足条件的也不一定被回收,可以通过参数进行设置。在大量使用反射、动态代理、CGLib、动态生成JSP以及OSGi等场景都需要支持类卸载,以保证永久代不会溢出。
前提条件:(1)所有实例被回收(2)加载该类的ClassLoader被回收(3)该类对于的Class对象没有被引用,无法在任何地方通过反射访问该类的方法
 
3、垃圾收集算法
(1)分代收集算法:分代是垃圾回收的大框架;新生代一般使用复制算法;老年代一般使用标记-清理算法或标记-整理算法。
(2)标记-清除算法(Mark-Sweep)
效率问题:标记和清除的效率都不高
空间问题:标记清除之后会产生大量不连续的内存碎片,大对象分配空间时容易内存不足从而出发另一次GC
(3)复制算法:内存分成大小相等的两块,GC时将可用对象复制到另一个内存块。问题:内存缩小为原来的一半。新生代的高朝生夕死比(低存活率/低存活时间),使得复制算法的效率问题和空间问题都大大减轻。
GC一般用复制算法回收新生代。IBM的研究表明,新生代中的对象98%都是朝生夕死的;因此可以不用按1:1分配新生代,一般分为1个大的Eden区和2个较小的Survivor区;具体原理见下述补充。Hotspot默认Eden:Survivor大小比例为8:1,因此新生代可用空间为Eden+一个Survivor,每次浪费10%的空间。
【补充(来自《大型网站技术架构》):将应用程序的堆空间分为年轻代和年老代,年轻代又分为Eden区、From区和To区,新建对象总是在Eden区创建,当Eden区已满,触发一次YoungGC,将还在使用的对象复制到From区,这样Eden区就空了,可以继续创建对象,当Eden区再次用完,再触发一次YoungGC,将Eden和From区还在使用的对象复制到To区,下一次Young GC则是将Eden区和To区还在使用的对象复制到From区。经过多次YoungGC,某些对象会在From和To之间多次复制,如果超过某个阈值对象还没有释放,就复制到年老代。如果年老代空间用完,就会触发FullGC,即全量回收。全量回收对系统性能影响较大,因此应根据业务对象特点,合理设置年轻代和年老代的大小,减少FullGC。】
YoungGC的速度是FullGC的十倍以上,执行比较频繁;一般情况下,FullGC至少伴有一次YoungGC,但是并不绝对,如PS里直接FullGC的策略。YoungGC也称Minor GC,FullGC也称MajorGC。
分配担保:如果另外一块Survivor空间不足以存放上一次新生代收集下来的存货对象,这些对象直接通过分配担保机制进入老年代;这个过程仍然属于Young GC。
(4)标记-整理算法(Mark-Compact)
 
4、Hotspot如何发起GC
(1)枚举根节点:枚举根节点一定会Stop the world(即便是CMS或G1),因为这项工作要保证一致性;如果遍历方法区,停顿无法接受。
(2)优化:HotSpot是准确式GC,即VM知道某段内存是什么类型。因此,使用OopMap数据结构,当类加载完时记录对象内的引用类型的偏移量,在JIT编译过程中,也会在特定的位置记录栈和寄存器中哪些位置是引用。【问题,如果不用JIT就不记录吗?还是一定会用JIT?】
(3)问题:许多指令可能改变Oop内容,但如果为每个指令生成Oop,空间效率太低。
(4)优化:只在特定位置记录,这些位置称为Safepoint;程序只到Safepoint时停顿可能GC;Safepoint太少会导致GC等待时间太长,太多会导致空间效率低且运行成本高。一般选择跳转类,如方法调用、循转跳转、异常跳转。
(5)问题:GC时如何保证各个线程都到了Safepoint。
抢先式中断:先全部中断,没到Safepoint的继续跑,没有VM使用这种方式。
主动式中断:不操作线程,而是设置一个标志;当各个线程执行时主动轮询,如果标志为真则中断。轮询标志的地方包括安全点,加上创建对象需要分配内存的地方。
(6)问题:如果线程没有分配CPU时间,如Sleep或Blocked。
解决方案:安全区域,即该区域内引用关系不变,在区域内任何地方开始GC都是安全的。安全区域可以理解为扩展的Safepoint。
当线程执行到Safe Region的代码时,进行标识;JVM如果GC,就不管进入Safe Region的线程;当线程要离开Safe Region时,检查系统是否完成了枚举根节点(或整个GC),如果完成则继续执行,如果没有则等可以执行的信号。
 
5、垃圾收集器(Hotspot)
JVM-3.内存
(1)概述
如图所示,连线表示两个收集器可以搭配使用;没有最好/万能的收集器,只有最合适的。
GC语境下的并行(Parallel),指多条垃圾回收线程并行工作,并发(Concurrent)指用户线程与垃圾回收线程并行执行(同时或交替)。
(2)Serial:新生代,复制算法,单线程,Stop the world
单线程收集器;不仅仅说明只使用一个CPU或一个线程去完成垃圾收集工作,而且它工作时必须暂停所有其他工作线程,直到收集结束,即Stop The World。
Serial是Client模式下默认新生代收集器,因为简单高效(与其他单线程比),Client模式下收集速度很快。
(3)ParNew:新生代,复制算法,多线程,Stop the world【Serial的多线程版本】
ParNew在许多Server模式下是首选新生代虚拟机,一个原因是只有Serial和ParNew可以与CMS搭配。使用CMS时,ParNew是默认新生代虚拟机。
线程数:单核心机器ParNew性能不如Serial,双核也不能保证,越多越厉害;默认开启线程数与逻辑CPU(即核心)个数相同,CPU很多时可以用参数限制。
(4)Parallel Scavenge:新生代,复制算法,多线程,Stop the world
吞吐量优先收集器:其他收集器如CMS优化目标是缩短用户线程的停顿时间,Parallel Scavenge的优化目标是高吞吐量(即CPU用在用户代码上的时间比例);前者适合交互,后者适合后台运算。可以使用-XX:MaxGCPauseMillis或-XX:GCTimeRatio控制暂停时间或吞吐量。
自适应调节策略:-XX:+UseAdaptiveSizePolicy开启,不需要手动指定新生代大小(-Xmn)、Eden和Survivor的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshhold)等参数;虚拟机动态调整。
(5)Serial Old:老年代,标记-整理,单线程,Stop the world
用途:Client模式下使用;Server下:Parallel Old出来之前与Parallel Scavenge合作;CMS的后背预案,并发收集发生Concurrent Mode Failure时使用。
(6)Parallel Old:老年代,标记-整理,多线程,Stop the world【Parallel Scavenge的老年代版本】
在Parallel Old出现之后,Parallel Scavenge收集器才真正有了用武之地;Parallel Scavenge+Serial Old不一定比ParNew+CMS给力。
(7)CMS:老年代,标记-清理,多线程,尽可能少的Stop the world
Concurrent Mark Sweep:以获取最短停顿时间为目标;实现思路是将Stop the world的操作尽量压缩。
分为4步:
初始标记:标记GC Roots能够直接关联的对象;Stop the world;时间很短
并发标记:GC Roots Tracing;不Stop the world;时间较长
重新标记:对上一阶段产生的变动进行修正;Stop the world;时间很短
并发清理:不Stop the world;时间较长
缺点与优化:CPU敏感(因为并发);浮动垃圾(因为并发);零碎空间(因为Mark-Sweep);详细略。其中需要注意,因为CMS运行时用户线程也在运行,因此CMS进行时要在老年代预留内存;如果CMS时预留内存不够,会产生Concurrent Mode Failure,启动后备预案:Serial Old。
(8)G1:新生代/老年代,复制(局部,即两个Region之间)+标记-整理(整体),多线程,尽可能少的Stop the world
Garbage-First:面向服务端应用;可预测的停顿时间模型;G1可以不需要其他收集器独立完成收集。
基本原理:将整个Java堆分为大小相等的Region,仍保留新生代和老年代的概念,但不再是物理隔离的,都是Region(不需要连续)的一部分。G1跟踪每个Region里面垃圾堆积的价值大小(回收获得空间及所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region;这样G1可以避免在整个Java堆中进行全区域的垃圾回收,因此能建立可预测的停顿时间模型。
不同区域互相引用问题:在G1的不同Region之间,以及其他收集器的新生代与老年代之间,如何避免扫描全堆?使用Remembered Set:当JVM检查到对Reference数据进行写操作时,检查Reference引用的对象是否处于不同Region中(或检查老年代Reference是否引用了新生代对象),如果是,则写入Remembered Set中。GC时,将Remembered Set加入Roots。
分为4步:初始标记,并发标记,最终标记,筛选回收;前3步与CMS类似;筛选回收阶段对各个Region的回收价值进行排序,然后根据用户期望的GC停顿时间制定回收计划。筛选回收阶段可以不Stop the world;但由于只回收一部分Region而且停顿时间可控,因此这一阶段也可以Stop the world。
性能测试:略。
 
6、理解GC日志
[GC [PSYoungGen: 2621K->552K(76288K)] 2621K->552K(249856K), 0.0008705 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 552K->0K(76288K)] [ParOldGen: 0K->468K(173568K)] 552K->468K(249856K) [PSPermGen: 2555K->2554K(21504K)], 0.0091921 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
(1)开头的[GC和[Full GC说明了垃圾收集的停顿类型,而不是区分老年代GC和新生代GC;如果有Full,说明是Stop-The-World类型的。如果是调用System.gc()触发的收集,有些版本可能显示Full GC(System)。
(2)2621K->552K(76288K):类似的3个数据分表表示,GC前该内存区域已使用容量,GC后该内存区域已使用容量,该内存区域总容量(没有中括号的表示Java堆总内存)。
(3)[Times: user=0.00 sys=0.00, real=0.00 secs]:分表表示用户态CPU消耗时间,内核态CPU消耗时间,GC开始到结束的墙钟时间。墙钟时间包括了IO等时间;但如果多核或多CPU,前两者之和仍有可能大于墙钟时间。
(4)内存区域(与GC相关)
PSYoungGen:Parallel Scavenge收集器的新生代;其他自己类推
 
7、内存分配与回收策略
(1)内存分配主要是堆上分配(JIT后可能栈上分配);对象主要分配在新生代的Eden区,如果启用了TLAB,将按线程优先在TLAB上分配;少数情况直接分配在老年代。
(2)不同收集器的分配规则可能不太相同;下面以Serial/Serial Old为例,ParNew/Serial Old类似。
(3)规则
对象优先在Eden分配:若Eden没有空间,发起Minor GC。
大对象直接进入老年代:PretenureSizeThreshold,该参数适用于Serial和ParNew,不适用于PS。
长期存活对象进入老年代:MaxTenuringThreahold;默认15岁
动态对象年龄判断:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,则年龄大于或等于该年龄的对象直接进入老年代,无须等到MaxTenuringThreahold。
空间分配担保:老年代进行担保,老年代空间不够就MajorGC;具体较复杂,略。HandlePromotionFailure设置是否允许担保失败,一般要设置,否则MajorGC过于频繁。
 
 
五、参考《深入理解Java虚拟机》
上一篇:关于nginx upstream的几种配置方式


下一篇:Linux学习之CentOS(一)--CentOS6.4环境搭建