图
程序计数器 Program Counter Register,可看做是当前线程所执行的字节码的行号指示器。 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,如:分支,循环,跳转,异常处理,线程恢复等都需要依赖计数器。
一个处理器或一个内核只会执行一条线程中的指令,因为Java的多线程是通过线程轮流切换并分配处理器时间的方式来实现的。 为了线程切换后恢复到正确的执行位置,因此每个线程都有自己独立的程序计数器。
如果线程执行的是java方法,计数器记录的就是正在执行的虚拟机字节码指令的地址;如果是Native方法,计数器值为Undefined。
Java Virtual Machine Stacks也是线程私有的,生命周期与线程相同。 栈描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧Frame,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。 一个方法从调用到执行完,就对应到一个栈帧在虚拟机栈中的入栈到出栈的过程。
局部变量表存放了:基本数据类型,对象引用(reference类型,可能是一个执行对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置) 和returnAddress类型(指向一条字节码指令的地址)。也就是方法返回之后,开始执行下一行代码。
StackOverflowError:当线程请求的栈深度大于虚拟机所允许的深度,就会抛出此异常。 OutOfMemoryError:虚拟机栈动态扩展时,无法申请到足够的内存,就会抛出此异常。
与虚拟机栈非常类似,只是里面存放的是使用Native方法。
Java堆是所有线程共享的,在虚拟机启动时创建。是垃圾收集器管理的主要区域,也称作“GC堆”。 现在收集器基本都采用: 分代收集算法。 可以粗略分为:新生代,老年代,细分为:Eden区,From Survivor区,To Survivor区。 Java堆可以物理上不连续,但逻辑连续。堆没有空间分配,也无法再扩展,就会抛出OutOfMemoryError异常。
Method Area,与Java堆一样,是线程共享的,用于存储已被虚拟机加载的:类信息,常量,静态变量,即时编译器编译后的代码等数据。 对于Hotspot虚拟机,很多人把方法区称为“永久代(Permanent Generation)”,本质上却不是。
方法区的内存回收目标主要是针对:常量池的回收和类型的卸载。
Runtime Constant Pool是方法区的一部分。Class文件中除了有类的版本Version,字段Fields,方法Methods,接口Interface等描述信息, 还有一项是常量池,用于存放编译期生产的各种字面量和符号引用。
JDK1.4新加入的NIO,引入了基于通道(Channel)和缓冲区(Buffer)的IO方式,可以使用Native函数库直接分配堆外内存(直接内存), 然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。避免了Java堆和Native堆来回复制数据,显著提高性能。
对象是如何创建,如何布局以及如何访问的。
虚拟机遇到一条new指令时,首先将去检查这个指令的参数(也就是哪个类)是否能在常量池中定位到一个类的符号引用, 并且检查这个符号引用代表的类是否已被加载,解析和初始化过。没有,就必须先执行类的加载。 类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存在类加载完成后便可完全确定。 为对象分配空间就是:将一块确定大小的内存从Java堆中划分出来。
分配方式有:指针碰撞(Bump the Pointer)和空闲列表(Free List)。 指针碰撞就是内存划分为:已使用的和未使用的,指针在中间,要分配内存时,将指针往前移动指定大小的距离就可以了。 空闲列表就是对需要垃圾回收对象占用的内存区域先进行标记Mark,之后进行清除Sweep,并不进行间隔空闲内存空间的整理和压缩, 之后要分配内存时,就从空闲的内存区域中找到一块足够大的划分就行了。 使用Serial,ParNew等带Compact过程(压缩整理算法)的收集器时,Java堆是规整的,这时采用的是指针碰撞。 使用CMS这种基于Mark-Sweep算法的收集器时,使用的空闲列表。
创建对象非常频繁,仅仅修改指针所指向的位置,并发情况下也不是线程安全的(因为Java堆是所有线程共享的)。 解决方案有2种:1. 采用CAS进行同步处理并失败后重试;2. 使用本地线程分配缓存,也就是事先给各个线程划分不同的小块独立内存。
之后,虚拟机要对对象进行必要的设置,如:这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等。 这些信息存在对象的对象头(Object Header)中。
对象在内存中存储的布局可以分为3块区域:对象头Object Header,实例数据Instance Data和对齐填充Padding。
对象头包括2部分信息,第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标记,线程持有的锁,偏向线程ID,偏向时间戳等。 官方称为Mark Word,32位和64位分别为32bit和64bit的Bitmap。
对象头另一部分是:类型指针,即对象指向它的类元数据的指针,以此确定是哪个类的实例。如果是Java数组,还要在对象头中记录数组长度。
实例数据:对象真正存储的有效信息,也就是在程序代码中定义的各种类型的字段内容。
不是必然的也没什么含义,仅仅是占位符,为了满足起始地址是8字节的整数倍。
通过栈上的reference数据来操作堆上的具体对象。主流的访问方式有:使用句柄和直接指针两种。 句柄访问:Java堆中划分出一块内存作为句柄池,reference中存储的是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。 (类型数据就是类的元数据信息) 优点是:reference中存储的稳定的句柄地址,对象被移动时只会改变句柄中的实例数据指针,而reference本身不需修改。
直接指针:reference中存储的直接就是对象地址,对象的对象头里面有类型指针,指向对象的类型数据。 优点是:速度更快,节省了一次指针定位的时间开销。
Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。 GC要完成3件事情:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器家1;当引用失效时,计数器减1。 当计数器为0时,对象就是没被引用,可回收的了。但会面对循环引用的问题。
Java,C#,Lisp都是通过可达性分析(Reachablility Analysis)来判断对象是否存活。 有两种节点,一种是”GC Roots”,在堆外,一种是对象节点,在堆上。 可达性分析算法:通过GC Roots为起始点,向下搜索,走过的路径成为引用链Reference Chain。当一个对象节点到GC Roots 没有任何引用链链接,证明此对象是不可用的,也即可回收的。
GC Roots有以下几种: 虚拟机栈(栈帧中的本地变量表)中引用的对象 方法区中静态属性引用的对象 方法区中常量引用的对象 本地方法栈中JNI(即Native方法)引用的对象。
JDK1.2引用的定义: 如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。
JDK1.2以后引用分为4种: 强引用Strong,软引用Soft,弱引用Weak,虚引用Phantom。 强引用:程序代码中普遍的赋值等,如Object obj = new Object(); 软引用:一些还有用但并非必需的对象。 弱引用:比软引用还要弱,被关联的对象只能生存到下一次垃圾收集发生之前。 虚引用:最弱的引用关系,唯一目的是:关联的对象呗收集器回收时能收到一个系统通知。
可回收对象至少要经历2次标记过程: 对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它会被第一次标记并进行一次筛选。 筛选的条件是此对象是否有必要执行finalize()方法,当对象没有覆盖finalize()方法,或已经被虚拟机调用过, 这两种情况都被视为“没有必要执行”。 当被判定为有必要执行finalize方法,那么这个对象会被放置在F-Queue的队列中,稍后有一个虚拟机自动建立的,低优先级的 Finalizer现成去执行啊。finalize是对象逃脱死亡的最后机会,可以在此方法里面重新与引用链上的任何一个对象建立关联即可。
GC会对F-Queue中的对象进行第二次小规模的标记。
方法区,Hotspot的永久代主要回收两部分:废弃的常量和无用的类。 废弃的常量,也就是没有引用指向的常量,如“abc”没有任何Sting str指向它。
无用的类,判断标准: 该类所有类的实例已被回收 加载该类的ClassLoader已被回收 该类对用的java.lang.Class对象没有任何引用,无法再通过反射访问该类的方法。
在大量使用反射,动态代理,CGLib等ByteCode框架,动态生成JSP以及OSGI这类频繁自定义ClassLoader的场景, 都需要虚拟具备类卸载的功能(也就是方法区无用类的回收),以保证永久代(方法区)不会溢出。
Mark-Sweep,不足有两个:效率;产生不连续的内存碎片。 后续算法都是基于此最基础的算法做改进产生的。
将内存分成2块,在其中一块上使用,每次进行垃圾收集时,将存活的对象复制到另一块内存上,按顺序。 然后将之前的一半内存整个清理掉。缺点是:内存变成原来一半。
改进:将内存分为8:1:1的Eden和Survivor,Survivor区,每次将Eden和Survivor移到空闲的Survivor上。
Mark-Compact,标记阶段相同,整理阶段就是:让所有存活对象向一段移动,这样不连续的空闲内存碎片就得到整理了。
Generational Collection,根据对象存活周期的不同将内存划分为:新生代和老年代。 新生代中,每次垃圾收集时都有大批对象失去,少量承诺或,使用复制算法最好。 老年代中,对象存活率高,没有额外空间对它进行分配担保,采用“标记-清理”或者“标记-整理”算法。
可达性分析对时间很敏感,而且必须保持一致性,因此会造成GC停顿。 不可能遍历所有根节点来得知那些对象有引用,那些没有。 HotSpot通过一组成为OopMap的数据结构来直接得知哪些地方存放着对象引用。
导致OopMap内容变化的指令非常多,只有在“特定位置”才生成OopMap,这些位置称为安全点(SafePoint)。 必须在达到安全点时才能开始GC,进行GC停顿。
如何在GC发生时让所有线程都跑到最近的安全点上再停顿下来。有两种方案: 抢先式中断(Preemptive Suspension)和主动式中断(Voluntary Suspension)。 抢先式中断:GC发生时,把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。 主动式中断:当GC需要中断线程时,设置一个标志(和安全点是重合的),各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。
Safe Region,是指在一段代码片段中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。 但是当线程离开Safe Region时必须坚持系统是否已完成了根节点枚举(或整个GC过程),没有的话就要进行等待。
单线程的,只使用一个CPU或一条收集线程去完成垃圾收集工作,必须暂停其他所有的工作线程,直到收集结束。 是Client模式下默认的收集器,简单高效,没有线程交互开销。
ParNew就是Serial的多线程版本。是Server模式下的首选新生代收集器,能和CMS配合。
新生代收集器,使用复制算法。Parallel Scavenge目标是达到一个可控制的吞吐量,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间), 而CMS等收集器是为了缩短GC停顿时间。停顿时间越短越适合交互性的程序,良好的响应速度提升用户体验,高吞吐量适合后台大量运算的任务。 有两个参数MaxGCPauseMillis,和GCTimeRatio,也就是垃圾收集时间占比。
是Serial收集器的老年代版本,使用标记-整理算法。
是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法。
Concurrent Mark Sweep,以获取最短GC停顿时间为目标,以给用户带来较好体验,适用于互联网站或B/S系统的服务端。 基于Mark-Sweep,标记-清除算法实现的。整个GC过程分为4个步骤: 初始标记(CMS initial mark),标记一下GC Roots能直接关联到的对象,速度很快,但需要停顿 并发标记(CMS concurrent mark),进行GC Roots Tracing的过程,耗时较长,但是并行的 重新标记(CMS remark),修正并发标记期间因用户程序继续运作而导致标记产生变动的那部分对象的标记记录,需停顿。 并发清除(CMS concurrent sweep),并发执行,无需停顿。
CMS收集器的3个缺点:
- 对CPU资源敏感 因为本身是并发的,需要占用较多CPU时间,导致用户程序变慢,总吞吐量降低。
- 无法处理浮动垃圾(Floating Garbage) 会出现Concurrent Mode Failure导致Full GC产生。
- 产生内存碎片,因为基于Mark Sweep算法
G1收集器的特点:
- 并行与并发
- 分带收集
- 空间整合,G1从整体上是基于“标记-整理”,局部上是“复制”
- 可预测的停顿
G1收集器原理机制: 他讲整个Java队划分位多个大小相等的独立区域Region,并避免在整个Java队中进行全区域的垃圾收集。 G1跟踪Region里面的垃圾堆的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先收回价值最大的Region。
如何避免回收新生代时老年代的扫描? 使用Remembered Set来记录收集器中的新生代和老年代之间的对象引用关系。 当对Refrence类型进行写操作时,会暂时中断写操作,检查引用对象是否出于不同Region中。
分配担保机制:当新生代里面空间不足,而又没有可回收的对象,也没空余空间给新对象时,就会通过分配担保机制转移到老年代。
Minor GC:新生代GC,当新生代空间不足以分配对象时,就会产生Minior GC。 Full GC:老年代GC,也称Major GC,会慢10倍。 问题:Full GC什么时候发生?理论上来说,应该是新生代移动对象到老年代,空间不足,就会进行Full GC。 当Full GC完还是没空间,就会申请扩展堆空间。原则就是:老年代空间不足的时候,就会Full GC。
大对象可能直接在老年代生产。
内存回收时,必须识别哪些对象放在新生代,哪些移动老年代。 垃圾回收过程:对象在Eden出生经过第一次Minior GC后存活,放入Surviror中,对象年龄Age加1,以后每次Miniror GC都会加1. 默认到15就会被移动到老年代中,可通过-XX:MAX Ternuring Threadhold设置。
虚拟机的类加载机制: 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
Java的类型的加载,连接和初始化过程都是在程序运行期间完成的,因此具有运行期动态加载和动态连接这些特点。
类加载的生命周期: 加载,链接(验证,准备,解析),初始化,使用,卸载。