JVM
2021-08-19 11:13:23 0 举报
AI智能生成
JVM
作者其他创作
大纲/内容
JVM内存区域
类加载子系统
运行时数据区
线程共享
方法区
各个线程共享的内存区域
存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
运行时常量池
常量池表
字面量
符号引用
符号引用翻译的直接引用
运行期间也可以将常量放入池中
String.intern()
当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
堆
所有线程共享的一块内存区域,
存放对象实例
线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。
线程私有
虚拟机栈
线程私有的,它的生命周期与线程相同
虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧 (Stack Frame)用于存储
局部变量表
操作数栈
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)
Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。
这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。
另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接
Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。
这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。
另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接
方法出口
指向方法区
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
如果Java虚拟机栈容量可以动态扩展 ,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常
PC
当前线程下一条指令的地址
唯一没有OOM的区域
本地方法栈
本地方法栈则是为虚拟机使用到的本地(Native)方法服务
本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
执行引擎
即时编译器 JIT Compiler
垃圾回收期 GC
本地库接口
本地方法库
HotSpot虚拟机对象
对象的创建
1. 检查对象的类是否加载
虚拟机遇到一条new指令时,先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程
2. 为对象分配内存
指针碰撞“(Bump The Pointer)
所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离
空闲列表“(Free List)
虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
3. 处理并发安全问题
同步处理: CAS+失败重试保证原子性
TLAB
每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local AllocationBuffer,TLAB),
哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定
4. 初始化分配的内存空间
内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为默认零值,
保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。
5. 对象头信息
hashcode
分代年龄
锁信息
6. 执行<init>方法
对象在内存中的存储布局
对象头 markword 8字节
锁信息
偏向锁位
第3个bit
锁标志位
第1,2个bit
偏向锁 101
存储着这个锁偏向的线程的指针
轻量级锁 00
指向线程栈中Lock Record的指针
重量级锁 10
指向互斥量mutex的指针
hashcode 不是重写的
GC信息
分代年龄 4bit 所以最多15次就需要到老年代
类型指针 class pointer 4字节(开启类型指针压缩)
数组长度(数组对象有)
实例数据
对齐 padding
需要补充对象长度使得可以被8整除, 因为CPU一次读取一个缓存行的大小为64bit
对象的访问定位
直接指针
指针: 指向对象,代表一个对象在内存中的起始地址。
优点: 直接访问 一次寻址
缺点: GC需要移动对象的时候, 指针需要变化
缺点: GC需要移动对象的时候, 指针需要变化
句柄访问
句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址
Java堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了到对象的指针和到对象类型的指针
优点: GC时, 对象被移动不影响句柄, 只是句柄中的实例数据指针变化
缺点: 间接访问, 两次寻址
缺点: 间接访问, 两次寻址
对象怎么分配?
判断是否可以栈上分配
判断是否大到分配到老年代
判断是否可以放到TLAB
判断是否被GC清理
判断分代年龄是否达到放入老年代
判断是否被GC清除
栈上-线程本地-Eden-Old
Object o = new Object() 在内存中占用多少字节?
16 字节
对象头 8字节
类指针 压缩后4字节 不压缩8字节
没有实例数据, padding 0字节
8+4 需要padding 4字节=16字节,
8+8不需要padding 16字节
8+8不需要padding 16字节
o OOP(OrdinaryObjectPointer) 压缩4字节, 不压缩8字节
padding
需要补充对象长度使得可以被8整除, 因为CPU一次读取一个缓存行的大小为64bit
垃圾收集器
判断对象不再被引用
引用计数算法
- 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的
- 但是,在Java领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就**很难解决对象之间相互循环引用**的问题。
- 但是,在Java领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就**很难解决对象之间相互循环引用**的问题。
可达性分析算法
通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),
如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
固定可作为GC Root的对象
虚拟机栈(栈帧中的本地变量表)中引用的对象
线程被调用的方法堆栈中使用到的 参数 局部变量表 临时标量
本地方法栈中JNI
(即通常所说的Native方法)引用的对象
方法区中类静态属性
Java类的引用类型静态变量
方法区中常量
字符串常量池(String Table)里的引用。
同步锁(synchronized关键字)持有的对象
Java虚拟机内部的引用
基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器
反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入
某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入GC Roots集合中
分代收集和局部回收, 只收集新生代的垃圾, 但是这些内存可能被老年代中的某些对象引用, 所以需要将这些其他关联区域的对象一起加入GC Root
分代收集和局部回收, 只收集新生代的垃圾, 但是这些内存可能被老年代中的某些对象引用, 所以需要将这些其他关联区域的对象一起加入GC Root
引用
垃圾收集算法
分代收集理论
弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
强分代假说(Strong Generational Hypothesis) :熬过越多次垃圾收集过程的对象就越难以消亡。
跨代引用假说(Intergenerational Reference Hypothesis)跨代引用相对于同代引用来说仅占极少数。
- 存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的
- 新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描
- 新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描
部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:
- 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
- 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
JVM内存分代模型
新生代
Eden+Survivor0+Survivor1 = 8:1:1
设置两个Survivor区最大的好处就是解决了碎片化,
- 刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;
- 等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1
- (这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)
- 等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1
- (这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)
老年代
分代年龄超过15 (CMS是6岁) 或者新生代放不下的大大对象放在老年代
老年代满了出发 Full GC
永久代1.7/元空间1.8
Class
永久代必须指定大小限制, 元数据可设置也可以不设置
字符串常量 1.7存放永久代 1.8存放堆
GC Tuning (generation)
尽量减少Full GC
标记清除
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象
缺点
执行效率不稳定
如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低
如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低
内存空间的碎片化问题,
标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
标记复制
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
缺点 : 复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费
Appel式回收
- HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局
- 把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间
- HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的
- 把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间
- HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的
分配担保
- Appel式回收还有一个充当罕见情况的“逃生门”的安全设计,当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)
- 如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保直接进入老年代
- 如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便将通过分配担保直接进入老年代
标记整理
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存,
Stop the World
- 如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行
- 如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行
HotSpot算法细节实现
根结点枚举
固定可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中
STW 所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的
在HotSpot的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的。一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈里和寄存器里哪些位置是引用。在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举
安全点
HotSpot也的确没有为每条指令都生成OopMap,记录了引用相关信息的位置,这些位置被称为安全点(Safepoint)
用户程序执行时并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是强制要求必须执行到达安全点后才能够暂停
线程如何跑到最近的安全点停顿下来
抢先式中断:
在垃圾收集发生时,系统首先把所有用户线程全部中断,如果发现有用户线程中断的地方不在安全点上,就恢复这条线程执行,让它一会再重新中断,直到跑到安全点上
主动式中断
当垃圾收集需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程时会不停地主动去轮询这个标志,一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。
安全区域
解决sleep blocked状态的线程, 他们没有分配CPU时间片, 也就无法运行到安全点
安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点
当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。
当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以离开安全区域的信号为止。
记忆集
记录从非收集区域到收集区的指针集合
为了避免在碰到跨带引用的对象时, 将整个非收集区域加入GC ROOT扫描范围
收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节
卡表(card table)
是一种实现记忆集的方式
卡表最简单的形式可以只是一个字节数组CARD_TABLE
数组每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”(Card Page)。
一般来说,卡页大小都是以2的N次幂的字节数(512Bytes)
数组每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”(Card Page)。
一般来说,卡页大小都是以2的N次幂的字节数(512Bytes)
一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),
没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。
没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。
写屏障
解决卡表元素如何维护的问题,例如它们何时变脏、谁来把它们变脏等。
有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻
HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的
写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面
在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作,也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值后的则叫作写后屏障(Post-Write Barrier)。
伪共享
- 处理并发底层细节时一种经常需要考虑的问题,现代中央处理器的缓存系统中是以缓存行(CacheLine)为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化或者同步)而导致性能降低
- 为了避免伪共享问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏
- 为了避免伪共享问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏
GC ROOT
虚拟机栈(栈帧中的本地变量表)中引用的对象
线程被调用的方法堆栈中使用到的 参数 局部变量表 临时标量
本地方法栈中JNI
(即通常所说的Native方法)引用的对象
方法区中类静态属性
Java类的引用类型静态变量
字符串常量池(String Table)里的引用。
同步锁(synchronized关键字)持有的对象
Java虚拟机内部的引用
基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器
反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入
某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入GC Roots集合中
分代收集和局部回收, 只收集新生代的垃圾, 但是这些内存可能被老年代中的某些对象引用, 所以需要将这些其他关联区域的对象一起加入GC Root
分代收集和局部回收, 只收集新生代的垃圾, 但是这些内存可能被老年代中的某些对象引用, 所以需要将这些其他关联区域的对象一起加入GC Root
三色标记
遍历对象图过程中遇到的对象,按照“是否访问过”这个条件标记成以下三种颜色:
黑色
表示对象已经被垃圾收集器访问过,且这个对象的所有引用(孩子结点)都已经扫描过。
灰色
表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用(孩子结点)还没有被扫描过
白色
表示对象尚未被垃圾收集器访问过。
用户线程与垃圾收集线程并发工作存在的问题
一种是把原本消亡的对象错误标记为存活,可以容忍的,产生了一点逃过本次收集的浮动垃圾而已,下次收集清理掉就好。
另一种是把原本存活的对象错误标记为已消亡,这就是非常致命的后果了,程序肯定会因此发生错误,
对象消失
两种情况
对象标记为黑色以后用户线程新增了一条黑色对象->白色对象的引用
对于一个白色对象, 删除了所有灰色对象到该白色对象的引用
解决方案
增量更新(Incremental Update)
新增黑色->白色对象的引用, 记录这个引用, 重新标记时以黑色对象为根再扫描一遍
当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,
等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。
这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。
这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
原始快照(Snapshot At The Beginning,SATB)
删除灰色->白色对象的引用, 记录这个引用, 重新标记时以灰色对象为根再扫描一遍
当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,
在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。
这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索
在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。
这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索
垃圾收集器
年轻代
Serial
单线程工作的收集器,只有一条工作线程完成垃圾回收, 并且Stop The Word(STW) 停止其他所有的工作线程
ParNew
Serial收集器的多线程并行版本,并且Stop The Word(STW) 目前只有它能与CMS收集器配合工作。
Parallel Scavenge
- 它同样是基于标记-复制算法实现的收集器,
- 也是能够并行收集的多线程收集器
STW
- 也是能够并行收集的多线程收集器
STW
老年代
Serial Old
- Serial收集器的老年代版本
- 同样是一个单线程收集器
- 使用标记-整理算法
STW
- 同样是一个单线程收集器
- 使用标记-整理算法
STW
Parallel Old
- Parallel Scavenge收集器的老年代版本,
- 支持多线程并发收集,
- 基于标记-整理算法实现
STW
- 支持多线程并发收集,
- 基于标记-整理算法实现
STW
CMS(Concurrent Mark Sweep)
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现
垃圾回收的4个步骤
初始标记(CMS initial mark)
STW, 找到所有的GC Root, 速度比较快
并发标记(CMS concurrent mark)
从GCRoot对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,用户线程可以与垃圾收集线程一起并发运行
重新标记(CMS remark)
STW
修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,
这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短
修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,
这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短
并发清除(CMS concurrent sweep)
清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
CMS总结
优点
并发收集、低停顿
缺点
对处理器资源非常敏感
并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低总吞吐量
CMS默认启动的回收线程数是(处理器核心数量+3)/4,
- 处理器核心数量 > 4 并发回收时垃圾收集线程只占用不超过25%
- 处理器核心数量 < 4 并发回收时垃圾收集线程 占用比例很高
- 处理器核心数量 < 4 并发回收时垃圾收集线程 占用比例很高
增量式并发收集器
并发标记、清理的时候让收集器线程、用户线程交替运行,尽量减少垃圾收集线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得较少一些,
无法处理“浮动垃圾”
浮动垃圾
并发标记过程之后用户线程产生的垃圾
并发标记时处于被引用的状态但是结束的时候不再被引用, 实际上是垃圾但是等到下一次清理才能被清理
需要预留内存给并发标记清理时用户线程使用
并发失败
要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure)
这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了
大量空间碎片产生
基于“标记-清除”算法实现的收集器
碎片导致大对象无法被分配, 触发Full GC
-XX:+UseCMS-CompactAtFullCollection
CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的, 这样空间碎片问题是解决了,但停顿时间又会变长
-XX:CMSFullGCsBefore-Compaction
要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理
增量更新 Increment Update
新增黑色->白色对象的引用, 记录这个引用, 重新标记时再扫描一遍
当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,
等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。
这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。
这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。
Mixed GC
G1
G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,老年代空间, 大对象区域。
收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果
分代区域划分
新生代
Eden
Survivor
老年代
Humongous区域
专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象
而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待
垃圾回收策略
Young GC
回收整个Eden区域时间接近-XX:MaxGCPauseMillis设定的值, 触发Young GC
如果远远小于 -XX:MaxGCPauseMillis设定的值, 增加年轻代的的region继续给新对象存放, 直到下一次Eden区满了,
Mixed GC
老年代堆的占有率达到参数 (-XX:InitiatingHeapOccupancyPercent) 设定的值触发,
回收所有Young, 部分Old(根据期望的GC停顿时间确定Old区域垃圾回收优先顺序), 以及大对象区域
G1会先做MixedGC, 使用复制算法, 把各个region存活的对象拷贝到别的region去, 拷贝过程如果发现没有足够的空region能够承载拷贝对象就触发Full GC
Full GC
停止系统程序, 采用单线程进行标记, 清理和压缩整理, 空出一些Region供下一层Mixed GC 使用, 非常耗时
垃圾回收流程
初始标记(Initial Marking)STW:
仅仅只是标记一下GC Roots能直接关联到的对象,需要停顿线程,但耗时很短
并发标记(Concurrent Marking):
从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
最终标记(Final Marking):STW
对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
筛选回收(Live Data Counting and Evacuation):STW
负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划
可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间
这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的
并发标记阶段如何保证收集线程与用户线程互不干扰地运行?
解决用户线程改变对象引用关系时,必须保证其不能打破原本的对象图结构
三色标记
CMS收集器采用**增量更新**算法实现,而G1收集器则是通过**原始快照(SATB)**算法来实现的
垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上,
G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。
G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围
原始快照 Snapshot At the Begining (SATB)
删除灰色->白色对象的引用, 记录这个引用, 重新标记时再扫描一遍
当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,
在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。
这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索
在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。
这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索
G1 vs CMS
可以指定最大停顿时间、分Region的内存布局、按收益动态确定回收集这些创新性设计带来的红利
CMS的“标记-清除”算法,
G1从整体来看是基于“标记-整理”,但从局部(两个Region之间)上看基于“标记-复制”算法实现
G1从整体来看是基于“标记-整理”,但从局部(两个Region之间)上看基于“标记-复制”算法实现
G1运作期间不会产生内存空间碎片
垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集
G1卡表维护的消耗比CMS要高
虽然G1和CMS都使用卡表来处理跨代指针,但G1的卡表实现更为复杂,而且堆中每个Region,无论扮演的是新生代还是老年代角色,都必须有一份卡表,这导致G1的记忆集(和其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间;
相比起来CMS的卡表就相当简单,只有唯一一份,而且只需要处理老年代到新生代的引用,反过来则不需要,由于新生代的对象具有朝生夕灭的不稳定性,引用变化频繁,能省下这个区域的维护开销是很划算的
在执行负载的角度
都使用到写屏障,CMS用写后屏障来更新维护卡表;
G1除了使用写后屏障来进行同样的(由于G1的卡表结构复杂,其实是更烦琐的)卡表维护操作外,为了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况
相比起增量更新算法,原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点,但是在用户程序运行过程中确实会产生由跟踪引用变化带来的额外负担
由于G1对写屏障的复杂操作要比CMS消耗更多的运算资源,所以CMS的写屏障实现是直接的同步操作,而G1就不得不将其实现为类似于消息队列的结构,把写前屏障和写后屏障中要做的事情都放到队列里,然后再异步处理
Class类文件结构
魔数
常量池
子主题
访问标志 ACC_FLAG
索引
类索引 This Class
父类索引 Super Class
接口索引 Interfaces
字段表
access_flags
name_index和descriptor_index
全限定名
- “org/fenixsoft/clazz/TestClass”是这个类的全限定名,仅仅是把类全名中的“.”替换成了“/”而已,
- 为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”号表示全限定名结束。
- 为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”号表示全限定名结束。
简单名称
- 指没有类型和参数修饰的方法或者字段名称,
- 这个类中的inc()方法和m字段的简单名称分别就是“inc”和“m”。
- 这个类中的inc()方法和m字段的简单名称分别就是“inc”和“m”。
描述符
用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。
基本数据类型
(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写V字符来表示,
对象类型
字符L加对象的全限定名
数组类型
每一维度将使用一个前置的“[”字符来描述
- 如一个定义为“java.lang.String[][]”类型的二维数组将被记录成“[[Ljava/lang/String;”,
- 一个整型数组“int[]”将被记录成“[I”
- 一个整型数组“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[CIII)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[CIII)I
方法表
acc_flags
descriptor_index u2
按照先参数列表、后返回值的顺序描述
参数列表按照参数的严格顺序放在一组小括号“()”之内。
- 如方法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[CIII)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[CIII)I
属性表
Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。
attribute_name_index
是一项指向CONSTANT_Utf8_info型常量的索引,此常量值固定为“Code”,它代表了该属性的属性名
max_stack
代表了操作数栈(Operand Stack)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。
max_locals
代表了局部变量表所需的存储空间。
在这里,max_locals的单位是变量槽(Slot),变量槽是虚拟机为局部变量分配内存所使用的最小单位。
- 对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,
- 而double和long这两种64位的数据类型则需要两个变量槽来存放。
在这里,max_locals的单位是变量槽(Slot),变量槽是虚拟机为局部变量分配内存所使用的最小单位。
- 对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,
- 而double和long这两种64位的数据类型则需要两个变量槽来存放。
code_length
code_length代表字节码长度,
this 存在局部变量表的第一个位置
Java语言里面的潜规则:在任何实例方法里面,都可以通过“this”关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,
而它的实现非常简单,仅仅是通过在Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。
因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个变量槽位来存放对象实例的引用,所以实例方法参数值从1开始计算
这个处理只对实例方法有效,如果代码清单6-1中的inc()方法被声明为static,那Args_size就不会等于1而是等于0了
而它的实现非常简单,仅仅是通过在Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。
因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个变量槽位来存放对象实例的引用,所以实例方法参数值从1开始计算
这个处理只对实例方法有效,如果代码清单6-1中的inc()方法被声明为static,那Args_size就不会等于1而是等于0了
字节码指令
虚拟机类加载机制
类加载时机
遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,
使用new关键字实例化对象的时候。
读取或设置一个类型的静态字段
调用一个类型的静态方法
使用java.lang.reflect包的方法对类型进行反射调用的时候
如果类型没有进行过初始化,则需要先触发其初始化。
当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
如果一个java.lang.invoke.MethodHandle实例最后的解析结果为
REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,
并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
接口的实现类发生了初始化,那该接口要在其之前被初始化。
接口
但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,
只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。
类加载过程
加载(Loading)
通过一个类的全限定名来获取定义此类的二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
链接(Linking)
验证(Verification)
文件格式验证
验证字节流是否符合Class文件格式的规范
- 是否以魔数0xCAFEBABE开头。
- 主、次版本号是否在当前Java虚拟机接受范围之内。
- 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
- 主、次版本号是否在当前Java虚拟机接受范围之内。
- 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
元数据验证
对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求
- 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
- 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
- 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)
字节码验证
通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况。
- 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
- 保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的
- 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
- 保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的
符号引用验证
最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用 的时候
- 符号引用中通过字符串描述的全限定名是否能找到对应的类。
- 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
- 符号引用中的类、字段、方法的可访问性(private、protected、public、<package>)是否可被当前类访问
- 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
- 符号引用中的类、字段、方法的可访问性(private、protected、public、<package>)是否可被当前类访问
准备(Preparation)
这时候进行内存分配的仅包括类变量,而不包括实例变量
赋值为数据类型的默认零值
解析(Resolution)
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用
直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄
初始化(Initialization)
初始化阶段就是执行类构造器< clinit>()方法的过程
< clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,
编译器收集的顺序是由语句在源文件中出现的顺序决定的,
静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
编译器收集的顺序是由语句在源文件中出现的顺序决定的,
静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
< clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成< clinit>()方法
Java虚拟机必须保证一个类的< clinit>()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的< clinit>()方法
类加载器
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间
双亲委派
启动类加载器(Bootstrap Class Loader)
加载Java核心类库的类
启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替
扩展类加载器(Extension Class Loader)
加载Java扩展类库的类
sun.misc.Launcher$ExtClassLoader
应用程序类加载器(Application Class Loader)
负责加载用户类路径(ClassPath)上所有的类库
ClassLoader类中的getSystemClassLoader()方法的返回值
sun.misc.Launcher$AppClassLoader
双亲委派模型(Parents Delegation Model)
父类加载器不是只语法上的继承关系而是ClassLoader中的parent属性, 这是final确定好的, 调用父类的加载器就是parent.loadClass()
工作流程
如果一个类加载器收到了类加载的请求,查看是否加载过, 加载过返回, 没加载过, 它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,
每一个层次的类加载器都是如此
因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,
只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
为什么使用双亲委派模型?
保证一个类只被加载一次, 唯一的存在于内存中
- 例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,
- 因此Object类在程序的各种类加载器环境中都能够保证是同一个类。
- 反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱
- 因此Object类在程序的各种类加载器环境中都能够保证是同一个类。
- 反之,如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中就会出现多个不同的Object类,Java类型体系中最基础的行为也就无从保证,应用程序将会变得一片混乱
loadClass
父类加载器存在于每个加载器的parent属性中, 并且这个属性是final的不可改变的
loadClass的时候先查看这个类是否加载过, 加载过了就不会加载直接返回
没加载过, 先调用parent.loadClass() 类似递归调用父类的加载器的loadClass(), 如果父类加载过了, 就返回, 没有加载过还是递归返回到当前这个类
这时候需要当前类去调用findClass()方法, 而ClassLoad类的findClass()方法直接抛出了异常
所以, 在重写自己的类加载器的时候, 重写这个findClass()就可以有自己的类加载器的实现了
所以, 在重写自己的类加载器的时候, 重写这个findClass()就可以有自己的类加载器的实现了
如何打破双亲委派
自定义类加载器
继承ClassLoader 重写loadClass() 和 findClass()方法
打开需要找到的class文件 File
通过File文件得到一个FileInputStream文件二进制流
将二进制流写入一个字节数组输出流
调用defineClass()方法, 传入Class文件名称, 字节数组 将字节数组转换成Class对象
虚拟机字节码执行引擎
运行时栈帧结构
局部变量表
局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量
局部变量表的容量以变量槽(Variable Slot)为最小单位通常每个变量槽占用32位长度的内存空间
对于64位的数据类型,Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间, 对于两个相邻的共同存放一个64位数据的两个变量槽,虚拟机不允许采用任何方式单独访问其中的某一个
如果执行的是实例方法(没有被static修饰的方法),那局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字**“this”**来访问到这个隐含的参数。
为了尽可能节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重用的
操作数栈
同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。
举个例子,例如整数加法的字节码指令iadd,这条指令在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会把这两个int值出栈并相加,然后将相加的结果重新入栈。
动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)
Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。
这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。
另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接
Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。
这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。
另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接
方法出口
方法调用
方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还未涉及方法内部的具体运行过程。
动态类型语言支持
基于栈的字节码解释执行引擎
前端编译与优化
类型擦除
Java泛型只在程序源码中存在,在编译后的字节码文件中,全部泛型都被替换为原来的裸类型(Raw Type,稍后我们会讲解裸类型具体是什么)了,并且在相应的地方插入了强制转型代码
擦除法对实际编码带来的不良影响,由于List<String>和List<Integer>擦除后是同一个类型,我们只能添加两个并不需要实际使用到的返回值才能完成重载
JIT
对于热点代码, 编译成机器码缓存起来, 下次调用直接使用机器码
编译器优化
方法内联
把目标方法的代码原封不动地“复制”到发起调用的方法之中,避免发生真实的方法调用而已
为了解决虚方法的内联问题,Java虚拟机首先引入了一种名为类型继承关系分析(Class HierarchyAnalysis,CHA)的技术,
- 这是整个应用程序范围内的类型分析技术,用于确定在目前已加载的类中,某个接口是否有多于一种的实现、
- 某个类是否存在子类、某个子类是否覆盖了父类的某个虚方法等信息。
- 这样,编译器在进行内联时就会分不同情况采取不同的处理:
- 这是整个应用程序范围内的类型分析技术,用于确定在目前已加载的类中,某个接口是否有多于一种的实现、
- 某个类是否存在子类、某个子类是否覆盖了父类的某个虚方法等信息。
- 这样,编译器在进行内联时就会分不同情况采取不同的处理:
逃逸分析
逃逸分析的基本原理是:
- 分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,
- 例如作为调用参数传递到其他方法中,这种称为方法逃逸;
- 甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;
- 从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度
- 分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,
- 例如作为调用参数传递到其他方法中,这种称为方法逃逸;
- 甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;
- 从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度
栈上分配 (Stack Allocations)
如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。
**栈上分配可以支持方法逃逸,但不能支持线程逃逸**。
标量替换(Scalar Replacement):
如果把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换
不允许对象逃逸出方法范围内
同步消除(Synchronization Elimination)
线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以安全地消除掉
Java内存模型 JMM
线程安全与锁优化
JVM调优
打印GC信息, 设置堆大小
-Xms200M
堆最小内存
-Xmx200M
堆最大内存
-XX:PrintGC
打印GC信息
jps
显示所有的Java进程, 以及进程ID
jinfo Java进程ID
显示Java进程的详细信息
jstat -gc Java进程ID
统计Java进程的统计信息, 内存各个区域的使用情况
top
跟踪消耗资源的进程, 显示进程PID, 消耗CPU, 内存情况
top -Hp 进程PID
显示进程中线程的资源消耗情况
jstack Java进程ID | more
显示进程所有线程的名称, 状态, 调用方法
jmap -histo 进程ID | head -20
列出所有的类的, 产生的对象, 以及占用的内存, 只显示占用内存最多的前20个
jmap -dump:format=b, file=文件名.hprof Java进程PID
需要暂停堆, STW 导出堆的信息
format=b 二进制文件的形式
file= 文件名
-XX:HeapDumpOnOutOfMemoryError
产生OOM自动dump文件
0 条评论
下一页