程序计数器是一块较小的内存空间,指示当前线程正在执行的字节码指令地址(如果执行的是Native方法,则计数器为空,因为Native方法是底层的C/C++方法,由系统执行,jvm无法获取)。解释器通过改变程序计数器的值来选取下一条要执行的字节码指令。程序计数器是jvm规范中唯一没有规定任何OutOfMemoryError情况的区域,因为计数器中改变的只是指令的地址,并不会有新的内存需求。 每个线程的计数器都是独立存储的,互不影响,属于线程私有内存(生命周期随线程而生,随线程而亡)。因为jvm的多线程执行是通过线程轮流切换并分配处理器执行时间(CPU时间片轮转)的方式来实现的,一个处理器(多核时一个内核)同一时刻只有一个线程在执行,线程切换后要保证能恢复到正确的执行位置。
java虚拟机栈也是线程私有的,生命周期同线程相同。虚拟机栈描述的是方法执行的内存模型:每个方法执行时都会创建一个栈帧(stack Frame,是方法运行时的基础数据结构),用来存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法执行的过程,就对应一个栈帧在虚拟机栈的入栈和出栈。平时常说的栈一般是局部变量表部分。 平时常说的栈一般是局部变量表部分。包含基本数据类型、对象引用和returnAddress类型(字节码指令地址)。long和double占两个局部变量空间(64位系统和jdk1.7后的版本还占用两个吗?QA),其他类型占一个。内存空间大小在编译期分配,运行期不会改变。栈深度大于jvm允许的深度会抛出StackOverFlowError,如果无法申请到足够的内存会抛出OutOfMemoryError.
类似于虚拟机栈,区别在于本地方法栈用于Native方法,由虚拟机自由实现,HotSpot甚至把它和虚拟机栈合二为一。也会抛出StackOverFlowError和OutOfMemoryError。
jvm所管理的内存中最大的一块,所有线程共享(但可以划分线程私有的分配缓冲区TLAB?QA),jvm启动时创建。所有的对象实例都在堆上分配(JIT编译期发展和逃逸分析技术,栈上分配、标量替换技术,该句话不再绝对)。 堆是GC的主要区域。可细分为:新生代、老年代,Eden、From Survivor、To Survivor。当内存无法满足时,抛出OutOfMemoryError.
是线程共享的,存储已被jvm加载的类信息、常量、静态变量、即时编译后的代码等。在jvm规范中,其为堆的一个逻辑部分,但其别名叫Non-Heap. HotSpot中方法区常被成为“永久代”(官方计划调整到Native Memory),HotSpot把GC扩展到了方法区,或者说使用永久代来实现方法区。其他虚拟机是不存在永久代的。会抛出OutOfMemoryError。
方法区的一部分(jdk1.7版本已经把字符串常量池移出),类加载后存放到方法区的常量池中,运行期也可能将新的常量放入池中(String.intern()).
不属于jvm,NIO类引入了一种基于通道与缓存区(Buffer)的I/O方式,可以使用Native函数库直接分配堆外内存,然后使用堆中的DirectByteBuffer作为这块内存的引用,避免了在堆和Native堆中来回复制。jvm参数(如-Xmx)的不当,也会引起OutOfMemoryError。
jvm遇到new指令时,先检查常量池中是否有类的符号引用,并检查该符号引用对应的类是否已被加载和解析、初始化。如果没有,则先执行类加载过程。加载完成,jvm为新生对象分配内存,内存大小在类加载完成后就可以完全确定。 内存分配有两种方式:
- 指针碰撞:堆中内存绝对规整,用过的内存放一边,空闲的放一边,中间通过指针作为分界点。内存分配就是把指针往空闲区移动对象大小需要的空间。
- 空闲列表:内存不规整,已使用的和空闲的内存交错,jvm维护一个列表来记录哪些内存块可用,分配的时候找到足够大的一块空间分配给对象实例。 分配方式由堆是否规整决定,而堆是否规整由GC是否带有压缩功能确定。Serial、ParNew采用指针碰撞,CMS采用空闲列表。 内存分配在线程并发情况下存在线程安全问题,解决方式有两种:CAS和本地线程分配缓存(Thread Local Allocation Buffer,TLAB)(分配新的TLAB时,采用同步锁定,-XX:+/-UseTLAB).
对象在内存中存储的布局分为三块区域:
- 对象头 包含两部分信息: 1.对象自身的运行时数据:如哈希码、GC分代年龄、锁状态标记、线程持有的锁、偏向线程ID、偏向时间戳。这部分数据32位和64位(未开启压缩指针)jvm下分别为32bit和64bit,官方成为"Mark Word",对象头信息与对象自身定义的数据无关的额外存储成本。是一个非固定的数据结构,会根据自己的状态复用自己的存储空间。32位下,如果是未锁定状态,25bit存储HashCode,4bit存储分代年龄,2bit存储锁标志位,1bit固定为0。其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)存储分布动态变化。 2.类型指针:对象指向它的类的源数据的指针,通过这个指针确定对象是哪个类的实例。如果是数组,对象头中还有一块记录数组的长度的数据。
- 实例数据:对象真正存储的有效信息,代码中定义的字段内容。存储顺序受jvm分配策略参数(FieldsAllocationStyle)和字段定义顺序影响。HotSpot默认策略是longs/doubles、ints、shorts/chars,bytes/booleans、oops(Ordinary Object Pointers),相同宽度的字段被分配到一起。父类变量默认在子类之前,如果CompactFields设为true,子类中较窄的会插入到父类的空隙中。
- 对齐填充:自动内存管理要求对象起始地址必须的8字节(?QA)的整数倍,对象实例数据没有对齐时,通过对齐填充补全。
通过栈上的reference来操作堆上的对象,访问方式取决于虚拟机,主流的实现方式有两种:句柄和直接指针。
- 句柄
堆中分出一块内存作为句柄池,reference是句柄地址,句柄包含了类型数据和对象实例数据各自的具体地址信息。好处是reference中存储吃句柄地址是稳定的,对象移动(GC)时只改变句柄中的实例数据指针。
- 直接指针:reference是对象地址,速度更快,HotSpot采用这种方式实现。
给对象添加一个引用计数器,有引用计数加一,引用失效计数减一,计数为0时表示对象不再被使用。jvm没有选用该方式来管理内存,因为它很难解决对象之间循环引用的问题,如下述代码。
O o1 = new O();
O o2 = new O();
o1.property = o2;
o2.property = o1;
o1 = null;
o2 = null;
o1、o2相互引用对方,引用计数分别为2,设置为null时,引用计数为1,但实际上,这两个对象都不会再访问,应该被GC。
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,证明此对象不可用。
GC Roots对象类型
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的变量
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
无论是引用计数还是可达性分析,判断对象存活都和“引用”有关。JDK1.2之前,引用的定义:reference类型的数据中,存储的数值是代表另一块内存的起始地址。1.2之后,引用的概念进行扩充:强引用,软引用,弱引用,虚引用。
- 强引用:类似“Object obj = new Object()”,只要强引用还在,永远不会GC。
- 软引用:描述还有用但非必须对象。系统将要内存溢出时,会把这些对象列进回收范围中进行二次回收,如果回收后还没足够的内存,才会内存溢出。JDK1.2之后,提供SoftReference类来实现软引用。
- 弱引用:也是描述非必须对象,强度比软引用更弱一些。GC时,无论内存是否足够,都会回收掉只被弱引用关联的对象,所以只被弱引用关联的对象,只能生存到下一次GC之前。JDK1.2之后,提供WeakReference类来实现弱引用。
- 虚引用:也称为幽灵引用和幻影引用,最弱一种引用关系。一个对象是否有虚引用的存在,不影响其生存时间,也无法通过虚引用来取得一个对象的实例。虚引用只是为了这个对象GC时收到一个系统通知。JDK1.2之后,提供PhantomReference类实现。
对象真正被回收,要经历两次标记,1在进行可达性分析后,发现没有与GC Roots相链接的引用链,会进行第一次标记,并进行是否有必要执行finalize()方法的筛选,如果没有覆盖finalize或者已经执行过,视为没有必要执行,进行第二次标记,之后等待回收。
方法区gc的性价比比较低,主要回收:废弃常量、无用的类(只是可能回收,条件:1.该类所有实例都已经回收;2.加载该类的ClassLoader已经回收;3.该类对应的java.lang.Class对象没有任何地方引用,无法在任何地方通过反射访问该类的方法)。 在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP及OSG这类频繁自定义ClassLoader的场景,都需要虚拟机具备类卸载的功能,以保证永久代(方法区)不会溢出。
最基础的算法。分为标记和清除两个阶段:先标记,之后进行清除。不足之处:
- 效率问题:标记和清除过程效率都不高。
- 空间问题:会产生大量不连续的内存碎片,进而影响较大对象分配内存时,没有足够的连续内存,从而引起GC。
内存分为两块,每次使用一块,内存用完时,将存活的对象复制到另一块内存上,清理当前的内存空间。好处是没有内存碎片,代价是内存利用率减少。如:Eden和Survivor,老年代对Survivor空间不够时进行分配担保。
标记过程与标记-清除算法一样,后续不是直接清理,而是让所有存活的对象都像一端移动,清理端边界以外的内存。
如:堆分为新生代和老年代,根据各个代的特点选择适当的收集算法。新生代中,每次都有大批对象死去,就选用复制算法。老年代中存活率高,而且没有额外空间进行分配担保,就必须使用MS或者MC算法。
准确式GC,虚拟机通过一组OopMap的数据结构,直接得知哪些地方存放着对象引用,GC扫描时快速的完成GC Roots的枚举。
引起OopMap内容变化的指令特别多,每条指令都生成对应的OopMap,GC空间成本将非常高。HotSpot只在特点位置记录OopMap,这些位置称为安全点。安全点选定基本上以程序“是否具有让程序长时间执行的特征”为标准的。长时间执行最明显特征是指令序列复用,如:方法调用、循环跳转、异常调整等。 GC发生时,让所有的线程(不包含JNI调用的线程)跑到最近的安全点上再停顿下来,有:
- 抢先式中断:先中断全部线程,发现有线程不在安全点,就恢复线程,跑到安全点。
- 主动式中断:不直接对线程操作,只是设置一个标记,各线程主动轮询,发现标记就运行到安全点后中断。
如果线程sleep或blocked,线程就无法响应jvm的中断请求,走到安全点中断挂起,就需要安全区域解决。安全区域是指在一段代码片段中,引用关系不会发生变化,可以看作扩展的安全点。线程执行到安全区域时,会先标识自己进入了安全区域,这样,JVM进行GC时,就不用管标识为安全区域状态的线程了,离开安全区域时,会检查系统是否已经完成了根节点枚举(或GC过程),完成就继续执行,否则等待收到可以离开安全区域的信号在进行执行。
最基本、历史最悠久的收集器,单线程收集器,进行GC时,必须暂停其他所有工作线程,直到GC结束。
Serial的多线程版本。
是一个新生代收集器,使用复制算法。目标是可控制的吞吐量。吞吐量是指CPU运行代码的时间与CPU总耗时的比值。能更高效的利用CPU,适合后台运算,不需要太多交互的任务。吞吐量和停顿时间成反比,停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。比如:新生代小,GC就快,但是收集频率就会高,总体耗时加大。
Serial的老年代版本,同样是单线程,使用标记-整理算法。与Parallel Scavenge搭配使用,或作为CMS的后备预案。
Parallel Scavenge的老年代版本,使用多线程和标记-整理算法。
是以获取最短回收停顿时间为目标的收集器。基于标记-清除算法。运作过程:
- 初始标记:标记GC Roots能直接关联到的对象,速度很快。
- 并发标记:GC Roots Tracing的过程,耗时最长,能和用户线程一起工作。
- 重新标记:修正并发标记期间用户程序继续运作导致标记变动的那一部分对象的标记记录,停顿时间远小于并发标记。
- 并发清除:耗时较长,能和用户线程一起工作。
初始标记和重新标记,需要“Stop the world”,但这两个阶段耗时很短。 缺点:
- 对CPU资源非常敏感。默认线程数是(cpu数量+3)/4,cpu少时,导致用户程序可用线程少,降低执行速度。
- 无法处理浮动垃圾。并发清理阶段用户线程运行产生的新垃圾,只能留待下次GC清理。为了保证有足够内存空间给用户线程,无法像其他收集器一样等老年代几乎填满再进行GC,默认老年代使用92%就会激活GC。
- 基于标记-清除,会产生空间碎片,进而引起Full GC。
整体基于标记-整理算法,句柄(两个Region)基于复制,不再特定用于新生代或老年代,规划堆的时候,划分为大学相等的独立区域(Region ),新生代和老年代就不再是物理隔离。特点:并行与并发、分代收集、空间整合、可预测的停顿。运作步骤大致如下:
- 初始标记
- 并发标记:停顿线程,但用户线程可并发执行。
- 最终标记:修正并发标记期间用户程序继续运作导致标记变动的那一部分对象的标记记录,需要停顿线程,但可并行执行。
- 筛选回收
对象的内存分配,大方向上讲,就是在堆上分配(也可能经过JIT编译后拆散为标量类型直接分配到栈上),主要分配在新生代的eden上,如果空间不足,将发起一次Minor GC。。如果启动了本地线程分配缓存,将按现场优先在TLAB上分配。
新生代GC(Minor GC):复制算法,较频繁,速度快 老年代GC(Major GC/FULL GC):速度慢
大对象直接进入老年代,大对象指需要大量连续空间的对象,如:很长的字符串或数组(避免大对象,尤其是短命大对象)。大对象会导致内存空间还比较多时提前触发垃圾收集器。通过-XX:PretenureSizeThreshold(只对Serial和ParNew生效)来控制大于设置值的,直接进入老年代,避免Eden区和Survivor去发生大量的内存复制。 长期存活的对象会进入老年代,对象每次Minor GC存活下来,并能被Survivor容纳,则年龄增加1岁,默认达到15岁,进入老年代。 动态对象年龄判定:在survivor中,相同年龄所有对象大小总和大于survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到默认年龄设置。 空间分配担保,在进行Minor GC前,虚拟机都会检查老年代最大连续可用空间是否大于新生代对象总大小或者历次晋升到老年代对象的平均大小,如果一个满足(jdk6 update 24之后,之前会通过参数HandlePromotionFailure控制,true同24之后的半版本,false则不判断历次晋升到老年代对象的平均大小,直接进行Full GC),则进行minor GC,都不满足时,进行Full GC。
主要选项:
- -q:只输出LVMID(本地虚拟机唯一ID:Local Virtual Mechine Identifier)
- -m:输出虚拟机进程启动时传递给主类main函数的参数
- -l:输出主类全名
- -v:输出虚拟机进程启动时JVM参数
Class文件采用类似于C语音结构体的伪结构来存储数据,只有两种数据类型:无符号数和表。 无符号数是基本的数据类型,用u1、u2、u4、u8分别代表1、2、4、8字节的无符号数。可以表示:数字、索引引用、数量值或UTF-8构成的字符串值。 表由多个无符号数或者其他表作为数据项构成的复合数据类型。 Class文件u4(魔数)+u2(次版本号)+u2(主版本)+常量池