JVM方法的执行是基于堆栈,方法调用 - 输入堆栈,方法调用是完整的堆栈,了解JVM的堆栈结构,可帮助我们分析,了解字节代码和方法调用更多实现过程。
为了学习方法调用,我们可以帮助我们了解从字节代码级别加载和重写调用方法的方法的规则。
堆栈框架是一种数据结构,用于支持用于方法调用和方法执行的虚拟机。它是虚拟机中虚拟机堆栈中的堆栈元素。堆栈框架存储信息,例如本地变量表,操作号码堆栈,动态连接和方法返回地址。执行的完成后,对应于虚拟机堆栈中堆栈帧的过程,从输入堆栈到堆栈。
编译程序代码时,堆栈框架需要最大的本地变量表,并且已完全确定了最深和深的操作编号堆栈,并编写了方法表代码属性。因此,运行数据的效果。
在线程中调用链条的方法可能很长,并且在同一时间执行许多方法。但是,对于执行引擎,仅堆栈顶部的堆栈框架有效。它称为当前堆栈框架。与此堆栈框架关联的方法称为当前方法。该方法的类称为当前类。本地变量表和操作号码堆栈的各种操作通常是指当前堆栈帧上本地变量表和操作号码堆栈的操作。如果当前方法调用其他方法,或者调用其他方法,或执行当前方法,此方法的堆栈框架不再是当前堆栈框架。当调用新方法时,还将创建一个新的堆栈帧,并且随着程序控件转移到新方法时,它将成为新的当前堆栈框架。返回该方法后,当前的堆栈帧将将此方法的执行结果返回到先前的堆栈帧。返回方法后,将当前的堆栈框架丢弃,并且先前的堆栈框架是当前的堆栈框架重新定位。
堆栈框架是线程的私有数据,不可能在一个堆栈框架中引用另一个线程的堆栈框架。
典型虚拟机堆栈的框架结构如下所示:
本地变量表是可变量值的一组存储空间,用于存储由方法参数和方法内部定义的本地变量。本地变量表中的变量仅在当前方法调用中有效。当方法调用结束后,将作为方法堆栈框架破坏而破坏本地变量表。
在编译类别类时,方法的局部变量表的最大容量是在方法的max_locals数据项中确定的,而局部变量存储在代码属性中的localvariabletable属性表中。
局部变量表的容量是变量插槽的最小单位(以下称为插槽)。虚拟机规范并未清楚地表明插槽应占用的内存空间,但是非常指导每个插槽都应处于方向。退货地址。这8种类型的数据可以使用32位或较小的物理内存存储。在Java虚拟机械的数据类型中,只有两种类型的数据类型:长和双重。这些本地变量表中有两个数据点:您需要注意:
执行方法后,虚拟机使用本地变量表来完成参数变量列表的传输过程。如果是实例方法,则使用本地变量表中每个索引的插槽来引用传输方法的实例。可以通过关键字“ this”访问该方法以访问此隐藏的本地变量,并且其余参数以参数列表的顺序排列,以占据插槽位置,从1开始。可变顺序和范围分配以分配其余的插槽。
应该注意的是,局部变量不存在,例如类变量的“准备”阶段。当加载类时,类变量将通过“准备”和“初始化”阶段。它还将在“准备”阶段给出系统默认值,但是局部变量并非如此。本地变量表没有“准备”阶段,因此程序员手动将初始值赋予了局部变量。
由于本地变量表位于堆栈框架中,因此它将占据堆栈空间内存。因此,如果方法参数和局部变量更多,则局部变量膨胀,因此每种方法的方法数量将占据更多的堆栈空间,最终将导致数字数量(例如递归)减少。
运行后,可以看出,较少局部变量的递归调用深度可以更深。
每个局部变量都有自己的功能范围(字节代码的范围)。为了尽可能节省,可以重复使用本地变量表中的插槽空间。它不一定涵盖整个方法。如果当前字节代码PC计数器的值超出了某个变量的范围,则可以将与此变量相对应的插槽移交给其他变量。
使用Jclasslib打开类文件,并找到两种方法的本地变量表:
可以看出,Solt2方法的局部变量表已实现重复使用。
本地变量表的变量也由垃圾回收器判断为根节点。只要不回收由局部变量表直接或间接引用的对象。在某些情况下,插槽的复制直接影响系统的垃圾收集行为。
如下所示,VM参数设置为-XX:+PRINTGC以运行以下方法。
其中,Solt()方法用作对照。
运行solt0(),我的GC信息是:
[GC(System.gc()5202K-> 848K(249344K),0.0011430 secs]
在空气方法中,年轻的GC回收约5000k作为对照。以下示例需要排除5000k
运行solt1(),我的GC信息是:
[GC(System.gc()10322K-> 6000K(249344K),0.0029231秒]
可以看出,剩下年轻的GC后剩下的6000K,表明字节数组占据的内存未回收,因为字节数组由局部变量B引用,因此没有回收存储器。
运行solt2(),我的GC信息是:
[GC(System.gc()10322K-> 912K(249344K),0.0011081秒]
在垃圾回收之前,将变量B放置为空,因此字节没有参考。
可以看出,年轻的GC之后大约有1000k,并且在年轻的GC时,字节阵列被回收。
运行Solt3(),我的GC信息是:
[GC(System.gc()10322K-> 6000K(249344K),0.0036167 secs] [Full GC(System.gc())6000K-> 5800K(249344K),0.0049001 secs]]
我们在变量B的范围后进行了垃圾回收。由于变量B的范围已经结束,因此GC应恢复数组的内存,但是发现字节数组的内存尚未恢复。为什么?
尽管该代码已留下了变量B的范围,但之后,没有其他变量尚未重复使用FART变量表的读写和写入操作 - 最初由变量B占据的插槽尚未重复使用,因此GC根root root root nodess nodesome nodesome nodesome nodesome局部变量表仍然与之保持关联。该关联尚未及时中断,因此内存没有回收。
运行Solt4(),我的GC信息是:
[GC(System.gc()10322K-> 848K(249344K),0.0014418秒] [Full GC(System.gc())848K-> 656K(249344K),0.0048550秒]]]]]]
您可以看到内存是回收的,因为垃圾在变量B的回收过程中回收了变量B的范围,并声明了新变量C。此时,变量C将重复使用变量B的插槽,并且目前测量对数组的引用。请透明,因此随后的GC可以恢复数组的内存。
运行solt5(),我的GC信息是:
[gc(system.gc()10322k-> 6000k(249344k),0.0030734秒] [Full GC(System.gc())6000K-> 5800K(249344K),0.0046043 secs]5800K-> 5800K(249344K),0.0006343秒] [Full GC(System.gc())5800K-> 680K(249344K),0.0041057秒]]]
您可以看到,当调用GC方法时恢复现有的外部方法时,尽管Soltgc1()犯罪分子不会回收记忆,但在返回Soltgc1()方法后,其相应的堆栈框架也会被销毁。天然局部变量的局部变量表不再存在,因此在第二个GC中,可以回收阵列的内存。
操作号码堆栈通常称为操作堆栈,它也是进入第一个的堆栈结构。必须通过操作数字堆栈来通过参数传输许多字节,因此它主要用于保留中间结果计算过程,以及在计算过程中变量的临时存储空间的同时。
当方法刚刚开始执行时,操作号码堆栈为空。在方法执行过程中,将有各种字节码指令来编写和提取操作号码堆栈的内容,即堆栈和堆栈操作。操作号码堆栈中的数据类型必须与字节代码指令序列。编译程序代码时,必须严格保证在汇编过程中,并且必须在验证阶段的数据流分析中进行验证。
例如,当字节代码指令运行IADD时,需要将两个元素的操作编号的两个元素存储在int type.iadd的值中。IADD将取出堆栈的两个元素,然后添加,然后添加,然后将结果保存到操作号码堆栈中。
像局部变量表一样,操作堆栈操作的最大深度也位于编译期间方法表的代码属性的MAX_STACKS数据项中。操作堆栈的元素可以是任何Java数据类型,包括包括长和双倍,长和双重的数据类型占据了两个单元的堆栈深度,而其他数据类型则占据了单元的深度。
虚拟机的执行引擎也称为“基于堆栈的执行引擎”,“堆栈”是操作号码堆栈。
作为虚拟机堆栈的元素,两个堆栈框架在理论上是彼此独立的。但是,在实现大多数虚拟机时,将进行一些优化处理,这将重叠两个堆栈框架的一部分。以下堆栈框架的数字堆栈与上述堆栈帧的某些局部变量表重叠。这样,当方法调用时,可以使用一部分数据,并且无需执行其他参数复制传输。重叠过程如下图所示:
除了本地变量表和操作号码堆栈外,Java堆栈框架还需要一些数据来支持动态链接,方法返回地址和其他信息。他们共同称为堆栈框架信息。
每个堆栈框架在运行常数池中包含对堆栈框架方法的引用。它具有此参考,以支持呼叫期间的动态链接(动态链接)。在类文件的常数池中有大量符号引用。这些符号引用将在类加载阶段或何时将直接引用转换为直接引用它们是第一次使用。该转换称为静态分析。在每个操作过程中,另一部分被转换为直接参考,称为动态链接。
在一种方法开始执行后,只有两种方法可以退出此方法:
当方法退出时,需要将其返回到可以继续执行程序之前要调用的方法的位置。当方法正常退出时,可以将呼叫者的PC计数器值用作返回地址,计数器值可能会保存在堆栈框架中;当该方法是异常出口时,返回地址由异常设备表确定。通常,堆栈框架通常是信息的一部分,不会保存。返回呼叫方法后,将在呼叫方法中抛出相同的异常性,并将尝试使用呼叫方法的异常表来解决此异常。
退出过程实际上等同于将当前的堆栈框架从堆栈中置于堆栈中,因此退出时可能执行的操作是:
方法调用是指确认哪种呼叫方法,而不是执行方法的过程。由于Java代码被编译到类文件中,该方法的符号引用(常数池中的符号)存储在类文件中(方法)在常数池中的方法),而不是方法的方法(内存布局中方法的入口地址),因此在加载或运行阶段中,必须确认目标方法的直接引用。
在类加载的分析阶段,某些方法符号将转换为直接引用。该分析的前提是该方法在程序运行之前具有特定的呼叫版本,并且在操作期间不可能进行此方法的呼叫版本。change.theath.change.themend,可以在程序时确定呼叫目标编写,可以确定编译器。方法称为解析。
在Java语言中,它符合“可以知道的汇编期,并且运行期是不可变的”,主要包括两个类别:静态方法和私人方法。前者与类型直接相关,后者是外面看不到。这两种方法的特征确定它们不能通过继承或其他方法重写其他版本,因此它们适合在类加载阶段进行分析。
Java虚拟机规范提供了5种调用字节码指令的方法:
只要通过发言和调查指令调用它,就可以在解析阶段确定唯一的呼叫版本。转换符号引用到直接参考。这种方法称为非虚拟方法,其他方法是虚拟方法(最终方法除外)。
除了使用Invokestatic和InvokeScial外,还有一种方法是通过Final修改的。尽管最终方法是使用InvokeVirtual调用的,因为它不能涵盖并且没有其他版本,因此无需在该方法上进行多态性接收器。在Java虚拟机规范中,它明确指出了最终方法是错误的。
分析调用是一个静态过程。在编译期间,完全确定在班级加载的解析阶段,所有涉及的符号将转换为一定的直接参考,该参考不会在完成之前延迟到工作期。DISPATCH呼叫可能是静态的或动态的。根据派遣祖先的数量,可以将其分为单个细分和多划分。两种类型的两种类型的两种组合形式的两个组合形成了静态单个细分的四个划分,静态多动不分,静态多二维,动态单 - 单个 - 动态单个 -部门和动态多分段组。
Java是一种面向对象的编程语言,因为Java具有对象面向对象的基本特征:继承,包装,多态性。分布调用将揭示多态性特征的一些最基本的体现,例如“重载”和“重载”和“如何”重写“在Java虚拟机中实现。
案件:
程序运行结果如下:
你好,伙计,你好,伙计
为什么您选择一种参数类型的重新加载方法作为人类?在解决此问题之前,让我们看看两个重要的概念。
人类= new Man();
对于上述代码,人是变量的静态类型,人类是变量的实际类型。可以通过编译期确定变量的静态类型,并且需要确定实际类型直到运行时。(准确地说是编译器)在静态参数类型中而不是实际类型中作为确定的基础。由于编译期已知静态类型,Javac编译器将决定哪种重负载版本的参数静态类型,因此Sayhello(人)被选为呼叫目标,该方法的符号写入InvokeVirtual的参数中。
可以验证使用Javap -v staticdispatch.class查看字节码文件:
所有依赖性静态类型都定位执行分布版本的方法称为静态分布,静态分布的典型应用是重量负载。静态分布发生在编译期间,因此确定静态分布的作用不是由虚拟机执行的。
此外,尽管编译器可以确定该方法的重载版本,但在许多情况下,此重型版本不仅是“仅”,而且通常只能确定一个“更合适的”版本。这种模糊的结论是相对的”罕见的“计算机世界中由0和1组成的东西。产生此模糊结论的主要原因是文字的数量不需要定义,因此字符中没有显示静态类型。它的静态静态类型。类型只能通过语言规则理解和推断。
以下代码演示了什么是“更合适”的版本:
直接运行代码,输出将输出:您好char
因为“ a”是一种char类型数据,所以它自然会寻找一种用于参数类型的char的重载方法。
如果评论了Sayhello(char arg)方法,则输出将成为:Hello Int
目前,发生了自动类型的转换。除了表示字符串外,“ A”还可以代表数字97(字符“ A”的Unicode值是十进制数字97),因此参数类型的重载也适合。
继续注释sayhello(irg)方法,输出将变为:你好长
目前,发生了两种自动类型转换。在将“ A”转换为整数97之后,它将进一步转换为长整数97L,该97L与参数类型长的重载相匹配。代码,但实际上,自动转换可以继续多次发生。根据char-> int-> long> float-> double,它与之匹配。但是它不会匹配字节和短的重载,因为从char到字节或简短的转换是不安全的。
继续注释sayhello(long arg)方法,输出将成为:你好角色
目前,发生了自动包装。“ A”包装为其包装类型Java.lang.Character,因此它匹配了参数类型字符的重载。
继续对Sayhello(角色ARG)方法发表评论,输出将成为:Hello serializizable
Hello serializable出现,因为Java.lang.Serializable是一个界面,出现在Java.lang.character类中。安装自动包装后,发现仍然有一个包装类,但是找到了包装类的接口类型。因此,还有另一种自动转换。char可以转换为int,但是字符永远不会转换为整数。它只能安全地转换为其实现接口或父类。Character还实现了另一个接口Java.lang.com.parable。如果两个参数同时出现,则可以序列化和可比较的重载方法,那么它们目前的优先级相同。编译器无法确定要自动转换的转换类型,并且会提示类型模糊并拒绝编译。该程序必须在调用时显示说明的文字卷的静态类型,例如:可比较的“ a”,以进行编译。
让我们继续注释sayhello(可序列化arg)方法,输出将成为:Hello Object
目前,这是Charg包装后的父班。如果有多个父类,它将开始从继承关系中的自下而上进行搜索。靠近上层的距离越低,优先级越低。即使该方法调用通行证的参数值,该规则仍然适用。
commese sayhello(object arg),输出将变成:你好char ...
已经注释了7种重载方法,只有一种。可以看出,长参数的重载优先级最低。目前,字符“ a”被视为数组元素。
静态方法将在类加载期间解析,并且静态方法显然可以具有重载版本。选择重负载版本的过程也通过静态分配完成。
补充:工人使用想法开发的工人,如果您不知道所使用的方法的特定版本,则可以将鼠标放在呼叫方法上,然后按住CTRL,然后单击该方法以单击该方法,然后将自动跳到特定的呼叫方法办公室。
动态分布和多态性的另一个重要表现:重写替代是密切相关的。请查看动态分布的示例:
执行程序,输出如下:
你好男人你好女人你好女人
虚拟机如何调用?显然,这是无法根据静态类型确定的,因为静态类型也是人类和女人的两个变量,当调用sayhello()方法。明显的。这两个变量的实际类型是不同的。Java虚拟机如何根据实际类型执行版本?使用Javap -v dynamicdispatch.class输出此代码的字节代码以尝试找到答案:
可以看出,DynamicDisPatch $ human.Sayhello在字节代码中执行。Invirtual指令的分析过程大致分为以下步骤:
由于Invirtual指令的第一步是在运行时确定接收器的实际类型,因此两个调用中的InvokeVirtual指令将同一类符号解析为不同的直接引用。该过程由Java语言方法中的方法重写。我们调用根据实际类型的实际类型作为动态分布来执行此方法版本的过程。
该方法的接收者和方法的参数共同涉及该方法的数量。根据基于划分的数量的划分数量,可以将分布分为两种类型:单分和多重分布。根据数量选择了单个派系派系,并且多点派系为根据额外的祖先数量法选择。
在Java语言中,静态分布必须同时考虑实际的类型和方法参数,因此Java语言中的静态分布是多点类型。执行InvokeVirtual指令时,唯一影响虚拟选择的静态指令机器是实际类型,因此Java语言中的动态分布属于单个划分类型。
由于动态分布是一个非常频繁的动作,因此动态分布需要在方法版本选择过程中的方法元素数据中搜索适当的目标方法。虚拟机实现了性能的考虑。它通常不会直接进行如此频繁的搜索。这是一种优化方法。
“稳定优化”方法之一是:在类的方法区域中建立一个虚拟方法表(虚拟方法表,也称为VTable,对应于接口方法表,接口方法表,也称为itable))使用虚拟方法表来参考元数据以找到提高性能的性能。该原理类似于C ++的缺陷函数表。
每个方法的实际入口地址存储在虚拟方法表中。如果未在子类中重写一种方法,则子类的虚拟方法中的地址入口与父类中的方法相同,并且实现父类。虚拟方法表通常在类加载的连接阶段初始初始化。
作者:Liu Java