跨平台,指令集小(8位),编译器容易实现,性能下降,指令更多
栈:运行时的单位,解决程序的运行问题
堆:存储的单位,解决数据存储的问题,比较大的区域
- 每个线程创建时都会创建一个Java虚拟机栈,保存的是栈帧,一个战帧对应一个Java方法,方法调用代表着栈帧的出栈操作
- 与线程相同生命周期
- 主管java乘除的运行,保存局部变量(8种基本数据类型、对象的引用地址)、部分结果,参与方法的调用和返回
- 局部变量 成员变量
- 基本数据类型变量 引用类型变量
栈的优点:
- 快速的分配存储,仅此于PC register
- 方法执行入栈,执行结束出栈
- 栈不存在垃圾回收
栈中可能出现的异常:
-
StackOverflowError:固定大小的Java虚拟机栈,如果超过,抛异常
-
OutOfMemoryError:可动态扩展,没有足够的内存
package charpter05;
public class StackErrorTest {
public static void main(String[] args) {
main(args);
}
}
- 设置栈内存大小:-Xss 256 k
- 基本存储结构:栈帧
栈运行原理:
- JVM对java 栈的操作只有压栈和出栈,先进后出原则
- 正在执行的方法对应的栈帧在栈顶
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
- 不同线程之间的栈帧不允许相互引用,因为JVM栈是线程私有的
- 正常返回和抛出异常(未处理)都会导致栈帧被弹出
栈帧的内部结构:
- 局部变量表
- 操作数栈(表达式栈)
- 动态链接
- 方法返回地址
- 一些附加信息
- 主要用于存储 方法参数和定义在方法体内的局部变量
- 线程私有,不存在数据安全问题
- 所需容量大小在编译期确定,保存在方法的Code属性的maximum local variables数据项中,运行期间不会改变
- 方法嵌套调用的次数由栈的大小决定。
- 栈大小固定,栈帧的大小决定了栈内可以存储栈帧的个数,局部变量表的大小决定了栈帧的大小
- 方法结束调用后,随着栈帧的销毁,局部变量表也随之销毁
package charpter05;
public class StackframeTest {
public static void main(String[] args) {
StackframeTest stackframeTest = new StackframeTest();
stackframeTest.method1();
}
public void method1(){
System.out.println("method1 方法开始");
method2();
System.out.println("method1 方法结束");
}
public int method2() {
System.out.println("method2 方法开始");
int i = 10;
int m = (int) method3();
System.out.println("method2 方法即将结束");
return i + m;
}
public double method3() {
System.out.println("method3 方法开始");
double j = 20.0;
System.out.println("method3 方法即将结束");
return j;
}
}
关于Slot的理解:
- 变量从索引0开始到长度为-1结束
- 局部变量表中最基本存储单元
- 存储基本数据类型,引用类型,返回地址
- 32位(引用类型,返回地址)变量占用一个Slot,64位(long和double)占用2个Slot
public String test2(Date date, String name){
date = null;
name = "abc";
double weight = 130.5;
char gender = '男';
return date + name;
}
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0 的slot处
public void test4(){
int a=0;
{
int b=0;
b=a+1;
}
int c = a+1;
}
变量分类
数据类型:基本数据类型和引用数据类型
在类中声明位置:成员变量(类变量/静态变量 实例变量,在使用前都经历过默认初始化赋值)和局部变量
- 类变量:linking – prepare给类变量默认赋值–initialization给类变量显示赋值及静态代码块赋值
- 实例变量:随着对象的创建会在堆空间中分配实例变量空间,并默认赋值
- 局部变量在使用前必须显式赋值
局部变量表中的变量是重要的垃圾回收根节点,只要被局部变量表中直接或者间接引用的对象都不会被回收
操作数栈- 在方法执行过程中,根据字节码指令,往栈中写入或者弹出数据
- 保存计算过程的中间结果,也作为计算过程中变量临时存储结构
- 操作数栈在编译期就确定了深度 max_stack中
- 栈中任意一个元素可以是java的任意类型,32bit占用一个深度,64bit占用两个深度
- 方法带有返回值,返回值也要放在栈中
- 解释引擎是基于栈的执行引擎
package charpter05;
public class Test {
public void testAdd(){
byte i =15;
int j =8;
int k = i + j;
}
}
如果被调用的方法带有返回值的话,其返回值会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
i++和++i的区别:字节码篇章讲解
栈顶缓存技术由于操作数是存储在内存中的,因此需要频繁的执行内存读写操作,影响速度。所以设计了栈顶缓存技术,将栈顶元素全部缓存在屋里CPU的寄存器中,降低读写次数,提升执行引擎执行效率。
动态链接- 每一个栈帧内部包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的是为了支持当前方法的代码能够实现动态链接。
- 指向运行时常量池的方法引用
- 将符号引用转换为调用方法的直接引用
package charpter05;
public class DynamiclinkTest {
int num =10;
public void methodA(){
System.out.println("methodA..");
}
public void methodB(){
System.out.println("methodB..");
methodA();
num++;
}
}
静态链接:一个字节码文件被装进JVm内部时,如果被调用的目标方法在编译期可知,且运行期保持不变
动态链接:被调用的方法在编译期无法确定下来,只能够在程序运行期将调用的符号引用转换为直接引用。
早期绑定:被调用的目标方法在编译期可知,且运行期保持不标
晚期绑定:被调用的方法在编译期无法确定下来,只能在程序运行期根据实际的类型绑定相关方法
package charpter05;
class Animal {
public void eat(){
System.out.println("动物进食。。。");
}
}
interface Huntable{
void hunt();
}
class Dog extends Animal implements Huntable{
public void eat(){
System.out.println("够吃骨头。。");
}
@Override
public void hunt() {
System.out.println("捕食耗子,多管闲事。。");
}
}
class Cat extends Animal implements Huntable{
//早期绑定
public Cat(){
super();
}
//早期绑定
public Cat(String name){
this();
}
public void eat(){
super.eat();//早起绑定
System.out.println("猫吃鱼。。");
}
@Override
public void hunt() {
System.out.println("捕食耗子,天经地义。。");
}
}
public class AnimalTest{
public void showAnimal(Animal animal){
animal.eat();//晚期绑定
}
public void showHunt(Huntable huntable){
huntable.hunt();//晚期绑定
}
}
- 非虚方法:编译期间就确定了具体的调用方法。包含静态方法,私有方法,final方法,实例构造器、父类方法。invokestatic,invokespecial
- 虚方法:其他方法
package charpter05;
public class Father {
public Father() {
System.out.println("Father 构造器");
}
public static void showStatic(String str){
System.out.println("father "+ str);
}
public final void showFinal(){
System.out.println("father show final");
}
public void showCommon(){
System.out.println("father 普通方法。。");
}
}
class Son extends Father{
public Son() {
super();
}
public Son(int age){
this();
}
public static void showStatic(String str){
System.out.println("son "+str);
}
public void showPrivate(String str){
System.out.println("son private "+ str);
}
public void show(){
//invokestatic
showStatic("xx");
//invokestatic
super.showStatic("yy");
//invokevirtual
showPrivate("hello");
//invokespecial
super.showCommon();
//invokevirtual 非虚方法
showFinal();
//invokevirtual 虚方法
showCommon();
//invokespecial 非虚方法
info();
Method method = null;
//invokeinterface 虚方法
method.methodA();
}
private void info() {
}
public void display(Father father){
father.showCommon();
}
interface Method{
void methodA();
}
public static void main(String[] args) {
Son son = new Son();
son.show();
}
}
普通调用指令:
- invokestatic:调用静态方法
- invokespecial:调用、私有及父类方法
- invokevirtual:调用虚方法
- invokeinterface:调用接口方法
动态调用指令:
- invokedynamic:动态解析需要调用的方法然后执行
- java7增加
- java8 lambda 出现后,有直接生成方式
动态类型语言:判断变量值的类型语言,变量没有类型信息,变量值才有
info = 13.5
静态类型语言:判断变量自身的类型信息
String name = "xx";
package charpter05;
interface Func{
public boolean func(String str);
}
public class Lambda {
public void lambda(Func func){
}
public static void main(String[] args) {
Lambda lambda = new Lambda();
Func func = str -> {
return true;
};
lambda.lambda(func);
}
}
方法重写本质:
- 找到操作数栈顶的第一个元素所执行的对象的实际类型C
-如果在过程结束: 如果不通过类型C找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回方法的直接引用,查找过,则返回IllegalAccessError异常 - 否则按照集成关系从下往上依次对C的父类进行第2步的搜索和验证
- 如果始终没有找到合适的方法,则抛出AbstractMethodError异常
面向对象编程中,会频繁使用动态分派,如果每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话会影响执行效率。为了提高性能,JVM采用在类的方法区建立一个虚方法表来实现,使用索引表来代替查找。
- 每个类中都有一个虚方法表,表中存放各种方法的实际入口
- 虚方法表在链接阶段(解析)被创建并初始化
package charpter05;
interface Friendly{
void sayHello();
void sayGoogBye();
}
class Dog{
public void sayHello(){
}
public String toString(){
return "Dog";
}
}
class Cat implements Friendly{
public void eat(){
}
@Override
public void sayHello() {
}
@Override
public void sayGoogBye() {
}
protected void finalize(){
}
@Override
public String toString() {
return "Cat";
}
}
class CockerSpaniel extends Dog implements Friendly{
@Override
public void sayHello() {
super.sayHello();
}
@Override
public void sayGoogBye() {
}
}
public class VirtualMethodTable {
}
方法返回地址
存放调用该方法的PC寄存器的值
当一个方法开始执行后,只有两种方式退出方法
- 执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口。
字节码指令中返回指令ireturn(当返回值为 Boolean、byte、char、short 和 int);lreturn(long);freturn(float);dreturn(double);areturn(引用类型);return(void方法、构造器) - 在方法执行过程中遇到了异常,并且异常没有再方法内处理,简称异常完成出口
package charpter05;
import java.io.FileReader;
import java.io.IOException;
public class ExceptionTableTest {
public void method2(){
try{
method1();
}catch (IOException e){
e.printStackTrace();
}
}
public void method1() throws IOException{
FileReader fileReader = new FileReader("antguigu.txt");
char[] cBuffer = new char[1024];
int len;
while ((len = fileReader.read(cBuffer)) != -1){
String s = new String(cBuffer, 0, len);
System.out.println(s);
}
fileReader.close();
}
}
正常会给PC寄存器地址,异常完成出口退出的不会给他的上层调用者产生任何的返回值
对程序调试提供支持的信息
栈的相关面试题- 举例栈溢出的情况?
StackOverflow,-Xss设置栈大小,栈固定大小提示 - 调整栈大小,就能保证不出现溢出吗?
不能保证,保证出现的晚,但不能保证不出现 - 分配的栈内存越大越好吗?
不好,内存空间有限,栈内存变大,堆空间及其他空间就变小 - 垃圾回收时候涉及到虚拟机栈?
不会,只有进栈和出栈 - 方法中定义的局部变量线程是否安全?
具体问题具体分析。内部产生内部消亡,则安全
何为线程安全? - 如果只有一个线程才可以操作此数据,则线程安全。
- 如果多个线程操作此数据,共享数据,如果不考虑同步机制,则线程不安全
package charpter05;
public class StringBuilderTest {
int num =10;
// stringBuilder声明方式线程安全
public static void method1(){
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
}
// stringBuilder 线程不安全,被传入,有可能被其他线程传入
public static void method2(StringBuilder stringBuilder){
stringBuilder.append("a");
stringBuilder.append("b");
}
// stringBuilder线程不安全,返回后可被其他线程操作
public static StringBuilder method3(){
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder;
}
// stringBuilder线程安全,stringBuilder内部死亡
public static String method4(){
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder.toString();
}
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder();
new Thread(()->{
stringBuilder.append("a");
stringBuilder.append("b");
}).start();
method2(stringBuilder);
}
}



