-
运行时数据区
-
方法区(Method Area)
-
元空间(Meta Space)JDK 8 +
- 字符串常量池
- 静态变量
-
运行时常量池
- Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
- 运行时常量池相较于Class文件常量池具有动态性,运行期间可以将新的常量放入运行时常量池中,比如String.intern
- 类型信息(解析Class之后加载到内存可以被JVM识别和使用的对象关联信息)
-
溢出
- -XX: MaxMetaspaceSize
- -XX: MetaspaceSize
- -XX: MinMetaspaceFreeRatio
- 在JDK 7及之前,HotSpot使用永久代来实现方法区时,而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候类变量在方法区就完全是一种种对逻辑概念的表述了,并不能说“JVM的元空间是方法区”
-
虚拟机栈(VM Stack)
- 虚拟机栈描述的是Java方法执行的线程内存模型,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
-
通常讲的“栈”更多的是指虚拟机栈中的局部变量表部分
-
局部变量表
- 存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、 short、 int、 float、 long、 double) 、对象引用
- 这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的1ong和double类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。
- 本地方法栈(Native Method Stack)
-
堆(Heap)
- 溢出(Java.lang.OutOfMemoryError、Java heap space)
- 通过JVM参数设置大小-Xmx、-Xms
-
对象在堆内存中的存储布局
-
对象头(Header)
- Mark Word
- 存储对象自身的运行时数据:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
- 不同的比特位动态地去代表不同的标志位状态
- 类型指针
- 对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。
-
实例数据(Instance Data)
- 这部分是对象真正存储的有效信息,即程序代码里定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录在此。
- HotSpot虛拟机默认的分配顺序为longs/doubles、ints、 shorts/chars、 bytes/eooleans、oops(Ordinary Object Pointers, OOPs)
- 相同宽度的字段总是被分配到一起存放
- 在父类中定义的变量会出现在子类之前
-
对齐填充(Padding)
- 对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
- 可以通过Java内存(映像)分析工具(hprof文件分析工具)打开内存堆转储快照分析
-
程序计数器(Program Counter Register)
- 线程所执行的字节码的行号指示器
- 每个线程都有一个独立的程序计数器,各线程之间的计数器互不影响
-
HotSpot不区分虚拟机栈和本地方法栈,同时也不支持栈的动态扩展,栈容量由`-Xss`参数设定,HotSpot的栈溢出抛出的异常全都是StackOverFlowError。
-
溢出
- 使用-Xss参数减少栈内存容量。
- 定义了大量的本地变量,增大此`方法帧`中`本地变量表`的长度。
- 通过不断建立线程的方式,在HotSpot上也是可以产生内存溢出异常的。
-
对象
- 对象的内存布局
-
对象的访问定位
- reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。
- 速度更快,不需要多一次间接访问的开销
- 通过栈上的reference类型数据来操作堆上的具体对象
-
直接内存(Direct Memory)
-
溢出
- 在JDK 1.4中新加入了NIO (New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer) 的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。
- 使用unsafe直接分配本机内存,模拟内存溢出
- 由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果读者发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了。
- 在设置-Xmx等参数时,除了考虑运行时数据区里的各大内存区域,还要考虑到直接内存。
-
垃圾收集器
- 程序计数器
- 虚拟机栈
- 本地方法栈
-
Java堆
-
对象已死?
-
引用计数法
- 单纯的引用计数就很难解决对象之间相互循环引用的问题
-
可达性分析算法(Reachability Analysis)
- GC Roots
- 为了避免GCRoots包含过多对象而过度膨胀,GC使用的是局部回收
-
生存与死亡?(两次标记过程)
- 对象在进行可达性分析后发现没有与GC Roots相连接的引用链
- finalize()方法是对象逃脱死亡命运的最后一次机会,只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字) 赋值给某个类变量或者对象的成员变量,那它将被移出“即将回收”的集合
-
方法区
- 回收方法区的废弃常量和不再使用的类型(方法区垃圾收集的“性价比"通常也是比较低的)
-
判定一个类型是否属于“不再被使用的类”的条件
- 该类所有的实例都已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
- 加载该类的类加载器已经被回收
-
对象存活判断算法和垃圾收集算法
-
对象消亡
- “引用计数式垃圾收集”(Reference Counting GC)(直接垃圾收集)
- “追踪式垃圾收集”(Tracing GC)(间接垃圾收集)
-
分代收集理论
- 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的
- 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡
-
收集类型
-
部分收集(Partial GC)
- 新生代收集(Minor GC/Young GC)
- 老年代收集(Major GC/Old GC)
- 混合收集(Mixed GC)
- 整堆收集(Full GC)
-
不同区域安排不同的算法
-
标记-清除算法(Mark-Sweep)
- 执行效率不稳定,标记和清除两个过程的执行效率都随对象数量增长而降低
- 内存空间的碎片化问题
-
标记-复制算法
- 半区复制(Semispace Copying)
- 代价是将可用内存缩小为了原来的一半,另一半等同于空置不用
- Appel式回收
- Eden空间(默认占80%)
- 大对象GC时直接进入老年代
- Survivor From空间(默认占10%)
- 对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中
- 如果在Survivor空间中的一批相同年龄的对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄
- Survivor To空间(默认占10%)
- 分配担保(Handle Promotion)
- 虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间
- 冒险
- 把Survivor无法容纳的超大对象在Minor GC时无法被移步到Survivor时将被一直划分到老年代(最极端的情况就是内存回收后新生代中所有对象都存活),这样的话需要老年代进行分配担保,只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间
- 这个算法的高效是建立在大部分对象都“朝生夕灭”的特性上的,如果存活对象过多,把这些对象复制到Survivor并维持这些对象引用的正确性就成为一个沉重的负担,因此导致垃圾收集的暂停时间明显变长
-
标记-整理算法(移动式)
- 如果移动存活的对象太多,这将是一笔很大的开销
- 如果完全不考虑移动和整理存活对象的话,弥散于堆中的存活对象导致的空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决
- “和稀泥式”解决方案
- 平时多数时间都采用标记清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间
- 通常是需要短暂地暂停用户程序来做垃圾收集的
-
Java堆
- 新生代(Young Generation)
- 老年代(Old Generation)
- 两者之间互相引用,跨代引用假说(Intergenerational Reference Hypothesis)
-
经典垃圾收集器
- Serial收集器
- ParNew收集器(退役)
-
Parallel Scavenge收集器(吞吐量优先收集器)
- 标记-复制
-
精确控制吞吐量的参数
- -XX: MaxGCPauseMills
- -XX: GCTimeRatio
- -XX: +UseAdaptiveSizePolicy
- Serial Old收集器(标记-整理)
-
Parallel Old收集器
- Parallel Scavenge收集器的老年代版本
- 标记-整理
- 多线程并发收集
-
CMS收集器(如今已经被官方声明为不推荐使用)
- JDK5发布时,在当时具有划时代意义
- 以获取最短回收停顿时间为目标
- 标记-清除
-
Garbage First收集器(G1)
-
关注停顿时间控制
- -XX: MaxGCPauseMillis
- 跳出垃圾收集分代理论,只关注哪块内存中存放的垃圾数量最多,回收收益最大
- 基于Region的堆内存设计布局,还有一个Humongous区域,专门用来存储大对象(大小超过了一个Region容量一半的对象),超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中
- -XX: G1HeapRegionSize
- 全功能的垃圾收集器
- CMS的接替者和继承者
- 这是最古老,最基础的垃圾收集器,只会使用一个处理器单线程去做垃圾收集,而且它在进行垃圾收集期间,会停掉所有的用户工作线程
- 是Serial的多线程并行版本
- Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
- 适合较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验的场景。
-
垃圾收集器组合
- Parallel Scavenge + Parallel Old
- Serial + CMS
- ParNew + Serial Old
- 适合注重吞吐量或者处理器资源较为稀缺的场合(该组合已经被官方宣告被G1取代)
- 在JDK 8时将这两个组合声明为废弃(JEP 173),并在JDK 9中完全取消了这些组合的支持(JEP 214)
-
低延迟垃圾收集器
-
重要指标
- 内存占用(Footprint)
- 吞吐量(Throughput)
- 延迟(Latency)
- 一款优秀的收集器通常最多可以同时达成其中的两项(不可能三角)
- G1
- Shenandoah收集器(致力于要比G1更低延迟的更新一代搜集器,和G1有着一部分共同的代码)
- ZGC收集器(具有实验性质的低延迟垃圾收集器)
- ZGC和Shenandoah的目标是高度相似的,都希望在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。
-
选择合适的垃圾收集器
- Epsilon收集器
-
收集器的权衡
-
应用程序的关注点
- 吞吐量
- 如果是数据分析、科学计算类的任务、异步实时性追求不高的后台任务,目标是能尽快算出更多的结果,那吞吐量就是主要关注点
- 停顿时间
- 如果是SLA应用、服务影响时间要求高的应用、抢购服务等,那停顿时间直接影响服务质量,严重的甚至会导致事务超时,这样延迟就是主要关注点
- 内存占用
- 如果是客户端应用或者嵌入式应用、内存资源紧张的机器,那垃圾收集的内存占用则是不可忽视的
-
运行应用的基础设施如
- 硬件规格,要涉及的系统架构是x86-32/64、SPARC还是ARM/Aarch64
- 处理器的数量多少,分配内存的大小
- 选择的操作系统是Linux、Solaris 还是Windows等
-
使用JDK的发行商是什么
- 版本号是多少
- 对应哪个版本的《Java虚拟机规范》
-
虚拟机和垃圾收集器日志
- JDK 9之前鱼龙混杂
-
JDK 9之后
- -Xlog[:[selector][:[output][:[decorators][:output-options]]]]
- 如果客户应用只要运行数分钟甚至数秒,只要Java虚拟机能正确分配内存,在堆耗尽之前就会退出,那显然运行负载极小、没有任何回收行为的Epsilon便是很恰当的选择。
-
内存分配和策略回收
-
自动内存管理最根本的目标
- 自动给对象分配内存
- 自动回收分配给对象的内存
-
使用Serial和Serial Old客户端默认收集器组合做测试
- 对象优先分配在Eden空间,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
-
大对象直接进入老年代
- -XX: PretenureSizeThreshold
- 要尽量避免“短命大对象”
-
长期存活的对象将进入老年代
- -XX:MaxTenuringThreshold
-
动态对象年龄判定
- Survivor空间中的一批相同年龄的对象大小的总和是否大于Survivor空间的一半
-
空间分配担保
- 冒险
- 这3个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由即时编译器进行一些优化,但在基于概念模型的讨论里,大体上可以认为是编译期可知的),因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑如何回收的问题,当方法结束或者线程结束时,内存自然就跟随着回收了。
- 两个区域则有着很显著的不确定性,一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理。
-
特性
-
不同的模式
- 服务端模式(Server)
- 客户端模式(Client)
-
多线程高并发
- Java内存模型与线程
-
硬件的效率与一致性
-
由于计算机的存储设备与处理器的运算速度有着几个数量级的差距,所以现代计算机系统都不得不加入一层或多层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲
- 将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了
-
多核处理器系统
- 共享内存多核系统(Shared Memory Multiprocessors System)
-
处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致
- Java虚拟机的即时编译器中也有指令重排序(Instruction Reorder)优化
-
主内存和工作内存
- 线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据
- 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成
- 内存间的交互操作(最新的JSR-133已经被抛弃的操作/Deprecated/JSR-133已将其由8种简化为4种)
-
volatile型变量
-
可见性
-
当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的
- 在volatile修饰的变量做运算前,底层编译层汇编后,在赋值操作后,会多了一个内存屏障(Memory Barrier或Memory Fence),指重排序时不能把后面的指令重排序到内存屏障之前的位置,这个就能够保证其他线程读取变量值时的一致性
-
基于volatile变量的运算在并发下不一定是线程安全的,volatile变量在各个线程的工作内存中是不存在一致性问题的, 但是Java里面的运算操作符并非原子操作,这导致volatile变量的运算在并发下一样是不安全的
- 在实际开发使用中,如果在volatile会带来线程不安全问题的场景下,还是要依赖锁机制来写代码
- 分别多线程单一修改和分别多线程读取的情况,这个时候适合使用volatile修饰变量
-
指令重排序
- 使用volatile变量的另外一个作用是禁止指令重排序优化
-
性能开销
- volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢上一点,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行
- 大多数场景下volatile的总开销仍然要比锁来得更低,能用volatile满足的场景要优先考虑volatile代替锁机制
-
针对long和double型变量的特殊规则(64位数据类型)
- 未被Volatile修饰的64位变量,在多线程读写的情况下,可能会出现读到”半个变量值“脏数据的这种情况
-
三大特性
-
原子性(Atomicity)
- synchronized块实际上会被编译成为两种字节码指令:monitorenter和monitorexit来确保操作原子性
-
可见性(Visibility)
- volatile、synchronized和final关键字都可以实现可见性
- 有序性(Ordering)
-
Java与线程
-
线程的实现
-
使用内核线程实现(1: 1实现)
- “主流”商用Java虚拟机的线程模型普遍都使用基于操作系统原生线程模型来实现
- 每一个Java线程都是直接映射到操作系统原生线程来实现的,而且中间没有额外的间接结构,虚拟机是不会去干涉线程调度的(可以设置线程优先级给操作系统提供调度建议),全权交给底下的操作系统去处理,所以何时冻结或唤醒线程、该给线程分配多少处理器执行时间等,都是操作系统完成的
- 使用用户线程实现(1: N实现)
-
使用用户线程加轻量级进程混合实现(N: M实现)
- 由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换
- 每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的
-
Java线程的调度
-
系统为线程分配处理器使用权的过程
- 协同式(Cooperative Threads Scheduling)线程调度
- 线程要把自己的事情干完后才会进行线程切换
- 抢占式(Preemptive Threads-Scheduling)线程调度
- Thread:yield()
- 主动让出执行时间
- 不能想要主动获取执行权限
- 可以给操作系统做“建议”
- 通过设置线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY)
-
状态切换
- 新建(New)
- 创建后尚未启动的线程处于这种状态
- 运行(Runnable)
- 包括操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着操作系统为它分配执行时间
- 无限期等待(Waiting)
- 处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线程显式唤醒
- 没有设置Timeout参数的Object::wait()
- 没有设置Timeout参数的Thread::join()
- LockSupport::park()
- 限期等待(TimedWaiting)
- 处于这种状态的线程也不会被分配处理器执行时间,不过无须等待被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒
- Thread::sleep()
- 设置了Timeout参数的Object::wait()
- 设置了Timeout参数的Thread:join()
- LockSupport:parkNanos()
- LockSupport::parkUntil()
- 阻塞(Blocked)
- “阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态
- 结束(Terminated)
- 已终止线程的线程状态,线程已经结束执行
- 一个线程当下只能有且只有其中的一种状态
-
线程安全与锁优化
-
线程安全
-
不可变
- 基本数据类型
- 使用final关键字修饰
- 对象实例类型
- 对象可以自行保证其行为不会对其状态产生任何影响
- java.lang.String
- 调用它的substring()、replace()和concat()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象
- 把对象里面带有状态的变量都声明为final,这样在构造函数结束之后,它就是不可变的
-
绝对线程安全
- 任何代码都难以达到绝对线程安全,包括Java的API
-
相对线程安全
- 在Java中,大部分线程安全的类都是这种类型
-
线程安全实现的方法
-
互斥同步:临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)
- synchronized
- 在执行monitorenter指令时,首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一, 而在执行monitorexit指 令时会将锁计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止
- synchronized是Java语言中一个重量级的操作,必要的情况下才使用这种操作(用户态到核心态的转换需要耗费很多的处理器时间、资源)
- 非公平锁
- 在锁被释放时,任何一个等待锁的线程都有机会获得锁
- 只能绑定一个条件
- 锁对象的wait()跟它的notify()或者notifyAll()方法配合可以实现一个隐含的条件
- 可重入锁(ReentrantLock)
- 等待可中断
- 公平锁
- 多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
- ReentrantLock在默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁
- 公平锁性能开销会比非公平锁大得多
- 锁绑定多个条件
- ReentrantLock对象可以同时绑定多个Condition对象,多次调用newCondition()方法即可
- 非阻塞同步
-
虚拟机字节码执行引擎
-
运行时栈帧结构
-
当前栈帧
- 位于栈顶的当前栈帧(Current Stack Frame)才是有效的
- 与当前栈帧所关联的方法被称为当前方法(Current Method)
-
局部变量表(Local Variables Table)
- 在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量,局部变量表的容量以变量槽(Variable Slot)为最小单位,一个变量槽可以存放一个32位以内的数据类型
- 对于double和long类型的变量其存在于两个连续的变量槽,由于变量表是线程私有数据,所以是线程安全的
- 如果执行的是实例方法(没有被static修饰的方法),那局部变量表中第0位索引的变量槽默认是关键字“this”
- public void test() {
{
byte[] placeholder = new byte[1024 * 1024];
}
int a = 0;
// 这里操作GC,这次placeholder真的被回收了
// 上面两个例子placeholder所占用的变量槽没有被复用,而这里的
// 被复用了,老的placeholder对象实例失去引用自然会被回收
System.gc();
}
- 局部变量必须赋予初始值才能使用,不存在像类变量那样的基础类型默认初始值
-
操作数栈
- 编译的时候被写入到Code属性的max_stacks
- 栈帧之间的数据共享(比如方法之间的调用)
- 动态连接
-
方法返回地址
-
方法退出的过程实际上等同于把当前栈帧出栈
- 恢复上层方法的局部变量表和操作数栈
- 把返回值(如果有的话)压入调用者栈帧的操作数栈中
- 调整PC计数器的值以指向方法调用指令后面的一条指令等
-
方法调用
-
一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址
-
解析
- 所有方法调用的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用
- “非虚方法”(Non-Virtual Method)
- 静态方法
- 私有方法
- 实例构造器
- 父类方法
- 被final修饰的方法
- “虚方法”(Virtual Method)
- 在类加载的时候就可以把符号引用解析为该方法的直接引用
-
分派
- 静态分派
- 动态分派
-
动态类型语言的支持
- C++和Java等就是最常用的静态类型语言
-
invokedynamic指令
-
每一处含有invokedynamic指令的位置都被称作动态调用点(Dynamically-Computed Call Site),这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是CONSTANT_InvokeDynamic_info常量
- 引导方法(Bootstrap Method,该方法存放在BootstrapMethods属性中)
- 方法类型(MethodType)
- 名称
-
基于栈的字节码解释执行引擎
- Javac编译器输出的字节码指令流,基本上是一种基于操作数栈的指令集架构(Instruction Set Architecture, ISA),字节码指令流里面的指令大部分都是零地址指令
-
基于栈的解释器执行过程
-
三种编译
-
前端编译器(叫“编译器的前端”更准确一些)把.java文件转变成.class文件的过程
- Javac
- Eclipse JDT中的增量式编译器(ECJ)
-
Java虚拟机的即时编译器(常称JIT编译器,Just In Time Compiler)运行期把字节码转变成本地机器码的过程
- HotSpot虚拟机的C1、C2编译器,Graal编译器
-
使用静态的提前编译器(常称AOT编译器/Ahead Of Time Compiler、后端编译)直接把程序编译成与目标机器指令集相关的二进制代码的过程
- JDK的Jaotc
- GNU Compiler for the Java(GCJ)
- Excelsior JET
-
热点代码
-
热点探测
- 基于采样的热点探测(Sample Based Hot Spot Code Detection)
- 基于计数器的热点探测(Counter Based Hot Spot Code Detection)
-
HotSpot实现的探测计数器
- 方法调用计数器(Invocation Counter)
- 默认并不是方法被调用的绝对次数,是一段时间之内方法被调用的次数
- 衰减(Counter Decay)
- -XX: -UseCounterDecay
- 绝对次数
- -XX: CounterHalfLifeTime
- 回边计数器(Back Edge Counter,“回边”的意思就是指在循环边界往回跳转)
- 统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令就称为“回边(Back Edge)”
-
三种编译器与解释器的搭配模式
-
混合模式(Mixed Mode)
- HotSpot默认的方案,JVM采取解释器和其中一个编译器直接配合的运行模式,编译器根据自身的版本以及宿主机器的硬件性能自动选择
-
解释模式(Interpreted Mode)
- 通过-Xint参数开启,该模式初始化启动虚拟机时间快,更适合客户端应用
-
编译模式(Compiled Mode)
- 通过-Xcomp参数开启,该模式需要更长的预热时间,在服务器资源充足的情况下,更适合服务端应用
-
虚拟机性能监控、故障处理工具
-
基础故障处理工具(命令行)
-
jps
- 类似Unix的ps命令,可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID (LVMID, Local Virtual Machine Identifier) 。
- 对于本地虚拟机进程来说,LVMID与操作系统的进程ID(PID, Process Identifier)是一致的,使用Windows的任务管理器或者UNIX的ps命令也可以查询到虚拟机进程的LVMID,但如果同时启动了多个虚拟机进程,无法根据进程名称定位时,那就必须依赖jps命令显示主类的功能才能区分了。
-
jstatd
- 可以很方便地建立远程RMI服务器
-
jstat(JVM Statistics Monitoring Tool)
- 用于监视虚拟机各种运行状态信息的命令行工具
-
jinfo(Configuration Info for Java)
- 实时查看和调整虚拟机各项参数
-
jmap(Memory Map for Java)
- 生成堆转储快照(般称为heapdump或dump文件)
- 也可用-XX:+HeapDumpOnOutOfMemoryError参数”暴力“获取dump文件
-
jhat(虚拟机堆转储快照分析工具)
- 一般少在生产环境用,图形化工具比它更好用
-
jstack(Stack Trace for Java)
- 生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)
-
java.lang.Thread.getAllStackTraces()
- 通过JDK提供的API获取虚拟机所有线程的StackTraceElement对象,也可以查看线程状况。
- jcmd
-
可视化故障处理工具
-
JConsole(最古老)
- Memory标签
- Threads标签
- JHSDB(JDK 9之后才正式提供)
- VisualVM
-
Oracle Java SE Advanced & Suite
-
Java Mission Control(JMC)
- 作为JMX控制台,显示来自虚拟机MBean提供的数据;
- 另一方面作为JFR的分析工具,展示来自JFR的数据。
- AMC(Java Advanced Management Console)
- JUT (Java Usage Tracker)跟踪系统
-
JFR(Java Flight Recorder)
- JFR在生产环境中对吞吐量的影响一般不会高于1% (甚至号称是Zero Performance Overhead)
- JProfiler
- YourKit
-
HotSpot虚拟机插件及工具
- HSDIS(JIT生成代码反编译)
-
OutOfMemoryError异常
-
Java堆溢出
- 可以通过工具分析JVM堆转储快照文件(hprof文件)
-
虚拟机栈和本地方法栈溢出
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
- 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将拋出OutOfMemoryError异常 。
- 方法区和运行时常量池溢出
-
本机直接内存溢出
- Heap Dump文件中不会看见有什么明显的异常情况,如果发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了。
-
虚拟机调优
-
调优案例分析与实战
-
大内存硬件上的程序部署策略
-
机器上只有单个的Java虚拟机实例来管理大量的Java堆内存
- 降低Full GC的频率
- 应用中绝大多数对象能否符合“朝生夕灭”的原则
- 大多数对象的生存时间不应当太长,尤其是不能有成批量的、长生存时间的大对象产生,这样才能保障老年代空间的稳定。
- 在服务器空闲时执行定时任务的方式触发Full GC(甚至是重启服务器)
- 需要考虑的问题
- ZGC和Shenandoah这种低延迟的最好解决方案目前尚未完全成熟(在任意堆内存大小下都能很好地做到低延迟GC)。
- 大内存的情况下,64位虚拟机的性能测试结果普遍略低于相同版本的32位虚拟机。
- 大型单体应用发生堆内存溢出时,几乎无法产生堆转储快照(要产生十几GB乃至更大的快照文件),即使生成了也难以分析这么大的转储快照,如果一定要分析可能要用JMC这种工具。
- 在64位虚拟机中消耗的内存一般比32位虚拟机要大(可以开启(默认即开启)压缩指针功能来缓解)。
- 同时使用若干个Java虚拟机,建立逻辑集群来利用硬件资源
- 堆外内存导致的内存溢出(直接内存溢出)
- 外部命令导致系统缓慢(比如Shell脚本)
- 不合适的数据结构导致内存占用过大
-
trick
- 64位虚拟机可以开启(默认即开启)压缩指针功能来缓解
-
虚拟机类加载机制
-
类的生命周期
-
类的加载时机
-
如果类型没有进行过初始化,则需要先触发其初始化阶段(主动引用)
- 使用new关键字实例化对象的时候
- 读取或设置一个类型的静态字段(被final修饰、 已在编译期把结果放入常量池的静态字段除外)的时候
- 调用一个类型的静态方法的时候
- 使用java.lang.reflect包的方法对类型进行反射调用的时候
- 如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
- 用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类
- 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化
-
其他引用类型的方式都不会触发初始化(被动引用)
- 通过子类引用父类的静态字段
- 通过数组定义来引用类
- 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
- 当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化
-
类的加载过程
- 加载
-
验证
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
-
准备
- 这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起起分配在Java堆中
-
public static int value = 123;
- 不会被预置为123的值的,123的赋值操作将在后面的初始化阶段中进行
-
public static final int value = 123;
- 常量池的字段属性表中的ConstantValue属性时,那么其在准备阶段会被初始化为具体指定的值
-
解析
-
直接引用
- 可以直接指向目标的指针
- 相对偏移量
- 是一个能间接定位到目标的句柄
-
符号引用
- 类或接口的解析
- 字段解析
- 方法解析
- 接口方法解析
- 对象类型、对象实例、字段、方法等实际运行时内存布局中的入口地址
-
初始化
-
<clinit>()
- 由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
- static {
test = 2;
System.out.println(test);// 这句编译不通过
}
static int test = 1;
- 父类的<clinit>()方法比子类的先执行
- 没有静态语句块与静态变量的赋值操作的类或接口,可以不生成<clinit>()
- <clinit>()方法是线程安全的,所以其里面有很耗时的初始化操作,当多个线程同时去初始化其类时,会造成线程阻塞
- 这也就是为什么有些资源配置加载逻辑会放到类的静态代码块来完成
- 如果程序运行的全部代码(包括自已编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用-Xverify: none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间
-
类加载器
- 双亲委派模型
-
虚拟机执行子系统
-
Class类文件结构
-
数据类型
- 无符号数(u1、u2、u4、u8)
- 表(由多个无符号数或者其他表作为数据项构成的复合数据类型)
-
魔数与Class文件的版本
-
头4个字节被称为魔数(Magic Number)
- 使用魔数而不是扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以随意改动
- 紧接着魔数的4个字节存储的是Class文件的版本号
-
常量池
- 常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count,计数从1开始,所以代表的真实常量数要减一)
- 字面量(Literal)
-
符号引用(Symbolic References)
- 当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中
- 截至JDK 13,常量表中分别有17种不同类型的常量
- CONSTANT_Class_info型常量
- CONSTANT_Utf8_info型常量
-
访问标志
- 总共16个,目前已用9个,7个归零不用
- 类索引、父类索引与接口索引集合
-
字段表集合
- 字段表(field_info)用于描述接口或者类中声明的变量
- 各种布尔值的修饰符(标志位)
- name_index;存放全限定名和简单名称
- descriptor_index:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值
- 对于数组类型,使用前置的”[“字符来描述,如一个定义为java.lang.String[][]类型的二维数组将被记录成[[Ljava/lang/String;,一个整型数组int[]将被记录成[I
- 对于方法的描述,按照先参数列表、后返回值的顺序,参数列表按照参数的严格顺序放在一组小括号()之内。如方法void inc()的描述符为()V,方法java.lang.String toString()的描述符为()Ljava/lang/String;,方法int indexOf(char[]source, int sourceOffset, int sourceCount,char[]target, int targetOffset,int targetCount, int fromIndex)的描述符为([CII[CII)I
- 字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本Java代码之中不存在的字段
-
方法表集合
- 与字段表集合高度相似
- 访问标志(access_flags)
- 名称索引(name_index)
- 描述符索引(descriptor_index)
- 属性表集合(attributes)
- Code属性表
- 方法体里面的Java代码,经过Javac编译器编译成字节码指令之后,存放在方法属性表集合中一个名为Code的属性里面
- 接口或者抽象类中的方法不存在此表
- 操作数栈和局部变量表直接决定该方法的栈帧所耗费的内存
- 局部变量表中的变量槽一般在栈帧中会被重用
- Exceptions属性表
- 其他属性表
- 异常表(try-catch-finally)(在Code属性中并不是必须存在的)
- 要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,返回值不会包含在特征签名之中,返回值不会包含在特征签名之中
-
属性表集合
- 等等
-
异常表
- 如果当字节码从第start_pc行到第end_pc行之间(不含第end_pc行)出现了类型为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。当catch_type的值为0时,代表任意异常情况都需要转到handler_pc处进行处理
- 《Java虚拟机规范》中明确要求Java语言的编译器应当选择使用异常表而不是通过跳转指令来实现Java异常及finally处理机制
-
Exceptions属性
- 列举出方法中可能拋出的受查异常(Checked Excepitons),也就是方法描述时在throws关键字后面列举的异常
- Class文件中由这三项数据来确定该类型的继承关系
-
Java程序
- 方法体的代码(Code属性)
- 元数据(Metadata,包括类、方法、字段的定义及其他信息)
-
字节码
-
字节码与数据类型
- 大多数指令都包含其操作所对应的数据类型信息
-
不同类型的字节码指令
- 加载和存储指令
- 运算指令
- 类型转换指令
- 对象创建与访问指令
- 操作数栈管理指令
- 控制转移指令
- 方法调用和返回指令
- 异常处理指令
- 同步指令
- 顾名思义字节码长度只能是一个字节(即0~255),这意味着指令集的操作码总数不能够超过256条