一款ava非常全面细致的Jvm架构图学习图,关注这一篇就够了
2021-04-17 08:47:05 55 举报
一款java非常全面细致r的Jvm架构图,包含java的类加载子系统、双亲委派机制、字节码、类加载器的讲解等
作者其他创作
大纲/内容
方法区Method Area
JVM的详细结构图
PC Registers
年轻代(Young/Newgeneration)
Thread1
15
Thread2
N
老年代
是
执行引擎
Code Optimizer
int
long
float
double
reference
1、前面提过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派( instruction dispatch) 次数和内存读/写次数。2、由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的 设计者们提出了栈项缓存(ToS,Top-of-Stack Cashing) 技术,将栈项元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
1、存储在JVM中的Java对象可以被划分为两类: ➢一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速 ➢另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。 2、Java堆区进一步细分的话, 可以划分为年轻代(YoungGen)和老年代(0ldGen)3、其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区)。
2
..........
Y
运行时数据区(Runtime Data Area)
否
伊甸园区
字节码指令集:Exception table :from to target type4 16 19 any19 21 19 any
一、在JVM中T将符号引用转换为调用方法的直接引用与方法的绑定机制相关。● 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的9过程称之为静态链接。● 动态链接:如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。二、对应的方法的绑定机制为:早期绑定(Early Binding) 和晚期绑定(Late Binding) 。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一-次。● 早期绑定:早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,。即可将这个方法与所属的类型进行绑定,这样- -来,由于明确了被调用的目。标方法究竟是哪一一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。● 晚期绑定:如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。三、小总结● 随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一-定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。● Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虛函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。
方法区
堆空间(The Heap Space)
1、简单地讲,一个Native Method就 是-一个Java调用非Java代码的接口。一个Native Method是这样 一个Java方法:该方法的实现由非Java语言实现,比如C,这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern \"C\"告 知C++编译器去调用-一个C的函数。2、\"A native method is a Java method whose implementation isprovided by non-java code.\"3、在定义-一个native method时,并不提供实现体(有些像定义一一个Javainterface),因为其实现体是由非java语言在外面实现的。4、本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。
7、栈顶缓存(Top-of- StackCashing)技术
类加载子系统Class Loader
Java栈
Return Value
。。。。。。。
方法返回地址Return Address
堆的核心概述:内存细分部份
对象产生及GC过程
●Java虚拟机栈是什么? 1、Java虚拟机栈(Java Virtual_ Machine Stack) ,早期也叫Java栈。每个线程在创建时都会创建--个虚似机栈,其内部保存-一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。 2、是线程私有的●生命周期:生命周期和线程一致。●作用:主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。 1、局部变量 vs 成员变量(属性) 2、基本数据变量 vs 引用数据类型(类、数组、对象)●栈的特点: 1、栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。 2、JVM直接对Java栈的操作只有两个,➢每个方法执行,伴随着进栈(入栈、压栈)➢执行结束后的出栈工作 3、对于栈来说不存在垃圾回收问题 4、Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。●设置栈内存大小:我们可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。官方文档设置栈大小的地址:https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE
1
四、图解对象分配过程
二、设置堆内存与OOM
S0(from)
Eden分配
Eden
A、堆空间内部结构(JDK1.7)
GarbageCollection
举例:静态变量与局部变量的对比
Eden放得下
Survivor 1(To)
Execution Engine
逃逸分析概述
PC寄存器器介绍
方法信息
Heap
PC寄存器
●每一个独立的栈帧中除了包含局部变量表以外,还包含- -个后进先出(Ladt- In-First-Out)的操作数栈,也可以称之为表达式栈(Expression Stack) 。 ●操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push) /出栈(pop)。➢某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。 使用它们后再把结果压入栈。➢比如:执行复制、交换、求和等操作
B、堆空间内部结构(JDK1.8)
FGC
6、操作数栈(Operand Stack)
2、为什么需要把Java堆分代?不分代就不能正常工作了吗?(jdk8)答:其实不分代完全可以,分代的唯- - 理由就是优化Gc性能。如果没有分代,那所有的对象都在一块,就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用,这样就会对新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把行回收,这样就会腾出很大的空间出来。
一、JVM在进行GC时,并非每次都对三个内存(新生代、老年代、方法区)区域一起回收,大部分时候回收的都是指新生代。针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型: ➢ 一种是部分收集(Partial GC), ➢ 一种是整堆收集(Full GC)。1、 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为: ➢ 新生代收集(MinorGC/YoungGC):只是新生代的垃圾收集 ➢ 老年代收集(MajorGC/0ldGC):只是老年代的垃圾收集。 a、目前,只有CMS GC会有单独收集老年代的行为。 b、注意,很多时候Major GC会和Fu1l GC混淆使用,需要具体分辨是老年代回收还是整堆回收。 ➢ 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。V目前,只有G1 GC会有这种行为2、 整堆收集(Fu11 GC):收集整个java堆和方法区的垃圾收集。
Native MethodInterface(JNI)
动态链接
Stack Frame
一、对象分配过程: TLAB,为什么有TLAB ( Thread Local Allocation Buffer ) ?1、堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据2、由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的3、为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。二、什么是TLAB?1、从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。2、多线程同时分配内存时,使用TLAB可以避免--系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。3、据我所知所有OpenJDK衍生出来的JVM都提供了TLAB的设计。
当前线程
S1
Extension Class Loader
java堆区
1、OutOfMemory例子:public class OOMTest { public static void main(String[] args) { ArrayList<Picture> list = new ArrayList<>( ); while(true){ list. add(new Picture(new Random( ). nextInt(1024 * 1024))); } }}2、抛出的异常如下Exception in thread \"main\" java. lang . outofMemoryError: Java heap spaceat com. atguigu. java.Picture. <init>(OOMTest. java:25)at com. atguigu. java .00MTest . main(OOMTest. java:16)
Initialization
本地方法接口Native Method Interface(JNI)
一、什么是本地方法?
JDK7
Application Class Loader
JDK8
5、JVM类加载器的双亲委派机制
方法返回地址
六、Java程序对类的使用方式分为主动使用和被动使用
类型
二、最简单的分代式GC策略的触发条件1、年轻代GC(Minor GC)触发机制: ➢ 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次 Minor GC会清理年轻代的内存。) ➢ 因为Java对象大多都具备朝生夕灭的特性,所以MinorGC非常频繁,一般回收速度也比较快。这一-定义既清晰又易于理解。 ➢ Minor GC会引发STW, 暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。2、老年代GC (Major GC/Fu1l GC)触发机制: ➢ 指发生在老年代的GC,对象从老年代消失时,我们说“Major GC”或“Fu1l GC”发生了。 ➢ 出现了Major GC,经常会伴随至少一次的Minor GC (但非绝对的,在Paral1el Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。 也就是在老年代空间不足时,会先尝试触发Minor GC。 如果之后空间还不足,则触发Major GC ➢ Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。 ➢ 如果Major GC后,内存还不足,就报00M了。 ➢ Major GC的速度- -般会比Minor GC慢10倍以上。3、Fu11GC触发机制,触发Fu1l GC执行的情况有如下五种 (1) 调用System.gc()时,系统建议执行Fu11 GC,但是不必然执行 (2) 老年代空间不足 (3) 方法区空间不足 (4) 通过Minor GC后进入老年代的平均大小大于老年代的可用内存 (5) 由Eden区、survivor space0 (From Space) 区向survivor space1 (To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小 说明: full gc是开发或调优中尽量要避免的。这样暂时时间会短一些。
Thread
第一页
栈桢2
Old放得下吗?
6
Class Loader Subsystem
系统类加载器Application Class Loader
Class File字节码文件
双亲委派机制工作原理
装载类HolleLoader了吗
方法的调用:关于invokedynamic指令
操作数据栈总结
类加载子系统
学习更多,请扫码
结果
Target CodeGenerator
●操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。●操作数栈就是JVM执行引擎的一一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。●每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_ stack的值。●栈中的任何-一个 元素都是可以任意的Java数据类型。 ➢32bit的类型占用一个栈单位深度 ➢64bit的类型占用两个栈单位深度●操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访 问。●如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。●操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。●另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
结束
1、初始化阶段就是执行类构造器方法<clinit> ()的过程。2、此方法不需定义,是javac编译 器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。3、构造器方法中指令按语句在源文件中出现的顺序执行。4、<clinit> ()不同于类的构造器。(关联:构造器是虚拟机视角下的<init>())5、.若该类具有父类,JVM会 保证子类的<clinit>()执行前,父类的<clinit> ()已经执行完毕。6、虚拟机必须保证一一个类的<clinit> ()方法在多线程下被同步加锁。
虚拟机栈Java Virtual Machine Stack
一、Java语言中方法重写的本质: |1、找到操作数栈顶的第-一个元素所执行的对象的实际类型,记作C。2、如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java. lang. IllegalAccessError异常。3、否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。4、如果始终没有找到合适的方法,则抛出java. lang . AbstractMethodError异常。二、小总结:IllegalAccessError异常介绍,程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般情况下,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。
本地方法栈Native Method Stack
Profler
动态类型语言和静态类型语言:动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。说的再直白一点就是, 静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。
一、运行时数据区子系统概述
Interpreter
一、非虚方法:●如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。●静态方法、私有方法、final方法、实例构造器、父类方法都是非虛方法。●其他方法称为虚方法。二、虚拟机中提供了以下几条方法调用指令普通调用指令:1. invokestatic: 调用静态方法,解析阶段确定唯一方法版本2. invokespecial: 调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本3. invokevirtual: 调用所有虚方法4. invokeinterface: 调用接口方法动态调用指令: 5. invokedynamic: 动态解析出需要调用的方法,然后执行
bootstrap Class Loader
幸存者0区
Old Gen
JIT Compiler
栈桢3
10、一些附加信息:栈帧中还允许携带与Java虚拟机实现相关的一-些附加信息。例如:对程序调试提供支持的信息。
对象实例化
超大对象 否
加载Loadding
S0
堆区篇
Operand Stack
动态链接Dynamic Unking
二、为什么要使用Native Method
PC Register forThread 1
堆区Heap Area
Stack Area
栈桢1
pc register
作用:PC寄存器用来存储指向下一条指令的地址,也即指将要执行的指令代码。由执行引擎读取下一条指令。
10、方法返回地址(return address)
23
其他元素
栈底
开始
在发生MinorGC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。|1、如果大于,则此次Minor GC是安全的2、如果小于,则虚拟机会查看-XX : HandlePromotionFailure设置值是否允许担保失败。 a、如果HandlePromotionFailure=true, 那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。 b、如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的; c、如果小于,则改为进行一次Full GC。 d、如果HandlePromotionFailure=false, 则改为进行一次Full GC。2、在JDK6 Update24之后, HandlePromotionFailure 参数不会再影响到虚拟机的空间分配担保策略,观察OpenJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
多个线程共享区域
5、局部变量表(Iocal variables)
第四页
.........
public void testAddoperation() ;Code :0: bipush 152: istorle_ 13: bipush 35: istore 26: iload 17: iload 28: iadd8: istore_ 310: return
Promotion
Intermediate CodeGenerator
Servivor故得下?
老年代(OIdGen)
Survivor区
局部变量表
1、Java使用起来非常方便,然而有些层次的任务用Java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。●与Java环境外交互:有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。你可以想想Java需要与一\
3
小总结:前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
●每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。●在这个线程上正在执行的每个方法都各自对应-一个栈帧(Stack Frame)。●栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
●不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在-一个栈帧之中引用另外一个线程的栈帧。●如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。●Java方法有两种返回函数的方式,-种是正常的函数返回,使用return指令;另外- -种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
方法4
Thread3
TLAB1
TLAB2
TLAB3
1、堆空间大小的设置。● Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项\"-Xmx\"和”- Xms”来进行设置。 ➢“-Xms\"用于表示堆区的起始内存,等价于-XX: InitialHeapSize ➢“-Xmx”则用于表示堆区的最大内存,等价于-XX :MaxHeapSize ● 一旦堆区中的内存大小超过“-Xmx\"所指定的最大内存时,将会抛出OutOfMemoryError异常。● 通常会将-Xms 和- -Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。● 默认情况下,初始内存大小::物理电脑内存大小/ 64 ,最大内存大小:物理电脑内存大小/ 4
索引
4
对象分配过程:概述为新对象分配内存是一-件非 常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。1. new的对象先放伊甸园区。此区有大小限制。2.当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(MinorGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区。3.然后将伊甸园中的剩余对象移动到幸存者0区。4.如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。.5.如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。6.啥时候能去养老区呢? ➢ 可以设置次数。默认是15次。可以设置参数: -XX:MaxTenuringThreshold=<N>进行设置。7.在养老区,相对悠闲。当养老区内存不足时,再次触发GC: Major GC, 进行养老区的内存清理。8.若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生00M异常java. lang . OutOfMemoryError: Java heap space
Method Area
start to end
当前栈桢
方法的调用:虚方法表
NEW GEN(Eden + Survivor)
OLD GEN
放置在S0/S1区域
● Java虛拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。本地方法栈,也是线程私有的。● 允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的) ➢ 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虛拟机将会抛出一个stackoverflowError 异常。 ➢ 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虛拟机将会抛出一个OutOfMemoryError 异常。● 本地方法是使用C语言实现的。● 它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。● 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。 ➢ 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。 ➢ 它甚至可以直接使用本地处理器中的寄存器。 ➢ 直接从本地内存的堆中分配任意数量的内存。● 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求只本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。● 在Hotspot JVM中, 直接将本地方法栈和虚拟机栈合二为- -。
栈桢n
官网地址:https: L Idocs . oracle. com/javase/8/docs/technotes/tools/unix/java . html常用参数:● -XX: +PrintFlagsInitial :查看所有的参数的默认初始值● -XX:+PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改不再是初始值)|● -Xms: 初始堆空间内存 (默认为物理内存的1/64)● -Xmx:最大堆空间内存(默认为物理内存的1/4) ● -Xmn:设置新生代的大小。(初始值及最大值)● -XX: NewRatio: 配置新生代与老年代在堆结构的占比● -XX:SurvivorRatio: 设置新生代中Eden和S0/S1空间的比例● -XX:MaxTenuringThreshold: 设置新生代垃圾的最大年龄● -XX: +PrintGCDetails: 输出详细的GC处理日志 ➢打印gc简要信息: ①-XX: +PrintGC②-verbose:gc● -XX:HandlePromotionFailure: 是否设置空间分配担保
栈桢4
方法3
验证Verify
Permanent
TLAB的再说明:1、尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。2、在程序中,开发人员可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间。默3、认情况下,TLAB空 间的内存非常小,仅占有整个Eden空间的1号,当然我们可以通过选项“-XX :TLABWasteTargetPercent”设置TLAB空间所占用Eden空间的百分比大小。3、一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
向上委托
Tenured/Old
操作数栈Operand Stack
类加载示例
PERM GEN(-XX:MaxPermSize)
九、堆空间的参数设置
1、JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。2、在一条活动线程中,-一个时间点上,只会有一个活动的栈帧。即只有当前正.在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame) ,与当前栈帧相对应的方法就是当前方法(Current。Method),定义这个方法的类就是当前类(Current Class)3、执行引擎运行的所有字节码指令只针对当前栈帧进行操作。4、如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
小总结:● 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。● 正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
Old放得下
Heap Area
元空间
YGC/Minor GC
本地方法库
public void testAddOperation (){ byte i = 15; intj=8; intk=i+j;}
类加载子系统的概述
运行时数据区子系统
1、虚拟机栈概述
1、由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。2、优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
Native MethodLibrary
字节码指令信息
S0(to)
抛出异常
例如自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt. jar包中java\\lang\\String. class),报错信息说没有main方法,就是因为加载的是rt. jar包中的String类。这样可以保证对j ava核心.源代码的保护,这就是沙箱安全机制。
public class IHaveNatives { public native void methodNative1 ( int X ) ; public native static long methodNative2 () ; private native synchronized float methodNative3( Object 0 ) ; native void methodNative4 ( int[] ary ) throws Exception ;}标识符native可以与所有其它的java标识符连用,但是abstract除外。
Byte Code
类的加载过程
float p
Survivor 0(From)
HEAP-Xmx
Eden空间
年轻代(YoungGen)
初始化Initialization
2、Linking
年经代
方法2
运行时常量池
string constants
numberic constants
class references
field references
method references
name and type
六、堆空间的思想
新对象申请
伊句网(Eden)
域信息
LV OS DL RA
永久代
三、本地方法栈(Native Method Stack)
一、动态链接(或指向运行时常量池的方法引用)●每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking) 。比如: invokedynamic指令。●在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference) 保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
每个栈帧中存储着:1、局部变量表(Local variables)2、操作数栈(operand Stack) (或表达式栈)3、动态链接(Dynamic Linking) (或指向运行时常量池的方法引用)4、方法返回地址(Return Address) (或方法正常退出或者异常退出的定义)5、一些附加信息
三、年轻代与老年代
NativeStack
Resolve(解析)
一些附加信息
单个线程私有区域
4、获取ClassLoader的途径
二、程序计数器(PC寄存器)
4、虚拟机栈运行原理
Linking
程序计数器PC Register forThread 1
● 在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立-一个虚方法表(virtual method table) (非虛方法不会出现在表中)来实现。使用索引表来代替查找。● 每个类中都有一个虚方法表,表中存放着各个方法的实际入口。● 那么虚方法表是什么时候被创建的呢?虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
1、为什么需要把Java堆分代?不分代就不能正常工作了吗?(jdk7)答:经研究,不同对象的生命周期不同。70%-99%的对象是临时对象。 ➢新生代:有Eden、两块大小相同的Survivor (又称为from/to,s0/s1) 构成,to总为空。 ➢老年代:存放新生代中经历多次cG仍然存活的对象。
接下来学习区域
eden
三、虚拟机栈
9、方法的调用
拓展类加载器Extension Class Loader
YGC
本地方法接口、本地方法库
五、Mi nor GC、Major GC与Full GC
Prepare
Native Memory
int k
3、Initialization
16
幸存者1区
1、栈是运行时的单位,而堆是存储的单位。即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的只是数据存储的问题,即数据怎么放、放在哪儿。也可以理解成栈管运行,堆管存储。2、方法区有时可以使用本地内存。堆空间在内存中相当于最大的。
TLAB
0
NativeMethodStack
代码举例
Verify
老年代(Tenure /0ldGeneration)
1、Loadding
内存分配策略(或对象提升(Promotion)规则)1、如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次MinorGC ,年龄就增加1 岁,当它的年龄增加到- - 定q程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年伏中。2、对象晋升老年代的年龄阈值,可以通过选项-xx: MaxTenuringThreshold来设置。针对不同年龄段的对象分配原则如下所示:答:优先分配到Eden-->大对象直接分配到老年代(尽量避免程序中出现过多的大对象)-->长期存活的对象分配到老年代-->动态对象年龄判断(如果Survivor 区中相同年龄的所有对象大小的总和大于Survivor空间的-一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。)-->空间分配担保(➢-XX : HandlePromotionFailure)
一、逃逸分析概述1、如何将堆上的对象分配到栈,需要使用逃逸分析手段。2、这是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。3、通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。4、逃逸分析的基本行为就是分析对象动态作用域: ➢当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。 ➢当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。二、逃逸分析:代码优化,使用逃逸分析,编译器可以对代码做如下优化:1、栈上分配I将堆分配转化为栈分配。如果-一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。2、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。3、分离对象或标量替换。有的对象可能不需要作为-一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。小总结:开发中能使用局部变量的,就不要使用在方法外定义。三、代码优化之栈上分配1、JIT编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。2、常见的栈上分配的场景:在逃逸分析中,已经说明了。分别是给成员变量赋值、方法返回值、实例引用传递。四、代码优化之同步省略(消除)1、线程同步的代价是相当高的,同步的后果是降低并发性和性能。2、在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。五、代码优化之标量替换1、标量(Scalar)是指-一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。2、相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。3、在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。六、标量替换参数设置参数-XX: +EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配在栈上。七、逃逸分析小结:逃逸分析并不成熟1、关于逃逸分析的论文在1999年就已经发表了,但直到JDK 1. 6才有实现,而且这项技术到如今也并不是十分成熟的。2、其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复,杂的分析的,这其实也是一个相对耗时的过程。3、一个极端的例子, 就是经过逃逸分析之后,发现没有一一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。4、虽然这项技术 并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。5、注意到有一-些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择。据我所知,Oracle HotspotJVM中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上。5、目前很多书籍还是基于JDK 7以前的版本,JDK已经发生了很大变化,intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。但是,intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上。
对象存帮超过阀值
将Reference入栈
七、内存分配策略
晋升老年代
Prepare(准备)
准备Prepare
● 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。● Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。 ➢堆内存的大小是可以调节的。● 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。● 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区( ThreadLocal Allocation Buffer, TLAB) 。● 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area fromwhich memory for all class instances and arrays is allocated ) ➢我要说的是:“几乎”所有的对象实例都在这里分配内存。一从实际使用角度看的。● 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向I对象或者数组在堆中的位置。在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。● 堆,是GC( Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
例:
方法返回值
一、方法返回地址概述● 存放调用该方法的pc寄存器的值。● 一个方法的结束,有两种方式: ➢正常执行完成 ➢出现未处理的异常,非正常退出 ● 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中--般不会保存这部分信息。二、当一个方法开始执行后,只有两种方式可以退出这个方法:1、执行引擎遇到任意-一个方法返回的字节码指令(return) ,会有返回值传递给.上层的方法调用者,简称正常完成出口; ➢一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定。 ➢在字节码指令中,返回指令包含ireturn (当返回值是boolean、byte、char、short和int类型时使用)、lreturn、 freturn、 dreturn以及areturn, 另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用。2、在方法执行的过程中遇到了异常(Exception) ,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口。方法执行过程中抛出异常时的异常处理,存储在-一个异常处理表,方便在发生异常的时候找到处理异常的代码。
Loading
本地方法接口和本地方法栈篇
引导类加载器Bootstarp ClassLoader
Resolve
8
初始化
栈顶
关于Slot的理解
自定义类加载器Custom Class Loader
八、为对象分配内存(TLAB)
链接
1、主动使用,又分为七种情况:➢创建类的实例➢访问某个类或接口的静态变量,或者对该静态变量赋值➢调用类的静态方法➢反射(比如: Class . forName (\"com. atguigu. Test\") )➢初始化一个类的子类➢Java虚拟机启动时被标明为启动类的类➢JDK 7开始提供的动态语言支持:java. lang. invoke . MethodHandle实例的解析结果REF getStatic、 REE putStatic、REE invokeStatic句柄对应的类没有初始化,则初始化2、除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。
TLAB分配
object i
Initialization初始化
Local Variables
最简单的分代式GC策略的触发条件(Mi nor GC、Major GC与Full GC)
方法的调用:方法重写的本质;
8、动态链接(Dynamic Linking)
线程n
Tenured
报OOM异常
局部变量表Local Varable Table
iadd执行后
方法1
METASPACE(-XX:MetaspaceSize)
JVM PROCESS
一、堆的核心概述
1、获取当前类的ClassLoader:clazz . getClassLoader ()、例:ClassLoader classLoader = Class.forName(\"java.lang.String\").getClassLoader();2、获取当前线程上下文的ClassLoader:Thread. cur rentThread() . getContextClassLoader ()例:ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();3、获取系统的ClassLoader:ClassLoader . getSystemClassLoader ()例:ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();4、获取扩展类加载器Extension:例:ClassLoader extensionClassLoader = ClassLoader.getSystemClassLoader().getParent();4、获取调用者的ClassLoader:DriverManager. getCal lerClassLoader ()
调用HelloLoader.main()
方法的调用:虚方法与非虚方法
double q
堆区
2、虚拟机内存中的栈与堆
1、JVM中的程序计数寄存器(Program Counter Register) 中,Register 的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有 把数据装载到寄存器才能够运行。2、这里的寄存器并不是指的物理寄存器,JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。寄存器我们通常也叫PC计数器(或指令计数器)、或叫程序钩子。
线程1
Linking(链接)
更新PC计数器
执行引擎Execution Engine
阀值
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。●局部变量表。最基本的存储单元是Slot (变量槽)●局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。●在局部变量表里,32位以内的类型只占用一个slot (包括returnAddress类型),64位的类型(long和double)占用两个slot。➢byte、short、charr在存储前被转换为int,boolean 也被转换为int,0表示false,非0表示true。➢long 和double 则占据两个Slot。● JVM会为局部变量表中的每一个slot都分配一个访的索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。● 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每-一个s1ot.上。●如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问1ong或double类型变量)。●如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
第三页
Class Loader类装载器
解析Resolve
参数
Current ClassConstant PoolReference
十、堆是分配对象存储的唯一选择吗?
检测是否加载
类型信息
1、Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有--些会随着虚拟机启动而创建,随着虚拟机退出而销毁。|另外-一些则是与线程一- -对应的,这些与线程对应。的数据区域会随着线程开始和结束而创建和销毁。2、红色右边的为单独线程私有的,红色的为多个线程共享的。即: ➢每个线程:独立包括程序计数器、栈、本地栈。 ➢线程间共享:堆、堆外内存(永久代或元空间、代码缓存)3、Hotspot java虚拟机的后台线程如下: ➢虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型包括”\" stop-the-world\"的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。 ➢周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行。 ➢GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。 ➢编译线程:这种线程在运行时会将字节码编译成到本地代码。 ➢信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理。
Stack
类加载步骤(加载、解析、初始化)
沙箱安全机制
Eden放得下吗?
第二页
Runtime Data Areas
long m
操作数栈
3、虚拟机栈基本内容
Loading(加载)
1、参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。2、我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。3、和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。 public void test() { int i; System. out. println(i); } 如上面这样的代码是错误的,没有赋值不能够使用,编译会出错。4、在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。5、局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
Verify (验证)
JVM的结构图简图
● JVM字节码指令集- -直比较稳定,- -直到Java7中才增加了一个invokedynamic指令,这是Java为了实现「动态类型语言」支持而做的一种改进。● 但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。● Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这-块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。
分配对象内存
Class File
Meta Space
永久代(Permanentgeneration)
Slot的重复利用:栈帧中的局部变量表中的槽位是可以重用的,如果-一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
● 配置新生代与老年代在堆结构的占比,不过这些参数在开发中,我们一般不调。 ➢默认-Xx: NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3 ➢可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5● 在HotSpot中, Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1● 当然开发人员可以通过选项“-XX:SurvivorRatio\"调整这个空间比例。比如-XX: SurvivorRatio=8 ● 几乎所有的Java对象都是在Edeh区被new出来的。● 绝大部分的Java对象的销毁都在新生代进行了。 ➢IBM公司的专门研究表明,新生代中80号的对象都是“朝生夕死”的。● 可以使用选项\" -Xmn\"设置新生代最大内存大小 ➢这个参数一般使用默认值就可以了。
本章总结:一、年轻代是对象的诞生、成长、消亡的区域,-个对象在这里产生、应用,最后被垃圾回收器收集、结束生命。二、老年代放置长生命周期的对象,通常都是从Survivor区域筛选拷贝过来的Java对象。当然,也有特殊情况,我们知道普通的对象会被分配在TLAB.上;如果对象较大,JVM会试图直接分配在Eden其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM就会直接分配到老年代。二、当GC只发生在年轻代中,回收年轻代对象的行为被称为MinorGC。当GC发生在老年代时则被称为MajorGC或者Ful1GC。一般的, MinorGC 的发生频率要比MajorGC高很多,即老年代中垃圾回收发生的频率将大大低于年轻代。
0 条评论
下一页