Open www1350 opened 7 years ago
JDK:包括编译器(javac.exe)、开发工具(javadoc.exe、jar.exe、keytool.exe、jconsole.exe)和更多的类库(如tools.jar)等。总结就是Java语言、Java虚拟机、Java API类库 JRE: 支持java运行的基本环境
JIT:just in time
运行时数据区:
当前代码行号指示器。各个线程计数器互不影响,独立存储。如果是执行Java方法,是虚拟机字节码地址。如果是native方法,计数器为空。唯一一个没有OOM的区域。是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
描述Java方法执行的内存模型:每个执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等。每个方法调用到执行完成过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。线程私有。
局部变量表:
存放编译期可知的各种基本数据类型、对象引用类型(一个指向对象起始地址的引用指针)、returnAddress类型(一个字节码指令地址)。其中64位长度long和double会占用2个局部变量空间,其余占一个。局部变量表所需内存大小是在编译期间完成分配的,方法运行期间不会改变。如果栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常;如果虚拟机栈动态扩张时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
运行时常量:
运行时常量是方法区的一部分(无法再申请内存时自然OOM)。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。Java虚拟机对于Class文件每一部分(包括常量池)格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范才会被认可、装载和运行。但是运行时常量池,每个虚拟机提供商都可以按自己需要实现。一般Class文件中描述的符号引用和直接引用都存储在其中。除了编译器外,运行过程也可以放入,比如String的intern()方法。
为虚拟机使用的Native方法服务。一样会抛出StackOverflowError和OutOfMemoryError。线程私有。
存放对象实例,被所有线程共享。所有对象实例以及数组都要在堆上分配,但随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致所有对象都在堆上分配也不是那么绝对。从内存分配看,可以划分出多个线程私有分配缓冲区。
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。(各个线程共享)。方法区是堆的一个逻辑部分,但是他有个别名非堆,为了和Java堆区区分。
JDK1.4引入的NIO,引入了一种基于通道与缓冲区的I/O方式,可以使用native函数库直接分配堆外内存,通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样避免Java堆和native堆中来回复制数据。(内存受到RAM、SWAP或分页大小以及处理器寻址空间的限制)
new的时候:1.检查指令参数是否能在常量池中定位到一个类的符号引用2.检查这个符号引用代表的类是否已被加载、解析、初始化过3.有就执行相应类加载过程4.类加载检查通过后,虚拟机为新生对象分配内存(假设Java堆内存绝对规整,用过的一边没用过的一边,中间放着指针作为分界点的指示器,指针则挪动和对象大小的距离,这叫做“指针碰撞”;假如不规整,已使用和空闲交错,虚拟机就必须维护一个列表,记录哪些内存块可用,在分配的时候从列表找到一个足够大的空间划分给对象实例,并更新列表上的记录,这叫做“空闲列表”;使用Serial、ParNew等带Compact过程收集器,采用指针碰撞,使用CMS基于Mark-Sweep算法收集器,采用空闲列表)
并发情况下不是线程安全的,存在正在给A分配内存,指针还没来得及修改,对象B又同时使用原来指针来分配内存。有两种解决方法,一、对分配内存空间的动作进行同步处理——虚拟机采用CAS+失败重试保证更新原子性;二、按内存分配动作按线程划分不同空间进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓存(TLAB)。哪个线程要分配,就在TLAB上分配,用完才分配新的,才需要同步锁定。(-XX:+/-UseTLAB)
内存分配后,会初始化内存的零值。接下来,虚拟机对对象进行必要设置,存放信息(对象是哪个类实例、如何找类元信息、哈希码、GC分代年龄)在对象头。从虚拟机角度对象已经创建,从java角度对象才刚刚开始创建init还没开始执行,所有字段为0。执行new指令以后接着执行init(invokespecial)
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头包括两部分信息:运行时数据和类型指针。
用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
32bit中25bit存储对象哈希码,4bit存储对象分代年龄,2bit存储锁标志位,1bit固定为0
对象头另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。 (并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据并不一定要经过对象本身,可参考对象的访问定位)
这部分分配顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oop,从分配策略中可以看出,相同宽度的字段总是分配到一起。
HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。
Java程序需要通过栈上的引用数据来操作堆上的具体对象。对象的访问方式取决于虚拟机实现,目前主流的访问方式有使用句柄和直接指针两种。
可以理解为指向指针的指针,维护指向对象的指针变化,而对象的句柄本身不发生变化;指针,指向对象,代表对象的内存地址。
优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而引用中存储的直接就是对象地址。
优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。
原理:给每个对象添加一个计数器,被引用计数器就加一;引用失效就减一;计数器为0就对象回收 缺陷:两对象之间循环引用则无法回收
原理:通过一系列“GC Roots”对象称为起始点,从这些结点开始向下搜索,搜索走过的链路称为引用链路,当GC不可达的时候会被判定可回收。
Java里,可以作为GC Roots对象的有:
1.虚拟机栈(栈帧本地变量)引用的对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈(JNI)引用的对象
强引用:Object a = new Object() 只要强引用存在,永远不会回收
软引用:描述有用非必须。系统内存即将发生溢出异常之前,会在回收范围内进行第二次回收。SoftReference
弱引用:非必需。强度比软引用弱,被弱引用关联对象只能生存到下一次垃圾收集之前。无论内存是否足够,都会回收被弱引用关联的对象。WeakReference
虚引用:最弱的。完全不会对生存时间构成影响,无法通过虚引用取到一个对象实例。作用是这个对象被收集器回收时收到一个系统通知。PhantomReference
优先在Eden区分配 在JVM内存模型一文中, 我们大致了解了VM年轻代堆内存可以划分为一块Eden区和两块Survivor区. 在大多数情况下, 对象在新生代Eden区中分配, 当Eden区没有足够空间分配时, VM发起一次Minor GC, 将Eden区和其中一块Survivor区内尚存活的对象放入另一块Survivor区域, 如果在Minor GC期间发现新生代存活对象无法放入空闲的Survivor区, 则会通过空间分配担保机制使对象提前进入老年代(空间分配担保见下).
大对象直接进入老年代 Serial和ParNew两款收集器提供了-XX:PretenureSizeThreshold的参数, 令大于该值的大对象直接在老年代分配, 这样做的目的是避免在Eden区和Survivor区之间产生大量的内存复制(大对象一般指 需要大量连续内存的Java对象, 如很长的字符串和数组), 因此大对象容易导致还有不少空闲内存就提前触发GC以获取足够的连续空间.
长期存活的对象将进入老年代 VM为每个对象定义了一个对象年龄(Age)计数器, 对象在Eden出生如果经第一次Minor GC后仍然存活, 且能被Survivor容纳的话, 将被移动到Survivor空间中, 并将年龄设为1. 以后对象在Survivor区中每熬过一次Minor GC年龄就+1. 当增加到一定程度(-XX:MaxTenuringThreshold, 默认15), 将会晋升到老年代.
幸存区相同年龄对象的占幸存区空间的多于其一半,将进入老年代 然而VM并不总是要求对象的年龄必须达到 MaxTenuringThreshold才能晋升老年代: 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代, 而无须等到晋升年龄.
空间担保分配(老年代剩余空间需多于幸存区的一半,否则要Full GC)
标记-清除算法:标记(可达性分析)出所有需要回收的对象,标记完成后统一回收。
不足:效率低、不连续碎片多(没有足够连续空间分配内存,提前触发另一次垃圾回收)
适用:对象存活率高的老年代。
复制算法:将可用内存分为两块,每次只使用其中一块,内存用完了就将存活对象复制到另一块并清理。一般会分为内存较大的Eden和两块内存较小的Surivivor(默认8:1:1),内存回收的时候会将Eden和Surivivor存活的对象一次性复制到另一块Surivivor,并且清理空间。(Surivivor不够用则向Old区进行分配担保)
不足:内存缩小为原来的一般,代价高。浪费50%的空间。
适用:新生代。
标记-整理算法:标记出所有需要回收的对象,所有存活对象都向一端移动,直接清理端以外内存
适用:老年代。
Eden区满后触发minor GC,将所有存活对象复制到一个Survivor区,另一Survivor区存活的对象也复制到这个Survivor区中,始终保证有一个Survivor是空的。当Survivor空间不够用(不足以保存尚存活的对象)时, 需要依赖Old区进行空间分配担保机制, 这部分内存直接进入Old区。
Young区Survivor满后触发minor GC后仍然存活的对象存到Old区,如果Survivor区放不下Eden区的对象或者Survivor区对象足够老了,直接放入Old区,如果Old区放不下则触发Full GC。
在方法区进行垃圾回收一般”性价比”较低, 因为在方法区主要回收两部分内容: 废弃常量和无用的类。 回收废弃常量与回收其他年代中的对象类似, 但要判断一个类是否无用则条件相当苛刻:
但即使满足以上条件也未必一定会回收, Hotspot VM还提供了-Xnoclassgc参数控制(关闭CLASS的垃圾回收功能). 因此在大量使用动态代理、CGLib等字节码框架的应用中一定要关闭该选项, 开启VM的类卸载功能, 以保证方法区不会溢出。
在执行Minor GC前, VM会首先检查老年代是否有足够的空间存放新生代尚存活对象, 由于新生代使用复制收集算法, 为了提升内存利用率, 只使用了其中一个Survivor作为轮换备份, 因此当出现大量对象在Minor GC后仍然存活的情况时, 就需要老年代进行分配担保, 让Survivor无法容纳的对象直接进入老年代, 但前提是老年代需要有足够的空间容纳这些存活对象. 但存活对象的大小在实际完成GC前是无法明确知道的, 因此Minor GC前, VM会先首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小, 如果条件成立, 则进行Minor GC, 否则进行Full GC(让老年代腾出更多空间).
然而取历次晋升的对象的平均大小也是有一定风险的, 如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然可能导致担保失败(Handle Promotion Failure, 老年代也无法存放这些对象了), 此时就只好在失败后重新发起一次Full GC(让老年代腾出更多空间).
枚举根结点 必须停顿所有Java线程(STW stop the world),采用OopMap数据结构检查所有上下文和全局的引用位置
安全点 每条指令都生成OopMap很耗费空间,实际上HotSpot没有为每条指令生成OopMap,而是只在特定位置记录,这些位置被称为“安全点”。(标准是 是否让程序长时间执行,如循环、方法调用等)。让GC在安全点停顿,HotSpot采用主动式中断(当GC需要中断线程的时候,只是标记,各个线程轮训这个标志,发现这个标志为真则挂起线程,和安全点重合则加上创建线程需要分配的位置)
新生代GC(Minor GC)
老年代GC (Major/Full GC)
Serial收集器
Serial收集器是Hotspot运行在Client模式下的默认新生代收集器, 它的特点是 只用一个CPU/一条收集线程去完成GC工作, 且在进行垃圾收集时必须暂停其他所有的工作线程(“Stop The World” -后面简称STW).
虽然是单线程收集, 但它却简单而高效, 在VM管理内存不大的情况下(收集几十M~一两百M的新生代), 停顿时间完全可以控制在几十毫秒~一百多毫秒内.
ParNew收集器
ParNew收集器其实是前面Serial的多线程版本, 除使用多条线程进行GC外, 包括Serial可用的所有控制参数、收集算法、STW、对象分配规则、回收策略等都与Serial完全一样(也是VM启用CMS收集器-XX: +UseConcMarkSweepGC的默认新生代收集器).
由于存在线程切换的开销, ParNew在单CPU的环境中比不上Serial, 且在通过超线程技术实现的两个CPU的环境中也不能100%保证能超越Serial. 但随着可用的CPU数量的增加, 收集效率肯定也会大大增加(ParNew收集线程数与CPU的数量相同, 因此在CPU数量过大的环境中, 可用-XX:ParallelGCThreads参数控制GC线程数).
Parallel Scavenge收集器
与ParNew类似, Parallel Scavenge也是使用复制算法, 也是并行多线程收集器. 但与其他收集器关注尽可能缩短垃圾收集时间不同, Parallel Scavenge更关注系统吞吐量:
系统吞吐量=运行用户代码时间(运行用户代码时间+垃圾收集时间)
停顿时间越短就越适用于用户交互的程序-良好的响应速度能提升用户的体验;而高吞吐量则适用于后台运算而不需要太多交互的任务-可以最高效率地利用CPU时间,尽快地完成程序的运算任务. Parallel Scavenge提供了如下参数设置系统吞吐量:
Serial Old收集器
Serial Old是Serial收集器的老年代版本, 同样是单线程收集器,使用“标记-整理”算法:
Parallel Old收集器
Parallel Old是Parallel Scavenge收老年代版本, 使用多线程和“标记-整理”算法, 吞吐量优先, 主要与Parallel Scavenge配合在 注重吞吐量 及 CPU资源敏感 系统内使用:
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一款具有划时代意义的收集器, 一款真正意义上的并发收集器, 虽然现在已经有了理论意义上表现更好的G1收集器, 但现在主流互联网企业线上选用的仍是CMS(如Taobao、微店).
CMS是一种以获取最短回收停顿时间为目标的收集器(CMS又称多并发低暂停的收集器), 基于”标记-清除”算法实现, 整个GC过程分为以下4个步骤:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark: GC Roots Tracing过程)
重新标记(CMS remark)
并发清除(CMS concurrent sweep: 已死象将会就地释放, 注意: 此处没有压缩)
其中两个加粗的步骤(初始标记、重新标记)仍需STW. 但初始标记仅只标记一下GC Roots能直接关联到的对象, 速度很快; 而重新标记则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录, 虽然一般比初始标记阶段稍长, 但要远小于并发标记时间.
(由于整个GC过程耗时最长的并发标记和并发清除阶段的GC线程可与用户线程一起工作, 所以总体上CMS的GC过程是与用户线程一起并发地执行的.
由于CMS收集器将整个GC过程进行了更细粒度的划分, 因此可以实现并发收集、低停顿的优势, 但它也并非十分完美, 其存在缺点及解决策略如下:
CMS默认启动的回收线程数=(CPU数目+3)4
当CPU数>4时, GC线程最多占用不超过25%的CPU资源, 但是当CPU数<=4时, GC线程可能就会过多的占用用户CPU资源, 从而导致应用程序变慢, 总吞吐量降低.
无法处理浮动垃圾, 可能出现Promotion Failure、Concurrent Mode Failure而导致另一次Full GC的产生: 浮动垃圾是指在CMS并发清理阶段用户线程运行而产生的新垃圾. 由于在GC阶段用户线程还需运行, 因此还需要预留足够的内存空间给用户线程使用, 导致CMS不能像其他收集器那样等到老年代几乎填满了再进行收集. 因此CMS提供了-XX:CMSInitiatingOccupancyFraction参数来设置GC的触发百分比(以及-XX:+UseCMSInitiatingOccupancyOnly来启用该触发百分比), 当老年代的使用空间超过该比例后CMS就会被触发(JDK 1.6之后默认92%). 但当CMS运行期间预留的内存无法满足程序需要, 就会出现上述Promotion Failure等失败, 这时VM将启动后备预案: 临时启用Serial Old收集器来重新执行Full GC(CMS通常配合大内存使用, 一旦大内存转入串行的Serial GC, 那停顿的时间就是大家都不愿看到的了).
最后, 由于CMS采用”标记-清除”算法实现, 可能会产生大量内存碎片. 内存碎片过多可能会导致无法分配大对象而提前触发Full GC. 因此CMS提供了-XX:+UseCMSCompactAtFullCollection开关参数, 用于在Full GC后再执行一个碎片整理过程. 但内存整理是无法并发的, 内存碎片问题虽然没有了, 但停顿时间也因此变长了, 因此CMS还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction用于设置在执行N次不进行内存整理的Full GC后, 跟着来一次带整理的(默认为0: 每次进入Full GC时都进行碎片整理).
分区收集- G1收集器
与其他基于分代的收集器不同, G1将整个Java堆划分为多个大小相等的独立区域(Region), 虽然还保留有新生代和老年代的概念, 但新生代和老年代不再是物理隔离的了, 它们都是一部分Region(不需要连续)的集合.
每块区域既有可能属于O区、也有可能是Y区, 因此不需要一次就对整个老年代/新生代回收. 而是当线程并发寻找可回收的对象时, 有些区块包含可回收的对象要比其他区块多很多. 虽然在清理这些区块时G1仍然需要暂停应用线程, 但可以用相对较少的时间优先回收垃圾较多的Region(这也是G1命名的来源). 这种方式保证了G1可以在有限的时间内获取尽可能高的收集效率.
基本概念
JDK:包括编译器(javac.exe)、开发工具(javadoc.exe、jar.exe、keytool.exe、jconsole.exe)和更多的类库(如tools.jar)等。总结就是Java语言、Java虚拟机、Java API类库 JRE: 支持java运行的基本环境
JIT:just in time
运行时数据区:
JVM的内存结构
程序计数器:
当前代码行号指示器。各个线程计数器互不影响,独立存储。如果是执行Java方法,是虚拟机字节码地址。如果是native方法,计数器为空。唯一一个没有OOM的区域。是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
Java虚拟机栈:
描述Java方法执行的内存模型:每个执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等。每个方法调用到执行完成过程,对应一个栈帧在虚拟机栈中入栈到出栈的过程。线程私有。
局部变量表:
存放编译期可知的各种基本数据类型、对象引用类型(一个指向对象起始地址的引用指针)、returnAddress类型(一个字节码指令地址)。其中64位长度long和double会占用2个局部变量空间,其余占一个。局部变量表所需内存大小是在编译期间完成分配的,方法运行期间不会改变。如果栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常;如果虚拟机栈动态扩张时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
运行时常量:
运行时常量是方法区的一部分(无法再申请内存时自然OOM)。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。Java虚拟机对于Class文件每一部分(包括常量池)格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范才会被认可、装载和运行。但是运行时常量池,每个虚拟机提供商都可以按自己需要实现。一般Class文件中描述的符号引用和直接引用都存储在其中。除了编译器外,运行过程也可以放入,比如String的intern()方法。
本地方法栈:
为虚拟机使用的Native方法服务。一样会抛出StackOverflowError和OutOfMemoryError。线程私有。
Java堆:
存放对象实例,被所有线程共享。所有对象实例以及数组都要在堆上分配,但随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致所有对象都在堆上分配也不是那么绝对。从内存分配看,可以划分出多个线程私有分配缓冲区。
方法区:
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。(各个线程共享)。方法区是堆的一个逻辑部分,但是他有个别名非堆,为了和Java堆区区分。
直接内存:
JDK1.4引入的NIO,引入了一种基于通道与缓冲区的I/O方式,可以使用native函数库直接分配堆外内存,通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样避免Java堆和native堆中来回复制数据。(内存受到RAM、SWAP或分页大小以及处理器寻址空间的限制)
对象的创建:
new的时候:1.检查指令参数是否能在常量池中定位到一个类的符号引用2.检查这个符号引用代表的类是否已被加载、解析、初始化过3.有就执行相应类加载过程4.类加载检查通过后,虚拟机为新生对象分配内存(假设Java堆内存绝对规整,用过的一边没用过的一边,中间放着指针作为分界点的指示器,指针则挪动和对象大小的距离,这叫做“指针碰撞”;假如不规整,已使用和空闲交错,虚拟机就必须维护一个列表,记录哪些内存块可用,在分配的时候从列表找到一个足够大的空间划分给对象实例,并更新列表上的记录,这叫做“空闲列表”;使用Serial、ParNew等带Compact过程收集器,采用指针碰撞,使用CMS基于Mark-Sweep算法收集器,采用空闲列表)
并发情况下不是线程安全的,存在正在给A分配内存,指针还没来得及修改,对象B又同时使用原来指针来分配内存。有两种解决方法,一、对分配内存空间的动作进行同步处理——虚拟机采用CAS+失败重试保证更新原子性;二、按内存分配动作按线程划分不同空间进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓存(TLAB)。哪个线程要分配,就在TLAB上分配,用完才分配新的,才需要同步锁定。(-XX:+/-UseTLAB)
内存分配后,会初始化内存的零值。接下来,虚拟机对对象进行必要设置,存放信息(对象是哪个类实例、如何找类元信息、哈希码、GC分代年龄)在对象头。从虚拟机角度对象已经创建,从java角度对象才刚刚开始创建init还没开始执行,所有字段为0。执行new指令以后接着执行init(invokespecial)
对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头
对象头包括两部分信息:运行时数据和类型指针。
运行时数据
用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
32bit中25bit存储对象哈希码,4bit存储对象分代年龄,2bit存储锁标志位,1bit固定为0
类型指针
对象头另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。 (并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据并不一定要经过对象本身,可参考对象的访问定位)
数据类型
这部分分配顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oop,从分配策略中可以看出,相同宽度的字段总是分配到一起。
对齐填充
HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。
对象访问定位
Java程序需要通过栈上的引用数据来操作堆上的具体对象。对象的访问方式取决于虚拟机实现,目前主流的访问方式有使用句柄和直接指针两种。
句柄
可以理解为指向指针的指针,维护指向对象的指针变化,而对象的句柄本身不发生变化;指针,指向对象,代表对象的内存地址。
优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
直接指针
如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而引用中存储的直接就是对象地址。
优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。
垃圾回收算法
引用计数法
原理:给每个对象添加一个计数器,被引用计数器就加一;引用失效就减一;计数器为0就对象回收 缺陷:两对象之间循环引用则无法回收
可达性分析
原理:通过一系列“GC Roots”对象称为起始点,从这些结点开始向下搜索,搜索走过的链路称为引用链路,当GC不可达的时候会被判定可回收。
Java里,可以作为GC Roots对象的有:
1.虚拟机栈(栈帧本地变量)引用的对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈(JNI)引用的对象
引用
强引用:Object a = new Object() 只要强引用存在,永远不会回收
软引用:描述有用非必须。系统内存即将发生溢出异常之前,会在回收范围内进行第二次回收。SoftReference
弱引用:非必需。强度比软引用弱,被弱引用关联对象只能生存到下一次垃圾收集之前。无论内存是否足够,都会回收被弱引用关联的对象。WeakReference
虚引用:最弱的。完全不会对生存时间构成影响,无法通过虚引用取到一个对象实例。作用是这个对象被收集器回收时收到一个系统通知。PhantomReference
JVM内存分配策略
优先在Eden区分配 在JVM内存模型一文中, 我们大致了解了VM年轻代堆内存可以划分为一块Eden区和两块Survivor区. 在大多数情况下, 对象在新生代Eden区中分配, 当Eden区没有足够空间分配时, VM发起一次Minor GC, 将Eden区和其中一块Survivor区内尚存活的对象放入另一块Survivor区域, 如果在Minor GC期间发现新生代存活对象无法放入空闲的Survivor区, 则会通过空间分配担保机制使对象提前进入老年代(空间分配担保见下).
大对象直接进入老年代 Serial和ParNew两款收集器提供了-XX:PretenureSizeThreshold的参数, 令大于该值的大对象直接在老年代分配, 这样做的目的是避免在Eden区和Survivor区之间产生大量的内存复制(大对象一般指 需要大量连续内存的Java对象, 如很长的字符串和数组), 因此大对象容易导致还有不少空闲内存就提前触发GC以获取足够的连续空间.
长期存活的对象将进入老年代 VM为每个对象定义了一个对象年龄(Age)计数器, 对象在Eden出生如果经第一次Minor GC后仍然存活, 且能被Survivor容纳的话, 将被移动到Survivor空间中, 并将年龄设为1. 以后对象在Survivor区中每熬过一次Minor GC年龄就+1. 当增加到一定程度(-XX:MaxTenuringThreshold, 默认15), 将会晋升到老年代.
幸存区相同年龄对象的占幸存区空间的多于其一半,将进入老年代 然而VM并不总是要求对象的年龄必须达到 MaxTenuringThreshold才能晋升老年代: 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半, 年龄大于或等于该年龄的对象就可以直接进入老年代, 而无须等到晋升年龄.
空间担保分配(老年代剩余空间需多于幸存区的一半,否则要Full GC)
垃圾回收算法
标记-清除算法:标记(可达性分析)出所有需要回收的对象,标记完成后统一回收。
不足:效率低、不连续碎片多(没有足够连续空间分配内存,提前触发另一次垃圾回收)
适用:对象存活率高的老年代。
复制算法:将可用内存分为两块,每次只使用其中一块,内存用完了就将存活对象复制到另一块并清理。一般会分为内存较大的Eden和两块内存较小的Surivivor(默认8:1:1),内存回收的时候会将Eden和Surivivor存活的对象一次性复制到另一块Surivivor,并且清理空间。(Surivivor不够用则向Old区进行分配担保)
不足:内存缩小为原来的一般,代价高。浪费50%的空间。
适用:新生代。
标记-整理算法:标记出所有需要回收的对象,所有存活对象都向一端移动,直接清理端以外内存
适用:老年代。
新生代和老年代的回收策略
Eden区满后触发minor GC,将所有存活对象复制到一个Survivor区,另一Survivor区存活的对象也复制到这个Survivor区中,始终保证有一个Survivor是空的。当Survivor空间不够用(不足以保存尚存活的对象)时, 需要依赖Old区进行空间分配担保机制, 这部分内存直接进入Old区。
Young区Survivor满后触发minor GC后仍然存活的对象存到Old区,如果Survivor区放不下Eden区的对象或者Survivor区对象足够老了,直接放入Old区,如果Old区放不下则触发Full GC。
永久代-方法区回收
在方法区进行垃圾回收一般”性价比”较低, 因为在方法区主要回收两部分内容: 废弃常量和无用的类。 回收废弃常量与回收其他年代中的对象类似, 但要判断一个类是否无用则条件相当苛刻:
但即使满足以上条件也未必一定会回收, Hotspot VM还提供了-Xnoclassgc参数控制(关闭CLASS的垃圾回收功能). 因此在大量使用动态代理、CGLib等字节码框架的应用中一定要关闭该选项, 开启VM的类卸载功能, 以保证方法区不会溢出。
空间分配担保
在执行Minor GC前, VM会首先检查老年代是否有足够的空间存放新生代尚存活对象, 由于新生代使用复制收集算法, 为了提升内存利用率, 只使用了其中一个Survivor作为轮换备份, 因此当出现大量对象在Minor GC后仍然存活的情况时, 就需要老年代进行分配担保, 让Survivor无法容纳的对象直接进入老年代, 但前提是老年代需要有足够的空间容纳这些存活对象. 但存活对象的大小在实际完成GC前是无法明确知道的, 因此Minor GC前, VM会先首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小, 如果条件成立, 则进行Minor GC, 否则进行Full GC(让老年代腾出更多空间).
然而取历次晋升的对象的平均大小也是有一定风险的, 如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然可能导致担保失败(Handle Promotion Failure, 老年代也无法存放这些对象了), 此时就只好在失败后重新发起一次Full GC(让老年代腾出更多空间).
枚举根结点 必须停顿所有Java线程(STW stop the world),采用OopMap数据结构检查所有上下文和全局的引用位置
安全点 每条指令都生成OopMap很耗费空间,实际上HotSpot没有为每条指令生成OopMap,而是只在特定位置记录,这些位置被称为“安全点”。(标准是 是否让程序长时间执行,如循环、方法调用等)。让GC在安全点停顿,HotSpot采用主动式中断(当GC需要中断线程的时候,只是标记,各个线程轮训这个标志,发现这个标志为真则挂起线程,和安全点重合则加上创建线程需要分配的位置)
新生代GC(Minor GC)
老年代GC (Major/Full GC)
新生代
Serial收集器
Serial收集器是Hotspot运行在Client模式下的默认新生代收集器, 它的特点是 只用一个CPU/一条收集线程去完成GC工作, 且在进行垃圾收集时必须暂停其他所有的工作线程(“Stop The World” -后面简称STW).
虽然是单线程收集, 但它却简单而高效, 在VM管理内存不大的情况下(收集几十M~一两百M的新生代), 停顿时间完全可以控制在几十毫秒~一百多毫秒内.
ParNew收集器
ParNew收集器其实是前面Serial的多线程版本, 除使用多条线程进行GC外, 包括Serial可用的所有控制参数、收集算法、STW、对象分配规则、回收策略等都与Serial完全一样(也是VM启用CMS收集器-XX: +UseConcMarkSweepGC的默认新生代收集器).
由于存在线程切换的开销, ParNew在单CPU的环境中比不上Serial, 且在通过超线程技术实现的两个CPU的环境中也不能100%保证能超越Serial. 但随着可用的CPU数量的增加, 收集效率肯定也会大大增加(ParNew收集线程数与CPU的数量相同, 因此在CPU数量过大的环境中, 可用-XX:ParallelGCThreads参数控制GC线程数).
Parallel Scavenge收集器
与ParNew类似, Parallel Scavenge也是使用复制算法, 也是并行多线程收集器. 但与其他收集器关注尽可能缩短垃圾收集时间不同, Parallel Scavenge更关注系统吞吐量:
系统吞吐量=运行用户代码时间(运行用户代码时间+垃圾收集时间)
停顿时间越短就越适用于用户交互的程序-良好的响应速度能提升用户的体验;而高吞吐量则适用于后台运算而不需要太多交互的任务-可以最高效率地利用CPU时间,尽快地完成程序的运算任务. Parallel Scavenge提供了如下参数设置系统吞吐量:
老年代
Serial Old收集器
Serial Old是Serial收集器的老年代版本, 同样是单线程收集器,使用“标记-整理”算法:
Parallel Old收集器
Parallel Old是Parallel Scavenge收老年代版本, 使用多线程和“标记-整理”算法, 吞吐量优先, 主要与Parallel Scavenge配合在 注重吞吐量 及 CPU资源敏感 系统内使用:
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一款具有划时代意义的收集器, 一款真正意义上的并发收集器, 虽然现在已经有了理论意义上表现更好的G1收集器, 但现在主流互联网企业线上选用的仍是CMS(如Taobao、微店).
CMS是一种以获取最短回收停顿时间为目标的收集器(CMS又称多并发低暂停的收集器), 基于”标记-清除”算法实现, 整个GC过程分为以下4个步骤:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark: GC Roots Tracing过程)
重新标记(CMS remark)
并发清除(CMS concurrent sweep: 已死象将会就地释放, 注意: 此处没有压缩)
其中两个加粗的步骤(初始标记、重新标记)仍需STW. 但初始标记仅只标记一下GC Roots能直接关联到的对象, 速度很快; 而重新标记则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录, 虽然一般比初始标记阶段稍长, 但要远小于并发标记时间.
(由于整个GC过程耗时最长的并发标记和并发清除阶段的GC线程可与用户线程一起工作, 所以总体上CMS的GC过程是与用户线程一起并发地执行的.
由于CMS收集器将整个GC过程进行了更细粒度的划分, 因此可以实现并发收集、低停顿的优势, 但它也并非十分完美, 其存在缺点及解决策略如下:
CMS默认启动的回收线程数=(CPU数目+3)4
当CPU数>4时, GC线程最多占用不超过25%的CPU资源, 但是当CPU数<=4时, GC线程可能就会过多的占用用户CPU资源, 从而导致应用程序变慢, 总吞吐量降低.
无法处理浮动垃圾, 可能出现Promotion Failure、Concurrent Mode Failure而导致另一次Full GC的产生: 浮动垃圾是指在CMS并发清理阶段用户线程运行而产生的新垃圾. 由于在GC阶段用户线程还需运行, 因此还需要预留足够的内存空间给用户线程使用, 导致CMS不能像其他收集器那样等到老年代几乎填满了再进行收集. 因此CMS提供了-XX:CMSInitiatingOccupancyFraction参数来设置GC的触发百分比(以及-XX:+UseCMSInitiatingOccupancyOnly来启用该触发百分比), 当老年代的使用空间超过该比例后CMS就会被触发(JDK 1.6之后默认92%). 但当CMS运行期间预留的内存无法满足程序需要, 就会出现上述Promotion Failure等失败, 这时VM将启动后备预案: 临时启用Serial Old收集器来重新执行Full GC(CMS通常配合大内存使用, 一旦大内存转入串行的Serial GC, 那停顿的时间就是大家都不愿看到的了).
最后, 由于CMS采用”标记-清除”算法实现, 可能会产生大量内存碎片. 内存碎片过多可能会导致无法分配大对象而提前触发Full GC. 因此CMS提供了-XX:+UseCMSCompactAtFullCollection开关参数, 用于在Full GC后再执行一个碎片整理过程. 但内存整理是无法并发的, 内存碎片问题虽然没有了, 但停顿时间也因此变长了, 因此CMS还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction用于设置在执行N次不进行内存整理的Full GC后, 跟着来一次带整理的(默认为0: 每次进入Full GC时都进行碎片整理).
分区收集- G1收集器
与其他基于分代的收集器不同, G1将整个Java堆划分为多个大小相等的独立区域(Region), 虽然还保留有新生代和老年代的概念, 但新生代和老年代不再是物理隔离的了, 它们都是一部分Region(不需要连续)的集合.
每块区域既有可能属于O区、也有可能是Y区, 因此不需要一次就对整个老年代/新生代回收. 而是当线程并发寻找可回收的对象时, 有些区块包含可回收的对象要比其他区块多很多. 虽然在清理这些区块时G1仍然需要暂停应用线程, 但可以用相对较少的时间优先回收垃圾较多的Region(这也是G1命名的来源). 这种方式保证了G1可以在有限的时间内获取尽可能高的收集效率.