目录
概述
由于跨平台的设计,java的指令是根据栈来设计的,不同CPU架构不同,不能设计成基于寄存器的。
优点,跨平台,简单实现,指令小。
缺点,指令多。
栈是运行时单位(只要是管理运行的,都是线程分开线程管理的),而堆是存储单位,
栈的读和写都是由规律的,
但是堆,就好比一个大仓库,里面的东西都是有大有小的,取的时候也是从中随机取的,取多了就会存在这一个碎片,那一个碎片,所以垃圾回收为了最大利用这个大仓库,减少碎片,搞了一套GC机制。
每个线程都会创建一个虚拟机栈,内部保存一个个栈帧(Stack Frame),一个栈帧对应一个方法的调用。
作用:
主管java程序的运行,保存方法的局部变量(8种基本数据类型 + 对象引用地址),部分结果,并参与方法调用和返回。
补充,你遇到过哪些异常
RuntimeException:
空指针
类型转化异常,
OutOfMemoryError (堆溢出,或者动态扩展的栈在太大情况下无法获取到足够内存时)
StackOrverFLowError(如果-Xss固定栈大小而溢出)
数组越界
NoClassDefFoundError(运行时根据aaa.bbb.XXX获取类定义时失败,一般都是包版本不一致导致,或者AOP失败导致)
非RuntimeException:
ClassNotFindException
这是一个检查型异常,需要显式捕获。
当程序试图通过字符串名加载类时,抛出该异常:
Class 类中的 forName 方法。
ClassLoader 类中的 findSystemClass 方法。
ClassLoader 类中的 loadClass 方法。
以及那些数据库链接异常等。
- 栈的存储单位栈帧
每个线程都有一个自己的栈,栈中的数据是以栈帧为基本单位进行存储的。
线程中对应的正在执行的方法对应一个栈帧, 当前方法return或者异常,该方法对应的栈帧出栈,
栈帧维系运行过程需要的数据。
栈帧
包含
- 局部变量表,
- 操作数栈,
- 动态链接(指向运行时常量池的方法引用),
- 方法返回地址(或者方法正常返回和异常的信息),
- 其他附加信息。
局部变量表(local variables)
一个数组,存放参数和方法体的局部变量
线程私有,没有线程安全问题
大小在编译期间就确定
基本单位32位的Slot (包括returnAddress)
32位以内的占一个Slot
byte,short,char,boolean 强转int
long,double占俩Slot,使用其起始下标
局部变量是有作用域的,过了作用域,该其占用的槽,会被重复利用。
按声明顺序放置
this引用放置在slots[0]中
在栈帧中,与性能调优最为密切的就是局部变量表,方法执行过程中,VM使用局部变量表完成方法的参数传递。
局部变量表也是垃圾回收重要节点,被局部变量表中的引用指向的不会被回收。
操作数栈
可以用数组和链表来实现。(数组实现最简单,下标前移表示出栈pop,后移并修改值就是入栈push)
32位和占一个深度,64位的占两个深度。
局部变量表和栈一样,基本单位大小固定32位(一个int的大小)由此可见,其实定义short和int其实区别不大。
如果调用方法有返回值,返回值会直接压入上一个栈帧的操作数栈,如果该返回值没有store则会调用Pop
++i和i++ ,单独的++i和i++其实是没有区别的,都是在局部变量表中自增,
如果配合a = ++i; a = i++; 前者表示先自增再load到操作数栈,后者表示先load的操作数栈,接着局部变量表中的值自增;
只要不写i = i++(当然这种是不符合编码风格规范的), 其实运行的结果都是一样的。
栈顶缓存技术(Top of Stack Caching):
HotSpot VM团队提出的一个技术,将栈顶元素缓存到物理寄存器中,降低对内存读写次数,提升效率。
动态链接
指向运行时常量池中栈帧所属方法的引用(程序运行时,内存分为数据区和指令区,物理寄存器就是取指令区的指令进行运行的)
class文件符号和方法的引用都是符号引用,动态链接就是将这些符号引用转化为方法的直接引用。
将动态链接,方法返回地址,附加信息合称为帧数据
多态其实就是源于这个动态链接
其实运行时常量池中也不过存储的都是方法和类的指针,其实这些真正信息还是在堆中。
方法的调用
JVM中,将符号转化为调用方法的直接引用与方法的绑定机制相关。
静态链接:(早期绑定)invokeSpecial(特定的方法,如构造方法, private 方法, super.xxx方法),invokeStatic
目标方法在编译期可知。这种情况的符号引用转化为直接引用称静态引用。
动态链接:(晚期绑定)invokeVirtual, invokeInterface,invokedynamic
目标方法在运行时确定下来的。
案例1:
public static void main(String[] args) {
Human human = new Son();
if (human instanceof Father) {
//其实还是调用Son
((Father)human).say();
}
if (human instanceof Son) {
((Son)human).say();
}
}
案例2:
ublic class Father implements Human {
public static void staticFun() {
}
public void publicFun() {
}
private void privateFun() {
}
public void testDynamicLink() {
staticFun();
//INVOKESTATIC com/jack/ascp/purchase/app/test/vm/dynamiclink/Father.staticFun ()V
publicFun();
//INVOKEVIRTUAL com/jack/ascp/purchase/app/test/vm/dynamiclink/Father.publicFun ()V
privateFun();
//INVOKESPECIAL com/jack/ascp/purchase/app/test/vm/dynamiclink/Father.privateFun ()V
String s = super.toString();
//INVOKESPECIAL java/lang/Object.toString ()Ljava/lang/String;
Daughter daughter = new Daughter();
// INVOKESPECIAL com/jack/ascp/purchase/app/test/vm/dynamiclink/Daughter.<init> ()V
Daughter.staticFun();
//INVOKESTATIC com/jack/ascp/purchase/app/test/vm/dynamiclink/Daughter.staticFun ()V
daughter.publicFun();
//INVOKEVIRTUAL com/jack/ascp/purchase/app/test/vm/dynamiclink/Daughter.publicFun ()V
daughter.fianlFun();
//INVOKEVIRTUAL com/jack/ascp/purchase/app/test/vm/dynamiclink/Daughter.fianlFun ()V
say();
//INVOKEVIRTUAL com/jack/ascp/purchase/app/test/vm/dynamiclink/Father.say ()V
Human human = new Daughter();
human.say();
//为什么final修饰的方法也是invokeVirtual
}
@Override
public void say() {
System.out.println("I am father");
}
}
留个问题,
1、为什么final修饰的方法也是invokeVirtual?我也不理解,我觉得应该是invokeSpecial
2、为什么super是调用invokeSpecial,他的直接父类也有可能没有实现这个方法,比如toString通常在Object里面。
(这个很好解释,既然是super.aaa() 了,那必定是父类以上,肯定不是子类,既然肯定不是子类,至于调用哪个方法,编译器在编译的时候就可以找到最近的哪个父类有该方法)
多态:
运行时多态:泛型,重写(接口参数类型时基类,调用可传子类)。
编译器多态:函数的重载。
四种调用
invokestatic 非虚方法
invokespecial 非虚方法
invokevirtual 虚方法
invokeinterface 虚方法
java的多态和c++不一样
java认为所有普通成员方法(非private非super调用的成员方法)都是invokevitual或invokeinterce,也就是编译的时候,java就认为他是一个虚方法。
而C++在编译的时候,编译器以为这个方法就是父类调用的,但是在运行的才知道原来是子类调用。
java中的接口就类似C++的虚类,但是其实思想不一样,C++的虚类也是继承,就好比继承了一个假的父类,他是为了多态而设计,而java虽然也是为了多态而设计,他是他认为interface就好像是一种能力,而继承就好比是模板。(人类是一个父类,超人就是继承了人类,并且实现了“飞”这个接口)
invokedynamic
1.7增加,为了实现动态类型语言而设计。
在1.7中并没有提供直接的调用invokedynamic的方法,需要借助ASM来产生invokedynamic指令。
知道1.8出现lambda表达式的出现,才出现直接的调用invokedynamic。
java是静态语言,但是可以用invokedynamic执行动态语言支持
动态语言和静态语言:区别在于类型检查是在编译期间进行还是运行期间进行
静态类型语言是判断变量自身的类型信息,动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息
动态语言:python (str = 'aaa')scala (val str = 'aaa') js (var str = 'aaa')
静态语言: java , C, C++, C# (String str = "aaa")
比如,在python中我们调用a.fun(); 传入对象如果有这个方法就没事, 但是如果传入对象没有这个方法,编译不会报错,运行才会报错。但是如果这个传入的不是期望的对象,但是也会有fun方法,那么他的返回信息,可能在运行时报错,比如你期望获得一个变量,他是他返回了元组。
/**
INVOKEDYNAMIC func()Lcom/jack/ascp/purchase/app/test/vm/dynamiclink/invokedynamic/Fun; [
// handle kind 0x6 : INVOKESTATIC
java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
// arguments:
(Ljava/lang/String;)Z,
// handle kind 0x6 : INVOKESTATIC
com/jack/ascp/purchase/app/test/vm/dynamiclink/invokedynamic/LambdaTest.lambda$main$0(Ljava/lang/String;)Z,
(Ljava/lang/String;)Z
]
*/
Fun fun1 = s -> {
return s.equals("true");
};
java动态语言的支持,是对java虚拟机规范的修改,而不是对java规范的修改,设计复杂,并且增加了方法的调用。使java可以传递函数,但是效率低,所以stream流效率低主要因为开了很多栈帧。
stream流可以尽量不用,比如map写着好看,filter好看,开发快,可以用。foreach就算了。或者map的时候我还得加代码而不是传方法,那也就算了,写个for循环吧,何必多次一举。其实有时候stream一串下来,看起来也很难看。java是静态语言,有时候没必要用就不用装逼了。
方法重写机制:
1、找到操作数栈的第一个元素所指向对象的实际类型,记为C
2、如果在C中找到方法与常量池中的方法描述相同,则进行权限校验,如果通过直接执行,查找过程结束,如果不通过则返回java.lang.AccessIllegalAccessError,查找过程结束。
3、如果找不到,则从下到上对父类进行查找。并执行验证。
4、如果都没有找到,则返回AbstractMethodError。
这个查找过程漫长,为了提高性能,JVM在类方法区建立了一个虚方法表(virtual method table),是用索引表代替查找。就可以直接找到具体方法,这个virtual method table在链接(的解析)阶段就建立了
AccessIllegalAccessError:一般情况编译器会校验。一般运行时报的话是因为子包的jar包冲突或者AOP没写好。
方法返回地址(return address)
存放该方法返回到的PC寄存器的值
返回方式:
正常返回,异常返回
不管哪种,都得回到被调用的位置,只不过异常退出没有返回值。
返回指令根据返回类型决定:ireturn(byte, short,int,boolean,char),lreturn(long), freturn(float), dreturn(double), areturn(引用a表示address) return(void)
异常处理表
异常捕获从第4个指令到8个指令,如果捕获到异常则到11个指令开始处理。如果代码正常运行到8指令,则goto到16返回了。
5.3.7 一些附加信息
如对调试支持的一些信息。
5.3.8 面试题
举例StackOrverFLow的原因情况
通过-Xss设置栈的大小,如果溢出整个内存则会OOM
通过调整大小就能保住不会溢出吗?
可能,但不能保证,如果是递归深度太大,可以调整,但是如果死循环,就一定溢出
分配栈内存越大越好吗?
按需分配,栈越大会挤占其他内存空间。
垃圾回收会涉及到虚拟机栈吗?
不会,栈是一个有规则(Push 和Pop)的内存空间,在用就在栈中,不需要了边Pop。怎么会有这么傻逼的问题。
局部变量是否数据安全
何为线程安全:当多个线程操作同一个数据时,不会违背我们的期望。
如果每个线程只读不写,或者只有一个线程读且写, 可以认为肯定是线程安全的。
JVM中所有内存结构都是线程安全的,如果不安全就是JVM的debug,JVM不会因为java程序员菜,导致由线程不安全引发的JVM中的数据结构混乱,从而使JVM无法运行。
参数:
-XX:ThreadStackSize
设置为0代表使用不同的操作系统默认值