运行时数据区域
程序计数器: 是当前线程所执行的行号指示器。字节码编辑器通过改变程序计数器来选取下一条需要执行的字节码命令、分支、循环、跳转、异常等。
java虚拟机栈: 所谓的栈是指局部变量表,存放基本类型、对象引用。其中64位的long和double栈2个局部变量。其余占1个。 虚拟机栈描述的是java方法执行的内存模型。每个方法执行的时候都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接、方法出口等信息。每一个方法的调用到完成就对应这一个栈帧在虚拟机中入栈到出栈的过程。
本地方法栈: 为虚拟机使用道native服务,会抛出outOfMemoryError和starkoverflowError异常。
Java堆(被所有线程共享): 存放数组和对象实例,Java堆可以处于物理上不连续的内存空间,只要逻辑上连续。如果没有内存分配则抛出outOfMemoryError。
方法区(被所有线程共享): 用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译后的代码数据、方法区无法满足内存分配则抛出outOfMemoryError。
运行时常量池: 属于方法区的一部分,class文件中除了有类的版本、字段、方法、接口等描述外,还有一项信息是常量池。用于存放编译期生成的各种字面量和符号引用。这部分内容将在类加载后进入方法区的运行时常量池中存放【运行时常量池还将翻译出来的直接引用也存储在其中】和方法区一样会抛出outOfMemoryError。
直接内存: JDK1.4之后,NIO通过一个存储在java堆中的DireatByteBuffer对象作为这块内存直接引用进行操作,避免在Java堆和Native堆来回复制数据。动态扩展抛出outOfMemoryError。
Java堆内存分配
指针碰撞:
若Java堆中内存绝对规整,所用过的内存放一边,空闲放一边。中间放的指针作为分界点的指示器。那所分配的内存仅仅是把指针向空闲空间挪动一段与对象大小相等的距离(使用Serial、parnew带cornpact过程的收集器)
空闲列表:
若Java堆内存并不规整,虚拟机必须维护一个列表,记录内存使用情况(可用内存);在分配时从列表重找到一块足够大的空间划分给对象实例,并更新列表记录。使用CMS这种基于Mark-Sweep算法的收集器通常采用空闲列表。
对象访问定位(如何从栈上访问到堆上的对象)
句柄访问:
Java堆中将会画出一块内存来作为句柄池,reference中存储的对象句柄地址;而句柄中包含了对象具体的实例数据与类型数据各自的具体地址。
直接指针访问:
reference直接指向存储对象地址。
优缺点:
使用句柄在对象移动时不需要重新更新reference值。
使用直接指针速度快。
对象死了吗
引用计数器法:
给对象添加一个引用计数器,当引用计数器为0时,对象被回收。引用计数器无法解决循环引用问题。
可达性分析算法:
GCRoots对象作为起始点,从这个节点开始向下搜索;搜索过的路径称为GC引用链。当一个对象到GCRoot没有引用链则证明此对象不可用。不可达对象会经过两次标记过程,如果对象没覆盖finalize()或已执行finalize() 则将对象设置F-Queue队列等待垃圾收集器回收。
可以作为GCRoots的对象:
虚拟机栈中的引用对象
方法区中类静态属性引用对象
方法区中常量引用对象
本地方法栈中JNI引用对象
枚举根节点(GCRoots可达性分析)
可达性分析对执行时间敏感体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行。整个分析过程中系统看起来像被冻结在某个执行点上,不可以出现分析过程中对象关系还在不断变化,该点不满足的话分析结果无法保证。这点导致GC进行时必须停顿所有Java线程。Sun称为(stop the world)。在oopMap的协助下,hotsopt可快速且准确的完成GCRoot枚举。hotSpot在安全点做了文章。
安全点:
safepoint让GC发生时让所有线程都跑到安全点停下来:
抢先式中断:
不需要线程执行代码主动配合,在gc发生时,首先把所有线程全部中断。发现有线程中断地方不在安全点则让线程跑到安全点。目前没有虚拟机采用此方式暂停线程响应gc条件。
主动中断式:
不对线程直接操作,设置一个标识,给个线程执行时,主动轮询标志。发现中断标识为真时,自己中断。
对象引用类型
强引用:
Object O = new Object();
软引用:
描述一些还有用但是非必须对象,对于软引用关联的对象在系统将要发生内存溢出时这些对象将被列入回收范围之中,进行二次回收;二次回收之后内存不足抛出异常。
弱引用:
用来描述非必须对象,弱引用关联的对象只能活到下一次垃圾回收之前。
虚引用:
最弱的引用关系,也称幽灵引用。为一个对象设置虚引用的目的为了对象回收时收到一个系统通知。
废弃对象判断
1、该对象所有的实例都已经被回收,java堆中不存在该类的任何实例
2、加载该类的classLoader已被回收
3、该类的java.class.Class对象没有在任何地方引用,无法在任何地方通过反射访问该类
垃圾收集算法
标记-清除法(mark-sweep):
首先表示所有需要回收对象,在标记完成后统一回收所有标记对象。
缺点:
1.效率不高
2.标记清除后产生很多内存碎片,在给大对象分配内存时无法找到足够空间,需要提前触发一次垃圾回收。
复制算法(Copying):
将内存分为一块较大的Eden空间和2块较小的survior空间,每次使用Eden空间和一块survior空间。当回收时,将Eden和Survior存活对象Copy到另一块survior空间,清理Eden和Survior空间。Eden和Survior比例为:8:1.只有10%浪费,若另一块survior空间不够用时,需要老年代进行内存担保(新生带的对象一般都是朝生夕死)。
缺点:
需要老年代进行担保。
标记整理法(mark-compact):
先标记存活对象,将存活对象移向一端,然后清理掉端界以外的内存。
分代收集算法(Generational Collection):
将Java堆分为新生带和老年代,在新生代中每次垃圾回收都发现有大批对象死去,选用复制法。老年代对象存活率较高,没有额外内存担保就使用标记-清除或标记整理。
垃圾收集器
Serial 收集器:
最基础、最老。基于单线程的收集器,采用复制算法。所谓单线程是指在进行垃圾回收时必须停掉其他所有工作线程。
优点:简单高效,对于限定单个cpu,Serial收集器没有线程交互的开销,专心做垃圾回收。适用于运行在client模式下,默认新生代收集器。
ParNew收集器:
ParNew收集器就是Serial收集器多线程版本,采用复制算法 ;除使用多线程进行垃圾收集之外,其余行为包括serial可用参数、收集算法、stop the world对象分配规则,回收策略-都与 Serial一样。ParNew在单cpu环境中绝对不会有比Serial更好的效果。甚至优于存在线程交互的开销该收集器在通过超线程技术实现的两个cpu环境中都不能百分之百保证可以超越Serial。当然随着使用cpu的数据增加。他对gc时系统资源的有效利用还是有好处的。它默认开启与cpu数量相等的的线程数。
优点:
运行在server模式下,虚拟机中首选的新生代收集器,除了Serial外,目前只有ParNew能和CMS收集器工作。CMS只作为老年代收集器。只选择和新生代的Serial或ParNew一起工作。
Parallel Scavenge收集器(paraller:并行 scavenge:清理):
新生代收集器,使用复制算法收集,又是并行的多线程收集器。Parallel Scavenge 收集器目的是为了达到可控吞吐量。
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
Serial old 收集器:
Serial old 是 Serial 收集器的老年代版本,同样单线程。使用标记-整理算法。主要给client 模式下虚拟机用。如果在server模式下,在jdk1.5及之前与Parallel Scavenge 搭配使用。作为CMS收集器后背预案。
Parallel old 收集器:
是Parallel Scavenge 收集器的老年代版本,使用多线程和标记整理算法。“吞吐优先” 和Parallel Scavenge 组合。
CMS收集器(concurrent mark sweep):
是一种以获取最短回收停顿时间为目标的收集器。CMS收集器基于标记清除算法实现,它的运作过程如下:
1.初始标记:
标识一下GCRoots能直接关联到的对象,速度很快
2.并发标记:
进行GCRoots tracing过程
3.重新标记:
修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段停顿时间会比初始标记稍长 但远比并发标记短。
4.并发清除:
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作。从总体来讲,CMS收集器的内存回收过程与用户线程并行执行。拥有低停顿特点。
缺点:
1.CMS收集器对cpu资源非常敏感,默认启动线程:(cpu数量+3)/4,CMS在垃圾回收时占用cpu资源较大,在cpu缺乏或者单cpu时不推荐使用。
2.CMS收集器无法处理浮动垃圾,可能出现 “concurrent mode failure” 失败而导致另一次 full gc发生。由于CMS在并发清理阶段用户线程还在运行着,伴随程序运行自然产生垃圾这部分垃圾出现在标记过程之后,CMS无法在当次收集器中处理掉他们。只好留下待下一次full gc时清理。这部分垃圾称为“浮动垃圾”。 也是由于垃圾收集阶段用户线程还在运行,那也就需要预留足够的内存空间给用户线程使用。因此,CMS收集器不能像其他收集器那样等到老年代几乎被填满了在进行收集。需要预留一部分空间提供并发收集时程序还运作使用。jdk1.6中cms启动阈值已提升至92%。要是CMS运行期间预留的内存无法满足程序需要就会出现concurrent 莫得filure失败,这时虚拟机将启动后备预案:临时启动serial old 收集器来进行老年代的垃圾收集。这样停顿时间很长。
3.CMS是基于:标记-清除算法实现。会产生大量的垃圾碎片。将会导致在大对象分配内存时无法找到合适的内存空间,不得提前触发一次full gc。为了解决这个问题 CMS一个-xx:usecmscompactatfullcollection参数用于在cms收集器顶不住要进行full gc时,开启内存碎片合并整理。这个过程导致停顿时间变长。虚拟机还提供了一个参数,--xx:cmsfullgcbeforecompaction用于设置执行多次不压缩的full gc后,跟着来一次带压缩的。
G1收集器:
G1将内存分为多个大小相等的独立区域,虽然还保留新生代和老年代概念,新生代和老年代不在进行物理隔离,他们都是一部分Region的集合。G1之所以能建立可以预测的停顿模型是因为他可以有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面垃圾堆积的价值大小在后台维护一个优先列表。每次根据允许的收集时间,优先回收最大Region。这种方式保证了G1在有限时间内可以获取尽可以高的收集率。面向服务端垃圾收集器与其他收集器相比G1具有如下特点:
1.并行与并发:
G1能充分利用cpu多核环境下优势,使用多个cpu来缩短“stop the world” 停顿时间。部分其他收集器原本需要停顿java线程执行gc动作。G1收集器仍可以通过并发方式让java线程继续执行。
2.分代收集:
以其他收集器一样,分代概念在G1中依然保留。虽然G1可以不需要其他收集器配合就能独立掌管gc堆,但它能够采用不同方式去处理新创建的对象和已经存活了一段时间,熬过多次gc的旧对象以获取更好的收集效果。
3.空间整合:
与CMS不同,G1基于 标记-整理算法实现的收集器,意味着G1运行期间不会产生内存空间碎片。收集后提供规整的可用内存。利于程序运行分配大对象时不会因为无法找到内存空间提起触发gc。
4.可预测的停顿:
能让使用者明确指定在 M毫秒内消耗在垃圾收集上的时间不得超过N毫秒。
G1运作步骤:
初始标记:
标记GCRoots能直接关联的对象,并修改 next top at mark start的值;让下一阶段用户程序并发运行时能正确可用Region中创建对象。需要停顿线程但是耗时很短。
并发标记:
从GCRoots开始对对堆中对象进行可达性分析,找出存活对象,耗时长可以和用户程序并发执行。
最终标记:
修改在并发标记期间因用户程序继续运行而导致标记产生变动的那部分记录。虚拟机将这段时间的变动记录在线程remembered set logs 里,最终标记阶段将 Remembered set logs 合并 Remembered set 中,这个间段需要停顿线程但可以并行。
筛选回收:
对各个Region的回收价值和成本进行排序,根据用户所期望的gc停顿时间来制定回收计划,理论上可以并发执行。