目录
一、运行时数据区
二、内存使用细节:以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();
	}
}

 
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。

(2)直接指针:如下所示。优势:速度快,少一次定位。HotSpot使用这种定位方式。
 

 
 
三、实战:OutOfMemoryError异常
1、所有内存区域中,只有程序计数器不会出现OutOfMemoryError(OOM);虚拟机栈和本地方法栈还可能出现StackOverflowError。
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)
(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虚拟机》

JVM-3.内存的更多相关文章

  1. jvm的内存分配总结

    最近看了周志明版本的<深入理解Java虚拟机>第一版和第二版,写的很好,收获很多,此处总结一下.   jvm中内存划分:   如上图,一共分为五块,其中: 线程共享区域为: 1.java堆 ...

  2. java语言:Linux与JVM的内存关系分

    在一些物理内存为8g的服务器上,主要运行一个Java服务,系统内存分配如下:Java服务的JVM堆大小设置为6g,一个监控进程占用大约 600m,Linux自身使用大约800m.从表面上,物理内存应该 ...

  3. 【转】JVM 堆内存设置原理

    堆内存设置 原理 JVM堆内存分为2块:Permanent Space 和 Heap Space. Permanent 即 持久代(Permanent Generation),主要存放的是Java类定 ...

  4. JVM的内存区域划分

            JVM的内存区域划分 学过C语言的朋友都知道C编译器在划分内存区域的时候经常将管理的区域划分为数据段和代码段,数据段包括堆.栈以及静态数据区.那么在Java语言当中,内存又是如何划分的 ...

  5. 转: 关于Linux与JVM的内存关系分析

    转自: http://tech.meituan.com/linux-jvm-memory.html Linux与JVM的内存关系分析 葛吒2014-08-29 10:00 引言 在一些物理内存为8g的 ...

  6. 设置TOMCAT的JVM虚拟机内存大小

    你知道如何设置TOMCAT的JVM虚拟机内存大小吗,这里和大家分享一下,JAVA程序启动时JVM都会分配一个初始内存和最大内存给这个应用程序.这个初始内存和最大内存在一定程度都会影响程序的性能. 设置 ...

  7. 解决JVM最大内存设置问题

    这里和大家讨论一下如何获得JVM最大内存,在命令行下用java-XmxXXXXM-version命令来进行测试,然后逐渐的增大XXXX的值,如果执行正常就表示指定的内存大小可用,否则会打印错误信息. ...

  8. 图解JVM在内存中申请对象及垃圾回收流程

    http://longdick.iteye.com/blog/468368 先看一下JVM的内存模型: 从大的方面来讲,JVM的内存模型分为两大块: 永久区内存( Permanent space )和 ...

  9. Linux与JVM的内存关系分析

    引言 在一些物理内存为8g的server上,主要执行一个Java服务,系统内存分配例如以下:Java服务的JVM堆大小设置为6g,一个监控进程占用大约600m,Linux自身使用大约800m. 从表面 ...

  10. jvm的内存管理【转】

    [转]JVM内存管理 这些日子一直在研究jvm内存管理的东西,网上的知识很多,总结一下,能沉淀下来的就是自己的! 首先,刚学java的时候就知道java类文件是以 .java为后缀的文件,经过java ...

随机推荐

  1. Html 经典布局(三)

    经典布局案例(三): <!DOCTYPE html> <html lang="en"> <head> <meta charset=&quo ...

  2. Jquery一些实用函数

    1.jQuery.parseJSON( json )第一个参数json的类型是字符串: var obj = jQuery.parseJSON( '{ "name": "J ...

  3. 时间同步方法及几个可用的NTP服务器地址

    大家都知道计算机电脑的时间是由一块电池供电保持的,而且准确度比较差经常出现走时不准的时候.通过互联网络上发布的一些公用网络时间服务器NTP server,就可以实现自动.定期的同步本机标准时间. 依靠 ...

  4. 多个git账号的SSH配置

    一般使用git都只需要维持一个默认的git账户就可以打天下了. 但如果自己确实需要多个git账号的需求的话,就有必要配置多个ssh key了. 首先为生成多个ssh key ssh-keygen -t ...

  5. EMMC与RAND的区别

    作者:Younger Liu, 本作品采用知识共享署名-非商业性使用-相同方式共享 3.0 未本地化版本许可协议进行许可. EMMC与RAND的区别 说到两者的区别,必须从flash的发展历程说起,因 ...

  6. [Linux] Linux 中的基本命令与目录结构

    Linux 中的基本命令与目录结构 目录 一.Linux 基本目录结构 二.基本命令 三.浏览目录 四.中间命令 五.更改密码 六.环境变量和 shell 变量 七.命令路径 八.文本编辑器 九.获取 ...

  7. Dockerfile 构建镜像 - 每天5分钟玩转容器技术(13)

    Dockerfile 是一个文本文件,记录了镜像构建的所有步骤. 第一个 Dockerfile 用 Dockerfile 创建上节的 ubuntu-with-vi,其内容则为: 下面我们运行 dock ...

  8. poj3261 Milk Patterns 后缀数组求可重叠的k次最长重复子串

    题目链接:http://poj.org/problem?id=3261 思路: 后缀数组的很好的一道入门题目 先利用模板求出sa数组和height数组 然后二分答案(即对于可能出现的重复长度进行二分) ...

  9. [刷题]算法竞赛入门经典(第2版) 6-4/UVa439 6-5/UVa1600

    比较忙比较累,只贴代码了. 题目:6-4 UVa439 - Knight Moves //UVa439 - Knight Moves //Accepted 0.000s //#define _XIEN ...

  10. 栈的Java简单实现

    关于栈 栈(Stack)是限定只能在一段进行插入和删除操作的线性表. 进行插入和删除操作的一端称为“栈顶”(top),另一端称为“栈底”(bottom). 栈的插入操作称为“入栈”(push),栈的删 ...