96SEO 2026-02-23 15:03 26
3、运行时数据区之PC寄存器、Java栈、本地方法栈一、JVM与JAVA体系结构

JAVA虚拟机与JAVA语言并没有必然的联系它只是与特定的二进制文件格式Class文件格式关联Class文件中包含了JAVA虚拟机指令集字节码、Bytecodes和符号表还有一些其他辅助信息。
基于栈的指令集架构基于零地址指令出栈入栈指令集小但指令多。
不依赖于硬件可移植性好基于寄存器的指令集架构以一地址、二地址、三地址指令为主依赖于硬件可移植性差但执行效率更高
由于跨平台性的设计Java的指令都是根据栈来设计的。
不同的平台CPU架构不同所以不能设计为基于寄存器的。
有点事跨平台指令集小编译器容易实现
混合工作模式HotSpotOracle引入了方法区Method
热点代码探测编译器解释器协同工作JRockitOracle专注于服务器端JRockit不包含解释器实现全部靠即时编译器编译后执行J9IBM
类加载器子系统负责从文件系统或者网络中加载Class文件class文件在文件开头有特定的文件标识ClassLoader只负责class文件的加载至于它是否可以运行则有执行引擎Execution
Engine决定;加载的类信息存放于一块称谓方法区Method
Area的内存空间。
除了类的信息外方法区中还会存放运行时常量池信息可能还包括字符串字面量和数字常量这部分常量信息是Class文件中常量池部分的内存映射
1、通过一个类的全限定名获取定义此类的二进制字节流从本地、网络、ZIP、动态代理、加密文件中等2、将这个字节流锁代表的静态存储结构转化为方法区的运行时数据结构3、在内存中生成一个代表这个类的java.lang.Class对象作为方法区这个类的各种数据的访问入口类加载器
ClassLoader派生于抽象类ClassLoader的类加载器都是自定义类加载器extention
ClassLoader是C/C编写的其他类加载器是java编写的他们是包含关系其他自定义类加载器
classloaderJava的核心类库都是使用bootstrap
classloader加载的1.1、引导类加载器Bootstrap
这个类加载使用C/C语言实现嵌套在JVM内部它用来加载java的核心类库用于提供JVM自身需要的类加载extention
classloader,并指定为它们的父类加载器出于安全考虑Bootstrap加载器只加载包名为java、javax、sun等开头的类它不继承自java.lang.ClassLoader没有父加载器
使用java编写由sun.misc.Launcher$ExtClassLoader实现派生于ClassLoader类父类加载器为Bootstrap
ClassLoader从java.ext.dir系统属性所指定的的jre/lib/ext子下也会由扩展类自动加载
java语言编写由sun.misc.Launcher$AppClassLoader实现派生于ClassLoader类父类加载器为Extension
ClassLoader负责加载环境变量classpath或系统属性java.class.path指定路径下的类它是程序中默认的类加载器一般java应用的类都是由它来完成加载通过ClassLoader.getSystemClassLoader()方法来获取该加载器
【java的日常应用开发中类的加载几乎都是由上述3种类加载器配合执行的】
ClassLoader.getSystemClassLoader();System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader18b4aac2//获取扩展类加载器ClassLoader
systemClassLoader.getParent();System.out.println(extentionClassLoader);//sun.misc.Launcher$ExtClassLoader77459877//获取引导类加载器ClassLoader
extentionClassLoader.getParent();System.out.println(bootstrapClassLoader);//null//获取自定义的ClassLoaderTest类的类加载器//说明对于用户自定义的类默认使用
ClassLoaderTest.class.getClassLoader();System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader18b4aac2//获取String的类加载器//Java的核心类库都是使用bootstrap
String.class.getClassLoader();System.out.println(stringClassLoader);//null}
}自定义自己的类加载器可以继承ClassLoader或者URLClassLoader类
JAVA虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类的时候才会将他的class文件加载到内存中生成class对象。
而且加载某个类的class文件时java虚拟机采用的是双亲委派模式,即把请求交由父类处理也是一种任务委派模式##工作原理
1、如果一个类加载器收到了类加载的请求它并不会自己先去加载而是把这个请求委托给父类的加载器去执行;2、如果父类加载器还存在其父类加载器则进一步向上委托依次递归请求最终将到达顶层的启动类加载器;3、如果父类加载器可以完成累的加载任务就成功返回若父类加载器无法完成此家在任务子加载器才会
【只有这样如果我们自定义的java.lang.String类才不会被执行也就不会覆盖系统自带的String了】
2、保护了程序安全防止核心API被恶意篡改1.5、沙箱安全机制
如上我们自定义java.lang.String类但是在加载自定义String类的时候会率先使用引导类加载器加载。
从而引导类加载器在加载的过程中会先加载JDK自带的文件rt.jar包下的java.lang.String.class。
这样可以保证对java核心源代码的保护这就是沙箱安全机制。
1.6、其他
1、在JVM中表示两个class对象是否为同一个类有两个条件a、类的完整类名必须一致包括包名b、加载这个类的ClassLoader类加载器必须相同2、JVM必须知道一个类型是由启动类加载器加载的还是用户自定义类加载器加载的。
如果一个类型是由用户自定义类加载器加载的那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。
当解析一个类型到另一个类型的引用的时候JVM需要保证这两个类型的类加载器是相同的。
3、JAVA程序对类的使用分为主动使用
d、反射如Class.forName(com.lee.MyTest)
g、JDK7开始提供的动态语言支持除了以上情况其他使用Java类的方式都被看做是队类的被动使用。
主动使用会导致类的Initailization初始化(即下面的初始化)被动使用不会2、链接Liking
##验证verify文件格式验证、元数据验证、字节码验证、符号引用验证保证类加载的正确性[.class文件一般以
BE开头]##准备Prepare为类变量分配内存并设置该类变量的默认初始值即零值。
(private
0。
在initial环节才被赋值为1)这里不包含用final修饰的static因为final在编译的时候就会分配了准备阶段会显示初始化。
(private
1)这里不会为实例变量分配初始化类变量会分配在方法区中而实例变量是会随着对象一起分配到JAVA堆中。
##解析Resolve将常量池内的符号引用转换为直接引用的过程
初始化阶段就是执行类构造器方法clinit()的过程【clinit()不同于类的构造器init()】此方法不需要定义是javac编译器自动收集类中的所有
合并而来构造方法中指令按语句在源文件中出现的顺序执行若该类具有父类JVM会保证子类clinit()执行前父类clinit()已经执行完毕虚拟机必须保证一个类的clinit()方法在多线程下被同步加锁(一个类只能被执行一次clinit())##解释
类变量与实例变量的区别1类变量属于类可以共享属于公共属性实例变量属于某个对象个体;2加上static
20;//System.out.println(number);//这里可以赋值但不可以调用-》非法前向引用}private
number10;//在linking的preparenumber0
args){System.out.println(number);//number的值为10}}加载子类()执行前必须保证父类()的执行
BA2System.out.println(Son.B);//2}
Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区其中有些会随着虚拟机的启动二创建随着虚拟机的退出而销毁。
另外一些则是与线程一一对应这些与线程对应的数据区域会随着线程的开始和结束而创建和销毁。
##上图
stack是各个线程独享的1、每个线程独立使用程序计数器、栈、本地栈。
2、线程间共享堆、堆外内存永久代或元空间、代码缓存Method
(java.lang.Runtime)每个JVM只有一个Runtime实例。
即为运行时环境(Run
线程(Thread)是一个程序里的运行单元。
JVM允许一个应用程序有多个线程并行执行。
在Hotspot
当一个Java线程准备好执行以后此时操作系统的本地线程也同时创建。
Java线程执行终止后本地线程也会被回收操作系统负责所有线程的安排调度到任何一个可用的CPU上一旦本地线程初始化成功他就会调用Java线程的run()方法##Hotspot
JVM中的PC寄存器是对屋里PC寄存器的一种抽象模拟也可以成为程序的钩子程序的行号指示器##作用
PC寄存器用来存储指向下一条指令的地址也就是即将要执行的指令代码。
·它是一块很小的内存空间也是运行速度最快的存储区域·JVM规范中每个线程都有它自己的PC寄存器是线程私有的生命周期与线程保持一致·任何时间一个线程都只有一个方法在执行也就是所谓的当前方法。
PC寄存器会存储当前线程
正在执行的Java方法的JVM指令地址或者如果是在执行native方法则是未指定值[undefined]下面的
方法当前栈帧就是程序中的当前Java方法·PC寄存器是程序控制流的指示器分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
·字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
·它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域javap
一、##使用PC寄存器存储字节码指令地址有什么用##为什么使用PC寄存器记录当前线程的执行地址因为CPU需要不停的快速切换各个线程切换回来后就得以知道接着从哪开始继续执行了。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
二、##PC寄存器为什么被设定为线程私有的所谓的多线程在特定的时间段内只会执行其中的某一个线程CPU会不停地做快速切换而已。
这样必然导致线程的经常中断或恢复。
为了能够准确地记录各个线程正在执行的当前字节码指令地址最好的办法自然是为每个线程都
分配一个PC寄存器这样一来各个线程之间变可以进行独立计算从而不会出现相互干扰的情况。
拓展CPU时间片
CPU时间片即CPU分配给各个程序的时间每个线程被分配一个时间段称作它为时间片。
宏观上我们可以同时打开多个应用程序每个程序并行不悖同时运行。
微观上由于只有一个CPU一次只能处理程序要求的一部分如何处理公平一种方法就是引入时间片每个程序轮流执行并行
没有GC垃圾回收问题但会存在OOMOutOfMemoryError的问题
一、##出现背景前面在JVM架构模型中讲到指令流基本上基于两种指令集架构基于栈的指令集架构、基于寄存器的指令集架构。
由于跨平台性的设计Java的指令都是根据栈来设计的。
不同平台的CPU架构不同所以不能使用基于寄存器的。
优点是跨平台指令集小编译器容易实现。
缺点是性能下降实现同样的功能需要更多的指令指令集小但指令多二、##内存中栈与堆的区别栈是运行时的单位堆是存储的单位即栈解决程序的运行问题即程序如何执行或者说如何处理数据。
堆解决的是数据存储的问题即数据怎么放放哪儿。
三、##Java虚拟机栈是什么Java虚拟机栈早期也叫Java栈。
每个线程在创建时都会创建一个虚拟机栈其内部保存一个个的栈帧Stack
一个个方法的调用对应着一个个栈帧的入栈和出栈操作。
Java栈是线程私有的。
其生命周期和线程一致。
主管Java程序的运行它保存
四、##栈的有点栈是一种快速有效的分配存储方式访问速度仅次于程序计数器。
JVM直接对Java栈的操作只有两个
工作对于栈来说不存在GC垃圾回收问题五、##栈中可能出现的异常Java虚拟机规范允许Java栈的大小是
·如果采用固定大小的Java虚拟机栈那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。
如果线程请求分配的栈内容超过Java虚拟机允许的最大容量Java虚拟机将会抛出一个StackOverflowError
异常【递归的时候可能会出现】·如果Java虚拟机栈可以动态扩展并且在尝试扩展的时候无法申请到足够的内存或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈那么Java虚拟机将会抛出一个OutOfMemoryError异常六、##设置栈的大小
一、##栈的存储单位每个线程都有自己的栈栈中的数据都是以栈帧Stack
Frame的格式存在。
在这个线程上正在执行的每一个方法都各自对应一个栈帧Stack
Frame。
栈帧是一个内存区块是一个数据集维系着方法执行过程中的各种数据信息。
二、##栈运行原理JVM直接对Java栈的操作只有两个就是对栈帧的“压栈”和“出栈”遵循“先进后出”、“后进先出”的原则。
在一条活动线程中一个时间点上只会有一个活动的栈帧即只有当前正在执行的方法的栈帧
Class.执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
如果在该方法中调用了其他方法对应的新的栈帧就会被创建出来放在栈的顶端称谓新的当前栈帧。
不同线程中锁包含的栈帧是不允许存在相互引用的即不可能在一个栈帧中引用另外一个线程的栈帧。
如果当前方法调用了其他方法方法返回之际当前栈帧会传回此方法的执行结果给前一个栈
帧接着虚拟机会丢弃当前栈帧使得前一个栈帧重新成为当前栈帧。
Java方法有两种返回函数的方式一种是正常的函数返回使用return指令另一种是抛异常。
本地变量表·定义为一个数字数组主要用于存储方法参数和定义在方法体内的局部变量
这些数据类型包括8种基本数据类型、对象引用refrence以及returnAddress类型·由于局部变量表是建立在线程的栈上是线程的私有数据因此不存在数据的安全问题·局部变量表所需的容量大小是在编译器确定下来的并保存在方法的Code属性的maximum
java代码行号对应的关系】·方法嵌套调用的次数由栈的大小决定如递归。
·局部变量表中的变量只在当前方法调用中有效。
方法执行时虚拟机通过使用局部变量表完成参数值
到参数变量列表的传递过程。
当方法调用结束后随着方法栈帧的销毁局部变量表也会随之销毁。
【局部变量表是主要影响栈帧大小的】##上图中LocalVariableTable中Slot的理解·参数值的存放总是在局部变量数组的index0开始到数组长度的-1的索引结束。
·局部变量表最基本的存储单元是Slot
变量槽·局部变量表中存放编译期可知的8中基本数据类型引用类型returnAddress类型的变量·局部变量表里32位以内的类型只占用一个slot包括returnAddress类型
64位的类型long和double占用两个slotbyte\short\char在存储前被转换为intboolean也被转换为int
0表示false非0表示truelong和double则占据两个slotbyte\short\char\float\int
long\double占2个slot·JVM回味局部变量表中每个slot都分配一个访问索引通过这个索引即可成功访问到局部变量表中指定的局部变量值。
·当一个实例方法被调用的时候他的方法参数和方法体内部定义的局部变量将会按照顺序被赋值到局部变量表中的每一个slot上。
·如果需要访问局部变量表中的一个64bit的局部变量值时只需要使用前一个索引即可。
long和double·如果当前栈帧是由构造方法或者实例方法创建的那么该对象引用this将会存放在index为0的slot处其余的参数按照参数表顺序继续排列。
因此被static修饰的方法中是不能够使用this的·栈帧中的局部变量表中的槽位slot是可以重复利用的如果一个局部变量过了其作用域那么在其作用域之后
声明的新的局部变量就很有可能会服用过期局部变量的槽位从而达到节省资源的目的。
其中b的作用域在其{}内变量c是使用之前已经销毁的变量b占据的slot的位置。
变量
②引用数据类型按照在类中的声明位置分①成员变量类变量(或叫静态变量被static修饰)实例变量②局部变量
##类变量linking的prepare阶段给类变量默认赋值--initial阶段给类变量显式赋值即静态代码块赋值##实例变量随着对象的创建会在堆空间中分配实例变量空间并进行默认赋值
##局部变量在使用前必须要进行显式赋值否则编译不通过##补充说明·在栈帧中与性能调优关系最为模切的部分就是
在方法执行时虚拟机使用局部变量表完成方法的传递。
·局部变量表中的变量也是重要的垃圾回收根节点只要被局部变量表中引用数据类型直接或间接引用的对象都不会被回收。
局部变量表中
引用数据类型引用数据类型引用的是堆空间中的对象所以影响了堆空间的GC垃圾回收。
如果局部变量表中的引用数据类型不存在了那么堆空间中的数据可能就会被回收2、操作数栈Operand
##操作数栈·后进先出也被称之为表达式栈·操作数栈主要是用于保存计算过程中的中间结果同时作为计算过程中变量临时的存储空间·操作数栈在方法执行的过程中根据字节码指令往栈中写入数据或提取数据即入栈PUSH/出栈POP
有些字节码指令是将值PUSH进操作栈有些字节码指令是将操作数POP出栈使用后再把结果PUSH进栈
比如执行赋值、交换、求和等操作·操作数栈主要是用于保存计算过程中的中间结果同时作为计算过程中变量临时的存储空间。
·操作数栈就是JVM执行引擎的一个工作区当一个方法开始执行的时候一个新的栈帧也会随
之被创建出来这个方法的操作数栈是空的。
·每一个操作数栈都会拥有一个明确的栈深度用于存储数值其所需的最大深度在编译期就定义
好了。
保存在方法的code属性中为max_stack的值。
·栈中的任何一个元素都是可以任意的Java数据类型32bit的类型占用一个栈单位深度64bit的类型占用两个栈单位深度·操作数栈并非采用访问索引的方式来进行数据访问的而是只能通过标准的入栈和川站操作来完成一次数据访问·如果被调用的方法带返回值的话其返回值将会被压入当前栈帧的操作数栈中。
并更新PC寄存器中下一条需要执行的字节码指令。
当当前方法用到上一条方法的返回值时会从操作数栈中load·操作数栈中元素的数据类型必须与字节码指令的序列严格匹配这由编译器在编译器期间进行
验证同时在类家在过程中的类检验阶段的数据流分析阶段要再次验证。
·另外我们说Java虚拟机的解释引擎就是基于栈的执行引擎其中的栈值得就是操作数栈。
·由于操作数是存储在内存中的因此频繁第执行内存读/写操作必然会影响执行速度。
为了解决这个问题HotSpot
JVM的设计者提出了栈顶缓存技术。
即将栈顶元素全部缓存在物理CPU的寄存器中以此降低对内存的读/写次数提升执行殷勤的执行效率。
3、动态链接Dynamic
中·每一个栈帧内部都包含一个执行运行时常量池中该栈帧所属方法的引用。
包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接invokedynamic指令·在Java源文件被编译到字节码文件中时所有的变量和方法引用都作为符号引用保存在class文件的常量池里。
比如描述一个方法调用了另外的其他方法时就是通过常量池中指向方法的符号引用来表示的·动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用·符号引用以一组符号来描述所引用的目标
符号可以是任何形式的字面量只要使用时能够无歧义的定位到目标即可
main(java.lang.String[]);....Code:stack2,
methods1:()I·直接引用直接引用可以是直接指向目标的指针、相对偏移量、一个能间接定位到目标的句柄
com/lee/jvm/rundataarea/LocalVariablesTest.methods1:()I##JVM指令拓展
invokedynamic:动态解析出需要调用的方法然后执行4、方法返回地址
·方法返回地址存放调用该方法的PC寄存器的值·一个方法的结束有两种方式
出现未处理异常非正常退出·无论哪种方式退出在方法退出后都返回到该方法被调用的位置。
方法正常退出是调用者的PC计数器的值作为返回地址即调用该方法的指令的下一条指令的地址。
而通过异常退出的返回地址是要通过异常表来确定栈帧中一般不会保存这部分信息。
1、执行引擎遇到任意一个方法返回的字节码指令return会有返回值传递给上层的方法调用这简称正常完成出口
一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定。
在字节码指令中返回指令包含为ireturn(当返回值时boolean\byte\chat\short\int类型时使用)、lreturn(long)、freturn(float)、dreturn(double)以及areturn
另外还有一个return指令提供声明为void的方法、实力初始化方法、类和接口的初始化方法使用。
2、在方法执行的过程中遇到异常Exception并且这个异常没有在方法内处理也就是只要在本方法的异常表中没有搜索到匹配的异常处理器就会导致方法退出。
简称异常完成出口方法执行过程中抛出异常时的异常处理存储在一个异常处理表方便在发生异常的时候找到处理异常的代码。
##本质上方法的退出就是当前栈帧出栈的过程。
此时需要恢复上层方法的局部变量表、操作数栈、将返回值亚茹调用者栈帧的操作数栈、设置PC寄存器值等让调用者方法继续执行下去。
正常完成出口和异常完成出口的区别在于通过异常完成的出口退出的不会给他的上层调用者产生任何的返回值。
5、一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。
如对程序调试提供支持的信息6、关于Java栈的思考
一、##举例栈溢出的情况如果设置了栈的大小栈空间固定虚拟机栈增加栈帧溢出会出现StackOverflowError
如果没有设置了栈的大小虚拟机栈增加栈帧内存溢出或者增加新的线程时会出现OutOfMemeryError二、##调整栈的大小就能保证不出现溢出吗不能保证只能保证栈溢出的阈值增大而已。
三、##分配的栈内存越大越好吗不是内存空间固定某个栈空间变大了挤占了线程数或者其他结构的内存空间四、##垃圾回收是否会涉及到虚拟机栈不会栈的生命周期是随
的创建和销毁的所以不存在GC垃圾回收如果溢出直接报错五、##方法中定义的局部变量是否线程安全不一定举例说明
上面线程安全的当多个线程同时执行此方法时每个线程都会产生一个自己的变量x每个线程之间互不干扰不会对其他线程的变量x有影响。
public
{sb.append(1);sb.append(2);System.out.println(sb.toString());
上面线程不安全在这里sb对象是作为参数传入的这意味着它并不是线程私有的多个线程都可以同时访问它。
所以不是线程安全的。
StringBuilder();sb.append(1);sb.append(2);return
上面线程不安全虽然sb对象是在方法内生成的对象。
但是sb作为一个返回变量返回其他线程可以去拿取它对它进行并发的操作。
StringBuilder();sb.append(1);sb.append(2);return
上面线程安全sb消亡了sb.toString()相当于我们又new
了一个string,这个new出来的string可能不安全但是方法内的sb是安全的。
4、本地方法栈
Method就是一个Java调用一个本地非Java代码的接口比如是C或者C。
即一个Java方法其方法内部是由非Java语言实现的
start0();标识符native可以与所有其他的java标识符连用但是abstract除外本地方法栈
·Java虚拟机栈用于管理Java方法的调用。
而本地房发展用于管理本地方法的调用。
·本地方法栈也是线程私有的·允许被是线程固定或者是可动态扩展的内存大小在内存溢出方面和虚拟机栈是相同的·本地方法时C语言实现的·具体做法是
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback