第2章 Java内存区域与内存溢出异常
2.2运行时数据区域
添加图片注释,不超过 140 字(可选)
2.2.1程序计数器
程序计数器:当前线程所执行的字节码的行号指示器,节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。Java虚拟机的多线程是通过线程轮流切换、分配处理器时间方式实现,任何一个时刻一个处理器都只会执行一条线程中的指令,为了切换后能恢复到正确位置,每条线程都需要有一个独立的程序计数器,各条线程互不影响,独立存储,“线程私有“。
2.2.2 Java虚拟机栈
虚拟机栈是线程私有,生命周期与线程一致。描述的是方法执行的内存模型,方法被执行时会同步创建一个栈帧存放局部变量表、操作数栈、动态连接、方法出口等信息。局部变量表存放了编译期可知的各种Java虚拟机基本数据类型、对象引用和returnAddress 类型,局部变量表的空间在编译期间完成分配,方法运行期间不会改变局部变量表的大小。
2.2.3本地方法栈
本地方法栈是为虚拟机使用到的本地(Native) 方法服务。
2.2.4 Java堆
被所有线程共享,用来存放对象实例。
2.2.5 方法区
被所有线程共享,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据,和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展。
2.2.6 运行时常量池
是方法区的一部分,存放用于存放编译期生成的的各种字面量与符号引用的常量池表,这部分内容将在类加载后存放到方法区的运行时常量池中。
2.2.7直接内存
配不会受到Java堆大小的限制。
2.3 HotSpot虚拟机对象探秘
2.3.1对象的创建
虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
两种内存分配方式:
类检查后,为新生对象分配内存,大小在类加载完成后就能确定。
指针碰撞:假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离。
解决并发情况下不是线程安全的方法,一种是对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性;另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。
空闲列表:如果Java堆中的内存并不是规整的,没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。
一般来说new指令之后会接着执行<init> ()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。
2.3.2 对象的内存布局
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头部分包括两类信息。第一类是用于存储对象自身的运行时数据,另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。
2.3.3对象的访问位
主流的访问方式主要有使用句柄和直接指针两种:
句柄:Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息
添加图片注释,不超过 140 字(可选)
直接指针:reference中存储的直接就是对象地址,速度更快。
添加图片注释,不超过 140 字(可选)
2.4实战:OutOfMemoryError异常
2.4.1 Java堆溢出
处理内存区域异常:
-
要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
-
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
-
不是内存泄漏,也就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
2.4.2虚拟机栈和本地方法栈溢出
本地方法:简单地讲,一个Native Method就是一个Java调用非Java代码的接口。一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以extern“C”告知C++编译器去调用一个C的函数。
关于虚拟机栈和本地方法栈,在《Java虚拟机规范》中描述了两种异常:
1)如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。2)如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出
OutOfMemoryError异常。
2.4.3方法区和运行时常量池溢出
运行时常量池是方法区的一部分,HotSpot从JDK 7开始逐步“去永久代”的计划,并在JDK 8中完全使用元空间来代替永久代。
方法区的主要职责是用于存放类型的相关信息,如类 名、访问修饰符、常量池、字段描述、方法描述等。
在JDK 8以后,元空间成为其替代者。
2.4.4 本机直接内存溢出
直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致,由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况。
第3章 垃圾收集器与内存分配策略
3.1概述
3.2对象已死?
3.2.1 引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
缺点:必须要配合大量额外处理才能保证正确地工作。
3.2.2 可达性分析算法
添加图片注释,不超过 140 字(可选)
通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。(从某个节点没有路径到GC Roots)
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
·在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
·在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
·在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
·在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
·Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
·所有被同步锁(synchronized关键字)持有的对象。
·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
3.2.3再谈引用
Java引用分为:强引用,软引用,弱引用,虚引用,强度递减。
强引用:是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
软引用:用来描述一些还有用,但非必须的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收。
弱引用:用来描述那些非必须对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止,无论内存是否足够都会被回收。
虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例,为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
3.2.4 生存还是灭亡?
finalize():是Object里面的一个方法,当一个堆空间中的对象没有被栈空间变量指向的时候,这个对象会等待被java回收。
宣告对象死亡至少要经历两个过程,在可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。如有必要,那么该对象将会被放置在一个名为F-Queue的队列之中,稍后新建一个线程去执行finalize()方法。稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合。
3.2.5回收方法区
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。
判断一个类是否被废弃要满足三个条件:
·该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
·加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。
·该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
3.3垃圾收集算法
从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”和“追踪式垃圾收集”两大类,这两类也常被称作“直接垃圾收集”和“间接垃圾收集”。
3.3.1分代收集理论
一般至少会把Java堆划分为新生代和老年代两个区域。解决跨代引用需要扫描整个老年代:
在新生代上建立一个全局的数据结构,这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。
·部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
■新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
■老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。
■混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
·整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
3.3.1 标记-清除算法
首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
缺点:执行效率不稳定,空间碎片化问题。
添加图片注释,不超过 140 字(可选)
3.3.3标记-复制算法
半区复制”(Semispace Copying)的垃圾收集算法:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
缺点:浪费空间
添加图片注释,不超过 140 字(可选)
Appel式回收:把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor,HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保。
3.3.4标记-整理算法
标记过程仍然与“标记-清除”算法一样,之后让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
添加图片注释,不超过 140 字(可选)
缺点:这种对象移动操作必须全程暂停用户应用程序才能进行。
从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算。
3.4HotSpot的算法细节实现
3.4.1根节点枚举
所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的,可达性分析算法耗时最长的查找引用链的过程已经可以做到与用户线程一起并发,但根节点枚举始终还是必须在一个能保障一致性的快照中才得以进行。
在HotSpot 的解决方案里,是使用一组称为OopMap的数据结构来直接得到哪些地方存放着对象引用,加载动作完成的时候, HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来。
3.4.2安全点
HotSpot没有为每条指令都生成OopMap,只是在“特定的位置”记录了这些信息,这些位置被称为安全点,用户程序达到安全点后才能够暂停。
在垃圾收集时让所有线程都跑到最近的安全点暂停有两种选择:抢先式中断,主动式中断。
抢先式中断:在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上。
(几乎不使用)
主动式中断:当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。
3.4.3安全区域
当程序没有分配处理器时间安全点就不适合,必须引入安全区域。
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。
3.4.4记忆集与卡表
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,有三种精度可以选择,字长精度,对象精度,卡精度。
卡精度所指的是用一种称为“卡表”的方式去实现记忆集,定义了记忆集的记录精度、与堆内存的映射关系等。一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0,垃圾收集时只筛选出变脏的元素。
添加图片注释,不超过 140 字(可选)
3.4.5写屏障
有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏。在HotSpot虚拟机里是通过写屏障技术维护卡表状态的。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面[2],在引用对象赋值时会产生一个环形通知,供程序执行额外的动作。赋值前的部分的写屏障叫作写前屏障,在赋值后的则叫作写后屏障。
为了避免伪共享问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏。
3.4.6并发的可达性分析
·白色:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
·黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
·灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过。
、
添加图片注释,不超过 140 字(可选)
解决扫描时对象消失:
增量更新:黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象 了。
原始快照:无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
3.5经典垃圾收集器
3.5.1Serial收集器
是一个单线程工作的收集器,垃圾收集时必须暂停其他工作线程,直至工作结束。
添加图片注释,不超过 140 字(可选)
优点:简单高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的。
3.5.2ParNew收集器
实质上是Serial收集器的多线程并行版本,用多条线程进行垃圾收集。
添加图片注释,不超过 140 字(可选)
CMS收集器:首次实现了让垃圾收集线程与用户线程(基本上)同时工作。
3.5.3Parallel Scavenge收集器
基于标记-复制算法实现的收集器,目标是达到一个可控制的吞吐量,提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间。有自适应调节策略可以设置-XX:+UseAdaptiveSizePolicy。
3.5.4Serial Old收集器
是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
添加图片注释,不超过 140 字(可选)
3.5.5Parallel Old收集器
是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。
添加图片注释,不超过 140 字(可选)
3.5.6CMS收集器
是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现的,过程分为四步:初始标记,并发标记,重新标记,并发清除。
添加图片注释,不超过 140 字(可选)
缺点:对处理器资源非常敏感;无法处理“浮动垃圾”,无法在当此处理在标记后出现的垃圾对象;收集结束时会有大量空间碎片产生;
3.5.7Garbage First收集器
它可以面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。Humongous区域,专门用来存储大对象。
G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的那些Region。
添加图片注释,不超过 140 字(可选)
G1收集器分为四步:初始标记,并发标记,最终标记,筛选回收。目标是在延迟可控的情况下获得尽可能高的吞吐量。
添加图片注释,不超过 140 字(可选)
优点:指定最大停顿时间,不会产生内存空间碎片。
缺点:记忆集所占内存大
3.6低延迟垃圾收集器
添加图片注释,不超过 140 字(可选)
3.6.1Shenandoah收集器
与G1不同,它支持并发的整理算法,可以与用户线程并发。默认不使用分代。改用连接矩阵记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题发生概率。
添加图片注释,不超过 140 字(可选)
工作过程分为9个阶段:
初始标记:只标记与GC Roots直接关联的对象,需要停顿,时间与GC Roots数量有关。
并发标记:遍历对象图,标记出全部可达的对象,与用户线程并发
最终标记:处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集。
并发清理:用于清理那些整个区域内连一个存活对象都没有找到的Region。
并发回收:把回收集里面的存活对象先复制一份到其他未被使用的Region之中,
初始引用对象更新:把堆中所有指向旧对象的引用修正到复制后的新地址。
并发引用更新:按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值即可。
最终引用更新:解决了堆中的引用更新后,还要修正存在于GC Roots中的引用。
并发清理:所有Region已无存活对象,回收这些Region的内存空间,供以后新对象分配使用。
转发指针:在原有对象布局结构的最前面统一增加一个新的引用字段,在正常不处于并发移
动的情况下,该引用指向对象自己,当对象拥有了一份新的副本时,只需要修改一处指针的值,即旧对象上转发指针的引用位置,使其指向新对象,便可将所有对该对象的访问转发到新的副本上。
添加图片注释,不超过 140 字(可选)
添加图片注释,不超过 140 字(可选)
3.6.2ZGC收集器
是一款基于Region内存布局的,(暂时) 不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
ZGC的Region分为大中小三种容量。
染色体指针:
添加图片注释,不超过 140 字(可选)
染色体指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理;大幅减少在垃圾收集过程中内存屏障的使用数量;
ZGC工作阶段:
并发标记:遍历对象图做可达性分析,标记在指针上进行,更新染色体指针的Marked0、Marked1标志。
并发预备重分配:根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集,每次回收都会扫描所有的Region。
并发重分配:把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”。
并发重映射:修正整个堆中指向重分配集中旧对象的所有引用。
添加图片注释,不超过 140 字(可选)
缺点:对象分配速率不高
3.7选择合适的垃圾收集器
3.7.1Epsilon收集器
不能够进行垃圾收集。
3.7.2 收集器的权衡
选择垃圾收集器,受运行应用的基础设施,JDK的发行商、版本号三个因素影响。
C4收集器:预算充足,没有调优经验
ZGC: 预算不足去使用商业解决方案,但能够掌控软硬件型号,使用较新的版本,同时又特别注重延迟。
Shenandoah:注重收集器稳定性。
CMS、G1:软硬件基础设施和JDK版本都比较落后。
3.7.3虚拟机及垃圾收集器日志
1)查看GC基本信息,在JDK 9之前使用-XX:+PrintGC,JDK 9后使用-Xlog:gc:
2)查看GC详细信息,在JDK 9之前使用-XX:+PrintGCDetails,在JDK 9之后使用-X-log:gc*
3)查看GC前后的堆、方法区可用容量变化,在JDK 9之前使用-XX:+PrintHeapAtGC,JDK 9之后使用-Xlog:gc+heap=debug:
4)查看GC过程中用户线程并发时间以及停顿的时间,在JDK 9之前使用-XX:+Print- GCApplicationConcurrentTime以及-XX:+PrintGCApplicationStoppedTime,JDK 9之后使用-Xlog: safepoint:
5)查看收集器Ergonomics机制(自动设置堆空间各分代区域大小、收集目标等内容,从Parallel收集器开始支持)自动调节的相关信息。在JDK 9之前使用-XX:+PrintAdaptive-SizePolicy,JDK 9之后使用-Xlog:gc+ergo*=trace:
6)查看熬过收集后剩余对象的年龄分布信息,在JDK 9前使用-XX:+PrintTenuring-Distribution, JDK 9之后使用-Xlog:gc+age=trace:
3.7.4垃圾收集器参数总结
3.8实战:内存分配与回收策略
3.8.1对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。当Survivor空间不够时,通过分配担保机制提前转移到老年代中。
3.8.2大对象直接进入老年代
HotSpot虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作。
3.8.3长期存活的对象将进入老年代
对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。
3.8.4动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX: MaxTenuringThreshold中要求的年龄。
3.8.5空间分配担保
在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX: HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。
第4章 虚拟机性能监控、故障处理工具
4.1概述
4.2基础故障处理工具 用于监视虚拟机运行状态和进行故障处理的工具根据软件可用性和授权的不同,可以把它们划分成三类:商业授权工具、正式支持工具、实验性工具。
4.2.1jps:虚拟机进程状况工具
可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID
4.2.2jstat:虚拟机统计信息监视工具
用于监视虚拟机各种运行状态信息的命令行工具,可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据。
4.2.3jinfo:Java配置信息工具
实时查看和调整虚拟机各项参数,使用jps命令的-v参数可以查看虚拟机启动时显式指定的参数列表。
4.2.4jmap:Java内存映像工具
用于生成堆转储快照,还可以查询finalize执行队列、Java堆和方法区的详细信息,如空间使用率、当前用的是哪种收集器等。
4.2.5jhat:虚拟机堆转储快照分析工具
与jmap搭配使用,来分析jmap生成的堆转储快照。
4.2.6jstack:Java堆栈跟踪工具
生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。
4.2.7基础工具总结
4.3可视化故障处理工具
主要包括JConsole、JHSDB、VisualVM 和JMC四个
4.3.1JHSDB:基于服务性代理的调试工具
是一款基于服务性代理实现的进程外调试工具,务性代理是HotSpot虚拟机中一组用于映
射Java虚拟机运行信息的、主要基于Java语言(含少量JNI代码)实现的API集合。
4.3.2JConsole:Java监视与管理控制台
它的主要功能是通过JMX的MBean(Managed Bean)对系统进行信息收集和参数动态调
整。
4.3.3VisualVM:多合-故障处理工具
是功能最强大的运行监视和故障处理程序之一
4.3.4Java Mission Control:可持续在线的监控工具
对吞吐量影响小
4.4HotSpot虚拟机插件及工具
HSDIS:JIT生成代码反汇编
第5章 调优案例分析与实战
5.2.1大内存硬件上的程序部署策略
目前单体应用在较大内存的硬件上主要的部署方式有两种:
1)通过一个单独的Java虚拟机实例来管理大量的Java堆内存。
2)同时使用若干个Java虚拟机,建立逻辑集群来利用硬件资源。
5.2.2集群间同步导致的内存溢出
5.2.3堆外内存导致的溢出错误
在处理小内存或者32位的应用问题时,除了Java堆和方法区之外,下面这些区域还会占用较多的内存
·直接内存:可通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOf-MemoryError或者OutOfMemoryError:Direct buffer memory。
·线程堆栈:可通过-Xss调整大小,内存不足时抛出StackOverflowError(如果线程请求的栈深度大于虚拟机所允许的深度)或者OutOfMemoryError(如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存)。
·Socket缓存区:每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB内存,连接多的话这块内存占用也比较可观。如果无法分配,可能会抛出IOException:Too many open files异常。
·JNI代码:如果代码中使用了JNI调用本地库,那本地库使用的内存也不在堆中,而是占用Java虚拟机的本地方法栈和本地内存的。
5.2.4外部命令导致系统缓慢
如外部脚本。
5.2.5服务器虚拟机进程崩溃
5.2.6不恰当数据结构导致内存占用过大
如HashMap,在HashMap<Long,Long>结构中,只有Key和Value所存放的两个长整型数据是有效数据,共16字节(2×8字节)。这两个长整型数据包装成java.lang.Long对象之后,就分别具有8字节的Mark Word、8字节的Klass指针,再加8字节存储数据的long值。然后这2个Long对象组成Map.Entry之后,又多了16字节的对象头,然后一个8字节的next字段和4字节的int型的hash字段,为了对齐,还必须添加4字节的空白填充,最后还有HashMap中对这个Entry的8字节的引 用,这样增加两个长整型数字,实际耗费的内存为(Long(24byte)×2)+Entry(32byte)+HashMap Ref(8byte)=88byte,空间效率为有效数据除以全部内存空间,即16字节/88字节=18%。
5.2.7由Windows虚拟内存导致的长时间停顿
5.2.8由安全点导致长时间停顿
使用int类型或范围更小的数据类型作为索引值的循环默认是不会被放置安全点的。这种循环被称为可数循环,相对应地,使用long或者范围更大的数据类型作为索引值的循环就被称为不可数循环,将会被放置安全点。
5.3实战:Eclipse运行速度调优
5.3.1调优前的程序运行状态
5.3.2升级JDK版本的性能变化及兼容问题
5.3.3编译时间和类加载时间的优化
编译时间是指虚拟机的即时编译器编译热点代码的耗时,如果一段Java方法被调用次数到达一定程度,就会被判定为热代码交给即时编译器即时编译为本地代码
5.3.4 调整内存设置控制垃圾收集频率
5.3.5 选择收集器降低延迟
第6章 类文件结构
6.2无关性的基石
Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。
添加图片注释,不超过 140 字(可选)
6.3Class类文件的结构
任何一个Class文件都对应着唯一的一个类或接口的定义信息。Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符。
添加图片注释,不超过 140 字(可选)
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式。
6.3.1魔数与Class文件的版本
每个Class文件的头4个字节被称为魔数,用来确定这个文件是否为一个能被虚拟机接受的Class文件
6.3.2常量池
主、次版本号之后的是常量池入口,入口放置常量池计数器,计数容量从0开始。量池中主要存放两大类常量:字面量和符号引用。
第7章 虚拟机类加载机制
7.2类加载的时机
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、和卸载七个阶段,其中验证、准备、解析三个部分统称为连接。解析阶段顺序不固定。
初始化触发:1)到new、getstatic、putstatic或invokestatic这四条字节码指令(使用new关键字实例化对象,读取或设置一个类型的静态字段,调用一个类型的静态方法)
2)使用java.lang.reflect包的方法对类型进行反射调用
添加图片注释,不超过 140 字(可选)
3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
4)虚拟机启动时需要指定一个主类,会先初始化这个类
5)一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄
6)一个接口中定义了JDK 8新加入的默认方法,有这个接口的实现类发生初始化,该接口要在其之前初始化
通过子类引用父类的静态字段,不会导致子类初始化‘
通过数组定义来引用类,不会触发此类的初始化
常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
接口初始化与类初始化的不通:接口中不能使 用“static{}”语句块,但编译器仍然会为接口生成“<clinit>()”类构造器;当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候才会初始化。
7.3类加载的过程
7.3.1加载
Java虚拟机需要:
-
通过类的全名获取定义此类的二进制字节流
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
-
在内存中生成一个这个类的java.lang.Class对象作为方法区这个类的各种数据的访问入
口。
数组类不通过类加载器创建,由java虚拟机直接在内存中动态构造出来,数组类加载需要遵循以下规则:
如果数组的组件类型(指的是数组去掉一个维度的类型)是引用类型,递归去加载这个组件类型,将被标识在加载该组件类型的类加载器的类名称空间上。
数组的组件类型不是引用类型,Java虚拟机将会把数组标记为与引导类加载器关联。
数组类的可访问性与它的组件类型的可访问性一致。
加载结束后,会在java堆内存中实例化一个java.lang.Class类的对象。
7.3.2验证
验证是连接阶段的第一步,确保Class文件符合规范。验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。
-
文件格式验证: ·是否以魔数0xCAFEBABE开头。 ·主、次版本号是否在当前Java虚拟机接受范围之内。 ·常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。 ·指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。 ·CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据。 ·Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。 保证输入的字节流能正确地解析并存储于方法区之内
-
元数据验证 对字节码描述的信息进行语义分析: ·这个类是否有父类。 ·这个类的父类是否继承了不允许被继承的类(被final修饰的类)。 ·如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。 ·类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。 主要目的是对类的元数据信息进行语义校验。
-
字节码验证 目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。 ·保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况。 ·保证任何跳转指令都不会跳转到方法体以外的字节码指令上。 ·保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。
-
符号引用验证
发生在虚拟机将符号引用转化为直接引用时,检查该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。
·符号引用中通过字符串描述的全限定名是否能找到对应的类。
·在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
·符号引用中的类、字段、方法的可访问性(private、protected、public、<package>)是否可被当前类访问。
7.3.3准备
为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
7.3.4解析
是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
-
类或接口的解析 假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那虚拟机完成整个解析的过程需要包括以下3个步骤: 1)如果C不是一个数组类型,那虚拟机啊将会把代表N的全限定名传递给D的类加载器去加载这个类C。
-
如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类
似“[Ljava/lang/Integer”的形式,那将会按照第一点的规则加载数组元素类型。如果N的描述符如前面所假设的形式,需要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个代表该数组维度和元素的数组对象。
-
确认D是否具备对C的访问权限
-
字段解析 解析一个未被解析过的字段符号引用,首先将会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用。 1)如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。 2)否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口, 如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。 3)否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。 4)否则,查找失败,抛出java.lang.NoSuchFieldError异常。 3.方法解析 先解析出方法表的class_index项中索引的方法所属的类或接口的符号引用。 1)由于Class文件格式中类的方法和接口的方法符号引用的常量类型定义是分开的,如果在类的方法表中发现class_index中索引的C是个接口的话,那就直接抛出异常。 2)如果通过了第一步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。 3)否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。 4)否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这时候查找结束,抛出java.lang.AbstractMethodError异常。 5)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。 4.接口方法解析 需要先解析出接口方法表的class_index项中索引的方法所属的类或接口的符号引用。 1)与类的方法解析相反,如果在接口方法表中发现class_index中的索引C是个类而不是接口,那么就直接抛出java.lang.IncompatibleClassChangeError异常。 2)否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。 3)否则,在接口C的父接口中递归查找,直到java.lang.Object类(接口方法的查找范围也会包括Object类中的方法)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。 4)对于规则3,由于Java的接口允许多重继承,如果C的不同父接口中存有多个简单名称和描述符都与目标相匹配的方法,那将会从这多个方法中返回其中一个并结束查找。 5)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。 7.3.5初始化 初始化阶段就是执行类构造器<clinit>()方法的过程。 静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。 Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。 7.4类加载器 7.4.1类与类加载器 对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。 7.4.2双亲委派模型 从java虚拟机角度类加载器分两种:启动类加载器、其他所有的类加载器; 启动类加载器:负责加载存放在<JAVA_HOM E>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的类库加载到虚拟机的内存中。 扩展类加载器:它负责加载<JAVA_HOM E>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。 应用程序类加载器:它负责加载用户类路径上所有的类库。 双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。 7.4.3破坏双亲委派模型 为了兼容jdk1.2之前的服务;由这个模型自身的缺陷导致的(有基础类型又要调用回用户的代码);由于用户对程序动态性的追求而导致的(代码热替换、模块热部署); OSGi实现模块化热部署
添加图片注释,不超过 140 字(可选)
: 1)将以java.*开头的类,委派给父类加载器加载。 2)否则,将委派列表名单内的类,委派给父类加载器加载。 3)否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。 4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。 5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。 6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。7)否则,类查找失败。 第八章 虚拟机字节码执行引擎 8.1概述 8.2运行时栈帧结构 栈帧是支持虚拟机进行方法调用和方法执行的数据结构,存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为“当前栈帧”,与这个栈帧所关联的方法被称为“当前方法”。
添加图片注释,不超过 140 字(可选)
8.2.1局部变量表 是变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。 Java中占用不超过32位存储空间的数据类型有boolean、byte、char、short、int、float、reference和returnAddress这8种类型。对于64位的数据类型(long和double),Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间。 Java虚拟机通过索引定位的方式使用局部变量表。调用实例方法,局部变量表中第0位索引的变量槽默认是用于传递方法所属对象示例的应用,可用this访问。 局部变量表中的变量槽是可以重用。 8.2.2操作数栈 在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。算术运算的时候是通过将运算涉及的操作数栈压入栈顶后调用运算指令来进行的。 8.2.3动态连接 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。 8.2.4方法返回地址 方法退出分为正常调用完成(正常返回)和异常调用完成(发生异常退出),方法退出之后,都必须返回到最初方法被调用时的位置,栈帧中存在信息来帮助恢复他的上层主调方法的执行状态。方法退出的过程等同于把当前栈帧出栈。 8.3方法调用 一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址。 8.3.1解析 调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法的调用被称为解析。 静态方法、私有方法、实例构造器、父类方法、被final修饰的方法会在类加载的时候就可以把符号引用解析为该方法的直接引用,称为非虚方法。 8.3.2分派
-
静态分派 所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。最典型应用是方法重载。
-
动态分派
与重写密切关联,在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
-
单分派与多分派 单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。Java语言是一门静态多分派、动态单分派的语言。
-
虚拟机动态分派的实现
动态分派的方法版本选择过程需要运行时在接收者类型的方法元数据中搜索合适的目标方法。在方法区中建立一个虚方法表,使用虚方法表索引来代替元数据查找以提高性能。
添加图片注释,不超过 140 字(可选)