目录
2.1 程序计数器(Program Counter Register)
2.1.2 使用程序计数器存储字节码指令地址有什么用?为什么使用程序计数器记录当前线程的执行地址呢?
2.2 Java虚拟机栈(Java Virtual Machine Stacks)
2.2.10 什么情况下会出现栈溢出(StackOverflowError)?
2.3 本地方法栈(Native Method Stack)
先用个demo解释一下
1 DEMO【文献3】
package com.demo.a6_transfervalue;
public class Person {
String personName;
int age;
public String getPersonName() {
return personName;
}
public void setPersonName(String personName) {
this.personName = personName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"personName='" + personName + '\'' +
", age=" + age +
'}';
}
}
package com.demo.a6_transfervalue;
public class TestTransferValue {
public void changeValue1(int age) {
age = 30;
}
public void changeValue2(Person person) {
person.setPersonName("XXX");
}
public void changeValue3(String str) {
str = "SSS";
}
public static void main(String[] args) {
TestTransferValue testValue = new TestTransferValue();
int age = 20;
testValue.changeValue1(age);
System.out.println("age = " + age);
Person person = new Person();
person.setPersonName("aaa");
testValue.changeValue2(person);
System.out.println("personName = " + person.getPersonName());
String str = "abc";
testValue.changeValue3(str);
System.out.println("String = " + str);
}
}
输出
age = 20
personName = XXX
String = abc
栈管运行,堆管存储
栈:先进后出原则
1.age = 20, 是因为age传给changeValue1时, changeValue1拿到的是拷贝的数据,打印用的是main方法的age, 故结果为20
2.personName = XXX 是因为person中记录的personName是引用地址(类似指针),setPersonName时改变的是引用地址指向的值,main方法和changeValue2方法指向的都是同一个地址,故,main输出时,还是根据该引用地址取值,此时值已被改变。结果为XXX
3.因为String类型的特殊性,当string str = "abc"时, 在字符串常量池中查找是否存在abc,没有则创建 abc, str引用地址指向abc。当str = ”SSS"时, 字符串常量中又创建了SSS,changeValue3中的str引用地址调整,变更为SSS的引用地址,此时main函数输出的str引用地址未改变,还是指向abc的,所以输出abc
2 JVM运行时数据区【内容来源于文献1】
JVM的运行时数据区:
2.1 程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间(内存小到几乎可以忽略不计,也是运行速度最快的存储区域),它可以看作是当前线程所执行的字节码的行号指示器。用来存储下一条指令的地址,也即将要执行的指令代码。有执行引擎读取下一条指令。
- java中最小的执行单位是线程,因为虚拟机的是多线程的,每个线程是抢夺cpu时间片,程序计数器就是存储这些指令去做什么、(即程序控制流的指示器),分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。
- 每个线程都有属于自己的程序计数器,线程私有,互不影响,独立存储,生命周期与线程生命周期保持一致。
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址。如果是在执行native方法,则是未指定值(undefined)。
- 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
- 它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2.1.1 程序计数器的作用位置
2.1.2 使用程序计数器存储字节码指令地址有什么用?为什么使用程序计数器记录当前线程的执行地址呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪儿开始继续执行。
JVM的字节码解释器就需要通过改变程序计数器的值来明确下一条应该执行什么样的字节码指令。
2.1.3 程序计数器为什么被设定为线程私有的
我们都知道所谓的多线程在一个特定的时间段内只会执行其中某个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?
为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每个线程都分配应该程序计数器,这样一来各个线程之间便可以独立计算,从而不互相干扰。
2.2 Java虚拟机栈(Java Virtual Machine Stacks)
2.2.1 Java虚拟机栈是什么?
Java虚拟机栈,早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应这一次方法的调用。
Java虚拟机栈是线程私有的。且生命周期和线程一致。
描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个线帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应这一个线帧在虚拟机栈中入栈到出栈的过程。
2.2.2 作用
主管Java程序的运行,它保存方法的局部变量(8中基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回。
2.2.3 栈中存储什么?
每个线程都有自己的栈,栈中的数据都以栈帧为单位存储,线程上正在执行的每个方法都有各自对应一个栈帧。
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
2.2.4 栈帧的内部结构
每个栈帧中存储着:
2.2.4.1 局部变量表(Local Variables)
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量则存的是只想对象的引用。
2.2.4.2 操作数栈(Operand Stack)(或表达式栈)
栈最典型的一个应用就是用来对表达式求值。在一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。
2.2.4.3 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
2.2.4.4 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
当一个方法执行完毕后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
2.2.4.5 一些附加信息
2.2.5 栈的特点
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
- JVM直接对Java栈的操作只有两个:调用方法,进栈。执行结束后出栈。
- 对于栈来说不存在垃圾回收问题。
- 基于栈的指令设计优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样功能需要更多的指令集
2.2.6 栈的运行原理
JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
在一条活动的线程中,一个时间点上,只会有一个活动栈。即只有当前执行的方法的栈帧(栈顶)是有效地,这个栈帧被称为当前栈,与当前栈帧对应的方法称为当前方法,定义这个方法的类称为当前类。
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
如果在该方法中调用了其他方法,对应的新的栈帧就会被创建出来,放在栈的顶端,成为新的当前栈帧。
不同线程中所包含的栈帧(方法)是不允许存在相互引用的,即不可能在一个栈中引用另一个线程的栈帧(方法)。
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
Java方法有两种返回的方式,一种是正常的函数返回,使用return指令,另一种是抛出异常,不管哪种方式,都会导致栈帧被弹出。
2.2.7 栈和堆区别
栈是运行时的单位,而堆是存储的单元。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放,放在哪儿。
2.2.8 栈中出现的异常
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
2.2.9 两个栈帧之间的数据共享
在概念模型中,两个栈帧作为虚拟机栈的元素,是完全互相独立的,但在大多虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无须进行额外的参数赋值传递,重叠的过程如图所示。
Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。
2.2.10 什么情况下会出现栈溢出(StackOverflowError)?
栈溢出就是方法执行时创建的栈帧超出了栈的深度。那么最有可能的就是方法递归调用产生这种结果。
2.2.11 通过调整栈大小,就能保证不出现溢出吗?
不能。
2.2.12 分配的栈内存越大越好吗?
并不是的,只能延缓这种现象的出现,可能会影响其他内存空间。
2.2.13 垃圾回收机制是否会涉及到虚拟机栈?
不会。
2.3 本地方法栈(Native Method Stack)
- 本地方法栈也是线程私有的。
- 与虚拟机的作用是相似的,只不过虚拟机栈是服务Java方法的,而本地方法栈是为虚拟机调用Native方法服务的(用于管理本地方法的调用)。
- 与虚拟机栈相同的是栈的深度是固定的,允许被实现成固定或者是可动态扩展的内存大小。内存溢出方面也是相同的。
- 当线程请求分配的栈容量大于本地方法栈允许的最大容量就会抛出StackOverFlowError异常
- 虚拟机栈动态扩展,如果扩展到无法申请到足够的内存就会抛出outofMemoryError异常。
- 本地方法是用C语言写的。
- 它的具体做法是在Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。
2.4 Java堆(Java Heap)
2.4.1 概念
是Java虚拟机中内存最大的一块,是被所有线程共享的,在虚拟机启动时候创建,Java堆唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
- 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
- Java堆区在JVM启动时的时候即被创建,其空间大小也确定了,是JVM管理的最大一块内存空间。
- 堆内存的大小是可以调节的。例如:-Xms:10m(堆起始大小) -Xmx:30m(堆最大内存大小)
- 一般情况可以将起始值和最大值设置为一致,这样会减少垃圾回收之后堆内存重新分配大小的次数,提高效率。
- 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但逻辑上它应该被视为连续的。
- 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区。
- 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例都应当在运行时配对在堆上。
- 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
- 堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
2.4.2 堆内存区域划分
Java8及之后堆内存分为:新生区(新生代)+老年区(老年代)
新生区分为Eden(伊甸园)区和Survivor(幸存者)区
2.5 方法区(Method Area)
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
内存区域是很重要的系统资源,是硬盘和CPU的中间桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异,我们现在以使用最为流行的HotSpot虚拟机为例讲解。
Java虚拟机定义了若干中程序运行期间会使用到的运行数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的。这些与线程对应的区域会随着线程开始和结束而创建销毁。
如图:红色的为多个线程共享,灰色的为单个线程私有的,即
线程间共享:堆,对外内存。
每个线程:独立包括程序计数器,栈,本地方法栈。
2.5.1 方法区的主要存储对象【文献2】
1、类型信息:
- 类的完整名称
- 类的直接父类的完整名称
- 类的直接实现接口的有序列表
- 类型标志(类类型还是接口类型)
- 类的修饰符(public private defautl abstract final static)
2、类型的常量池
- 存放该类型所用到的常量的有序集合,包括直接常量(字符串、整数、浮点数)和对其他类型、字段、方法的符号引用。
3、字段信息(该类声明的所有字段)
- 字段修饰符(public、peotect、private、default)
- 字段的类型
- 字段名称
4、方法信息
- 方法信息中包含类的所有方法。
- 方法修饰符
- 方法返回类型
- 方法名
- 方法参数个数、类型、顺序等
- 方法字节码
- 操作数栈和该方法在栈帧中的局部变量区大小
- 异常表
5、类变量(静态变量)
6、指向类加载器的引用
7、指向Class实例的引用
8、方法表
9、运行时常量池(Runtime Constant Pool)
3 数据存储图示(再次回顾,加深印象)
4 参考文献
【1】JVM运行时数据区(详解+面试)_qdzjo的博客-CSDN博客_jvm运行时数据区
【2】java方法区是线程共享么_JVM之Java运行时数据区(线程共享区)_明王道日语的博客-CSDN博客
【3】 Java面试_高频重点面试题 (第一、二、三季)_ 面试 第1、2、3季_柴林燕_周阳_哔哩哔哩_bilibili