JVM(三、运行时数据区)

三、运行时数据区

0.概述:

  不同的JVM对于内存的划分方式和管理机制存在着部分差异

  运行时数据区包括堆,方法区,程序计数器,本地方法栈,虚拟机栈

  堆和方法区是线程共享的,随着虚拟机的创建而创建,随着虚拟机销毁而销毁

  程序计数器,虚拟机栈,本地方法栈是线程私有的,每个线程都有一份

关于线程:

  JVM是支持多线程的

  在Hotspot JVM中,每个线程都与操作系统中的本地线程直接映射:

    当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。一旦本地线程初始化成功,他就会调用Java线程中的run()方法。Java线程终止后,本地线程也会回收

1、程序计数器(PC寄存器)

  存储指向下一条指令的地址,由执行引擎读取下一条指令。控制分支,循环等程序运行的流程。

  占用内存非常小,运行速度快,线程私有

  程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;如果是在执行native方法,则是未指定值(undefined)

  不会出现内存溢出。没有垃圾回收。

2、虚拟机栈

0).概述

  Java的指令都是根据栈来设计的,不同平台CPU架构不同,所以不能基于寄存器

  优点是跨平台,指令集小,编译容易实现,缺点是性能下降,实现同样的功能指令集多

  栈是运行时的单位,而堆是存储的单位

  即栈解决程序如何执行,如何处理数据。堆解决了数据的存储,数据怎么放,放在哪儿

  每个线程都有一个虚拟机栈,里面保存了一个个的栈帧,一个栈帧对应着java中的一个方法。

  作用:主管java程序的运行,保存方法的局部变量(8中基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回

栈的特点(优点):

  效率较高,访问速度仅次于程序计数器

  JVM对栈的操作只有两种:方法执行,入栈,执行结束后出栈

  不存在垃圾回收问题(GC、OOM)

  面试题:开发中遇到的异常有哪些?

  答:Java栈的大小是动态的或者是固定不变的,如果采用固定不变的,当一个线程请求的栈容量超过虚拟机栈允许的最大容量,会报栈溢出异常StackOverflowError

  如果是动态扩展的,当虚拟机栈扩展的时候没有申请到足够的内存空间,或者是没有足够的内存空间去创建虚拟机栈的时候,就会抛出内存溢出异常(OutOfMemoryError

  设置栈内存大小:使用参数-Xss设置。例-Xss256k

栈的存储单位:

栈中存储什么?

  栈中存储着许多的栈帧,线程上的每个方法都各自对应一个栈帧

  对栈的操作只有入栈和出栈,规则是先进后出

  一个线程在一个时间点上只有一个活动的栈帧,即当前执行的方法的栈帧,称为当前栈帧,当前栈帧对应的方法叫当前方法,当前方法所在的类叫做当前类

  执行引擎运行的所有字节码指令只针对当前栈帧

  当程序正常结束return或者抛出未处理的异常都会导致栈帧被弹出

  栈帧的内部结构:

    局部变量表

    操作数栈(表达式栈)

    动态链接(指向运行时常量池的方法引用)

    方法返回地址(方法正常退出或者异常退出的定义)

    一些附加信息

1).局部变量表(local variables)

  定义一个数组,主要用于存储方法参数和定义在方法体内的局部变量包括基本数据类型、对象引用、returnAddress类型

  线程私有,不存在安全问题

  局部变量表所需的容量大小是在编译期间确定下来的,保存在方法的Code属性的maximum local variables数据项中。在运行期间不会改变.

  一个方法中的局部变量越多,局部变量表越大,栈帧就越大,虚拟机栈固定大小的情况下,可存放的栈帧就越少,方法调用的嵌套次数就越少。

  局部变量表中的变量只在当前方法调用中有效。方法调用结束后,栈帧销毁,局部变量随之销毁。

 

 

 

   slot(变量槽):局部变量表的最基本单元在局部变量表中,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot

byte、short、char在存储前被转换为int,boolean也被转换为int,0-false 1-true

当一个实例方法被调用时,他的参数和局部变量将会按照顺序被复制到局部变量表中的每一个slot上

如果要访问局部变量表中一个64bit的局部变量表时只需使用前一个索引即可(例:占用了4号坑和5号坑 那么索引就是4号坑)

构造方法和实例方法的index为0处的slot存放的都是this所以我们能够直接试用,静态方法的局部变量表中没有this,所以不能使用this.

2)操作数栈(Operand Stack) 又名表达式栈

使用数组来实现,不是采用访问索引的方式来进行数据访问的 ,主要用于保存计算过程的中间结果。

进入操作数栈的时候,

-1~5 采用 iconst 指令

-128~127 采用 bipush 指令

-32768~32767 采用 sipush 指令

-2147473648~2147483647 采用 ldc指令

ps:byte,short,char,int进入操作数栈之后都会转换成int

字节码的角度分析i++和++i的区别

i++是先从局部变量表中取出之前的值,然后再自增,++i是先自增,再从局部变量表中取出这个值

栈顶缓存技术:将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。

3).动态链接(指向运行时常量池的方法引用)

桢数据区:包含动态链接,方法返回地址,一些附加信息

类加载之后所有的类或者方法,变量等信息都会作为符号被放在运行时常量池中

动态链接的作用就是把这些符号引用转换为调用方法的直接引用

为什么需要运行时常量池呢?

因为可以节省空间,我们可以把类加载后的东西都放在常量池中,然后需要的时候直接使用一个指针指向它。

方法的调用:

静态链接和动态链接

如果需要调用的方法在编译期间可知,则为静态链接。为早期绑定。(例如在构造方法中使用super())

如果无法被确定下来,就是动态链接。是晚期绑定。(例如在方法中传一个接口或者父类,在方法内部调用其方法)

链接--->符号引用转换为直接引用

虚方法和非虚方法:

非虚方法:如果编译期间就确定了具体的调用版本,并且在运行时是不变的,这种方法就是非虚方法(类似于上面的静态链接和早期绑定的概念)

静态方法,私有方法,final修饰的方法,实例构造器,父类方法都是非虚方法。其他方法称为虚方法

调用方法的指令:

普通调用指令:

invokestatic:调用静态方法,解析阶段确定唯一方法版本

invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本

invokevirtual:调用所有虚方法

invokeinterface:调用接口方法

动态调用指令:

invokedynamic:动态解析出需要调用的方法,然后执行.

其中invokestatic指令和invokespecial指令调用的方法为非虚方法,其余的(final修饰的除外)为虚方法。

动态类型语言和静态类型语言

区别:对类型的检查是在编译期还是在运行期

例:java中--->String nam = "zhangsan";

js中--->var name = "zhangsan";

立即推--->java是静态类型语言

jdk1.8引入的ambda表达式是采用的动态类型的

4).方法返回地址

存储调用该方法的pc寄存器(程序计数器)的值(下一条执行指令)

主要是用于方法的正常退出时。方法的异常退出的返回地址试要通过异常表来确定的。

5).一些附加信息

存储一些与虚拟机的实现相关的附加信息(不重要)

栈相关面试题

1.举例栈溢出的情况(StackOverflowError)

如果虚拟机栈是固定大小的话,如果存储的内容超过这个栈的大小,就会出现栈溢出的情况,可以通过-Xss设置虚拟机栈的大小

如果虚拟机栈的大小是自动扩容的话,如果虚拟机栈分配不到足够大的内存,就会出现内存溢出(OOM)的情况。

2.调整栈的大小,就能保证不出现溢出吗?

不能。如果写一个死循环,那多大的大小就不够用。

3.分配的栈内存越大越好吗?

不是,因为每一个线程都会有一个虚拟机栈,虚拟机栈分配的空间过大了,就可能会线程数量减少而且其他资源能够分配的空间就减少了。

4.虚拟机栈GC吗

不存在,因为在栈中执行完相关操作之后就直接从栈中弹出了,所以不存在GC的情况。

5.方法中定义的局部变量是否线程安全?

具体问题具体分析。单线程下线程安全,多线程下线程不安全。

4.本地方法栈

本地方法接口:

native关键字修饰的方法,不能用abstarct修饰

为什么要使用 Native Method?

1.与Java环境外交互

java诞生的时候C/C++非常厉害,所以尽量的和他们融合

2.与操作系统交互

因为操作系统的底层基本上都是使用C实现的

3.Sun's Java

Sun的解释器是用C实现的

本地方法栈:

参考虚拟机栈,本地方法栈是用于管理本地方法的

当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的市街。它和虚拟机拥有同样的权限:

本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区

它甚至可以直接使用本地处理器中的寄存器

直接从本地内存的对中分配任意数量的内存

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVM运行时数据是指在JVM进程运行过程中,用于存储数据的各个域,包括以下几个部分: 1. 程序计数器(Program Counter Register):线程私有的域,用于记录当前线程执行的字节码行号,以便线程在执行过程中能够从正确的位置继续执行。 2. Java虚拟机栈(JVM Stack):线程私有的域,用于存储方法的栈帧(Stack Frame),包括局部变量表(Local Variable Table)、操作数栈(Operand Stack)、动态链接(Dynamic Linking)、方法出口(Return Address)等信息。当一个方法被调用时,JVM会在栈中为该方法分配一个栈帧,方法执行完毕后,栈帧被出栈,JVM继续执行上一个栈帧中的方法。 3. 本地方法栈(Native Method Stack):与Java虚拟机栈类似,用于存储本地方法的栈帧,本地方法是指使用本地语言(如C语言)编写的方法,本地方法栈与Java虚拟机栈的别在于,本地方法栈用于执行本地方法,而Java虚拟机栈用于执行Java方法。 4. 堆(Heap):所有线程共享的域,用于存储对象实例。在JVM启动时,会分配一定大小的堆空间,当堆空间不足时,JVM会自动进行垃圾回收以释放无用的对象,从而维持堆空间的稳定。 5. 方法(Method Area):所有线程共享的域,用于存储类的元数据信息,包括类名称、访问修饰符、常量池、字段描述符、方法描述符等信息。在JDK8及以前的版本中,方法也包括永久代(Permanent Generation),用于存储一些与类加载、字符串常量等相关的信息。在JDK8及以后的版本中,永久代被移除,取而代之的是Metaspace(元空间),用于存储类的元数据信息。 6. 运行时常量池(Runtime Constant Pool):方法的一部分,用于存储编译期生成的各种字面量(Literal)、符号引用(Symbolic Reference)等信息。在方法调用时,JVM会将运行时常量池中的字面量、符号引用等信息加载到操作数栈中,供指令使用。 除了以上几个域外,还有一些其他的域,例如直接内存(Direct Memory),用于存储通过NIO(New IO)库分配的堆外内存。JVM运行时数据域对于Java程序的执行起到了关键的作用,了解这些域有助于我们更好地理解Java程序的执行过程和内存管理机制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值