JVM体系结构概览,
JVM体系结构概览,
宋红康老师视频传送门ˊᵕˋ
jvm虚拟机
jvm:跨语言的平台
jvm字节码:我们平时常说的java字节码,指的是用java语言编成的字节码。准确的说任何能在jvm平台上执行的字节码格式都是一样的,所以应该统称为jvm字节码
不同的编译器可以编译出相同的字节码文件,字节码文件也可以在不同的jvm运行。
Java虚拟机与Java语言并没有必然的联系,它只与特定的二进制文件格式一Class文件格式所关联, Class文件中包含了Java 虚拟机指令集(或者称为字节码、Bytecodes) 和符号表,还有一些其他辅助信息。
每个进程对应一个jvm虚拟机实例,一个jvm实例就有一个运行时数据区(Runtime Data Area)
jvm的架构模型
java编译器输入的指令流基本是一种基于栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构
基于栈式架构的特点(更少的指令集,更多的指令)
设计和实现更简单,适用于资源受限的系统
避开了寄存器的分配难题,使用零地址指令方式分配
指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈,指令集更小,编译器更容易实现
不需要硬件支持,可移植性更好,更好实现跨平台
基于寄存器架构的特点(更少的指令,更多的指令集)
典型的应用是X86的二进制指令集,比如传统的pc以及Anroid的Davlik虚拟机
指令集架构则完全依赖硬件,可移植性差
性能优秀和执行更高效
花费更少的指令去完成一些操作
在大部分情况下,基于寄存器的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集确是以零地址指令为主
jvm的生命周期
java虚拟机的启动是通过引导类加载器创建引导类加载器创建一个初始类来完成的,这个类是由虚拟机的具体实现指定的
虚拟机的执行
一个运行中的java虚拟机有一个清晰的任务:执行java程序。
程序开始时虚拟机运行,程序结束时他就停止。
执行一个所谓的java程序的时候,真真正正在执行的是一个叫做java虚拟机的进程。
虚拟机的退出
有如下的几种情况:
程序正常执行结束
程序在执行过程中遇到了异常或错误而异常终止
由于操作系统出现错误而导致Java虚拟机进程终止:
某线程调用Runtime类或system类的exit方法,或Runt ime类的halt方法,并且Javll安全管理器也允许这次exit或halt操作。
除此之外,JNI ( Java Native Interface )规范描述了用JNI Invocation API来 加载或卸载Java虛 拟机时,Java虚拟机的退出情况。
Sun Classic VM
- sun公司发布的第一款商用java虚拟机,于jdk1.4被淘汰
- 这款虚拟机只提供解释器。
- 如果使用JIT编译器,就需要进行外挂,但是一旦使用了JIT编译器,JIT就会接管虚拟机的执行系统。解释器就不再工作。解释器和编译器不能配合工作
- 现在hotspot内置了此虚拟机
*理解执行引擎
翻译机:
解释器:逐行解释代码,响应速度快,执行速度慢。
JIT:寻找热点代码,全部编译,响应速度慢,执行速度快。
类加载子系统
类加载器子系统负责从文件系统或者网络中加载Class文件,class文件开头有特定的文件标识(CAFEBABY)
ClassLoader只负责class文件的加载,至于它是否可以运行,则由ExecutionEngine(执行引擎)决定
加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息(当常量池开始运行时就被称为运行时常量池),可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
类的加载过程
1.通过一个类的全限定名获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java. lang.Class对象,作为方法区这个类的各种数据的访问入口
加载. class文件的方式
从本地系统中直接加载
通过网络获取,典型场景: Web Applet
从zip压缩包中读取,成为日后jar、war格式的基础
运行时计算生成,使用最多的是:动态代理技术
由其他文件生成,典型场景: JSP应用
从专有数据库中提取. class文件,比较少见
从加密文件中获取,典型的防Class文件被反编译的保护措施
链接(link)
验证(Verify ) :
●目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。准备(Prepare) :
为类变量分配内存并且设置该类变量的默认初始值,即零值。
● 这里不包含用final修饰的static, 因为final在编译的时候就会分配了,准备阶段会显式初始化;这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。解析(Resolve) :
●将常量池内的符号引用转换为直接引用的过程。
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java 虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_ Class_ info、 CONSTANT_ Fieldref_ info、CONSTANT_ Methodref_ info等
初始化(initialization)
将代码中的静态代码块和显示初始化合并在一起,构成
初始化阶段就是执行类构造器方法
此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
构造器方法中指令按语句在源文件中出现的顺序执行。
若该类具有父类,jvm会保证子类的
()执行前,父类的 ()已经执行完毕。 虚拟机必须保证一个类的
()方法在多线程下被同步加锁
public static void main(String args[]){
private static int b = 1;
static{
b = 2;
num = 20;
}
private static int num = 10;//在linking中:num默认初始化零值,之后在initialization中初始化为20,然后覆盖为10
}
类加载器的分类
分为两类:引导类加载器和自定义类加载器,直接或间接继承Class Loader的类加载器都是自定义加载器(java虚拟机规范)
引导类:Bootstrap Class Loader(使用c/c++语言编写,只负责加载java的核心类库,如String)
自定义类(使用java语言编写):Extension Class Loader(扩展类加载器)、System Class Loader(系统类加载器)........
启动类加载器(引导类加载器,Bootstrap ClassLoader)
这个类加载使用C/C++语言实现的,嵌套在JVM内部。
它用来加载Java的核心库(JAVA_ HOME/jre/ lib/rt. jar、resources . jar或sun . boot .class. path路径下的内容) ,用于提供JVM自身需要的类。
并不继承自java. lang. ClassLoader,没有父加载器。
加载扩展类和应用程序类加载器,并指定为他们的父类加载器。出于安全考虑、Bootstrap启动类 加载器只加载包名为java、javax、sun等开头的类。
扩展类加载器(Extension ClassLoader )
Java语言编写,由sun . mi sc. Launcher$ExtClassLoader实现。
派生于ClassLoader类。
父类加载器为启动类加载器。
从java. ext. dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
应用程序类加载器(系统类加载器,AppClassLoader )
java语言编写,由sun . mi sc . Launcher$AppClassLoader实现。
派生于ClassLoader类。
父类加载器为扩展类加载器。
它负责加载环境变量classpath或系统属性java. class.path指定路径下的类库。
该类加载是程序中默认的类加载器,一- 般来说,Java应用的类都是由它来完成加载。
通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
用户自定义类加载器
在Java的日常应用程序开发中,类的加载几乎是由,上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
为什么要自定义类加载器?
隔离加载类。
修改类加载的方式。
扩展加载源。
防止源码泄漏。
用户自定义类加载器实现步骤:
1.开发人员可以通过继承抽象类java. lang. ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求。
2.在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass ()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中。
3.在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
其他要点
1.在JVM中表示两个class对象是否为同一个类存在两个必要条件:
类的完整类名必须一致,包括包名。
加载这个类的ClassLoader (指ClassLoader实例对象)必须相同。.
即使两个类对象来源于同一个Class文件,被同一个虚拟机所加载,但只要它们的ClassLoader不同,那这两个类对象也是不一样的
2.JVM必须知道-一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
双亲委派机制
Java,虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
工作原理
1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
2)如果父类加载器还存在其父类加.载器,则进一步向上委托,依次递归,请求最终将到达项层的启动类加载器;
3)如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
优势:
避免类的重复加载
保护程序安全,防止核心API被随意篡改
沙箱安全机制
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java \lang\String.class),报错信息说没有main方法,就是因为加载的是rt. jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
运行时数据区间内部结构
Java虚拟机定义了若千种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一 一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
一个运行时数据区间对应一个Runtime Class(可以通过getRuntime()获取到),所以Runtime Class是单例的。(详见javaSE api)
灰色的为单独线程私有的,红色的为多个线程共享的。即:
每个线程:独立包括程序计数器、栈、本地栈。
线程间共享:堆、堆外内存(永久代或元空间、代码缓存)
(寄存器/程序计数器)Regist介绍
作用:
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
任何时间一一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址,或者,如果是在执行native方法,则是未指定值(undefined) 。
常见问题
使用PC寄存器存储字节码指令地址有什么用呢?
为什么使用PC寄存器记录当前线程的执行地址呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
PC寄存器为什么会被设定为线程私有?
我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。
虚拟机栈
Java虚拟机栈是什么?
Java虚拟机栈(Java Virtual Machine Stack) ,早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame) ,对应着一次次的Java方法调用。
是线程私有的
生命周期:生命周期和线程一致。
作用
主管Java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。
局部变量VS成员变量(或属性)
基本数据变量VS引用类型变量(类、数组、接口)
可能出现的异常
在这个内存区域中,如果线程请求的栈帧深度大于虚拟机所允许的深度,将抛出StarkOverflowError异常;如果java虚拟机栈容量可以动态扩展,当栈扩展是无法申请到足够的内存时会抛出OutOfMemoryError异常。
设置栈内存大小
可以使用参数-Xss选徐来设置线程最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
private int count = 0;
public static void main(String args[]){
count++;
main(rgs[]);
}
//最后报出StarkOverflowError
栈的存储单位
每个线程都有自己的栈,栈中的数据都是以栈桢(stack Frame)的格式存在。
在这个线程上正在执行的每个方法都各自对应一个栈桢(Stack Frame)
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
栈运行原理
JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame) ,与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。
如果当前方法调用 了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
Java方法有 两种返回函数的方式,一种是正常的函数返回,使用return指令; 另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
栈桢的内部结构
帧数据区
就是指栈帧中的方法返回地址、动态链接和附加信息
每个栈桢中存储着:
局部变量表(Local Variables)
操作数栈(operand stack) (或表达 式栈)
动态链接(Dynamic Linking) ( 或指向运行时常量池的方法引用)
方法返回地址(Return Address) ( 或方法正常退出或者异常退出的定义)
一些附加信息
局部变量表(Local Variables)
关于jclasslib操作在视频的p49
其大小在Class反编译文件中以locals查看,在jclasslib中的misc下
局部变量表也被称之为局部变量数组或本地变量表。
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference) ,以及,returnAddress类型。
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。.
方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
Slot(槽)
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
局部变量表,最基本的存储单元是Slot (变量槽)
局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
在局部变量表里,32位以内的类型只占用一个slot (包括returnAddress类型),64位的类型(long和double)占用两个slot。
byte、short、char在存储前被转换为int,boolean 也被转换为int,0表示false,非0表示true。
long和double则占据两个Slot。
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其做用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局变量的槽位,从而达到节省资源的目的。
静态变量与局部变量
参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。
和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。
ps:变量的分类:
按照数据类型分:基本数据类型 & 引用数据类型
按照类中声明的位置分:
成员变量
在使用前,都经历过默认初始化赋值
有static修饰:类变量-->在linking的prepare阶段给其默认赋值,在init阶段显示赋值
无static修饰:实例变量-->在对象创建时会在堆中分配内存,进行默认赋值
局部变量
在使用时必须进行显示赋值,否则编译不通过
如:
public static void mian(String args[]){
int i;
System.out.println(i);//变量i未初始化
}
其他
在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
操作数栈(Operand Stack)
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈就是JVM执行引擎的一个 工作区,当一个方法刚开始执行的候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_ stack的值。
栈中的任何一个元素都是可以任意的Java数据类型。
32bit的类型占用一个栈单位深度
64bit的类型占用两个栈单位深度操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push) 和出栈(pop) 操作来完成一次数据访问。
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
Public static void main(String args[]){
int i = 1;
int j = 2;
int l = i+j;
System.out.println(l);
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1 //stack即为操作数栈深度,如果此方法未用静态修饰,则局部变量表长度为5,索引0是this,在此方法中,由于是main方法,所以索引0是args
0: iconst_1//int i-->初始化int类型常量,压入操作数栈中<--栈顶(深度1)
1: istore_1//出栈,存入局部变量表,索引位置为1
2: iconst_2//itn j-->初始化int类型常量,压入操作数栈中<--栈顶(深度1)
3: istore_2//出栈,存入局部变量表,索引位置为2
4: iload_1//获得局部变量表中索引为1的值,压入操作数栈中(深度1)
5: iload_2//获得局部变量表中索引为2的值,压入操作数栈中(深度2)
6: iadd //取出操作数栈中的值,交由执行引擎执行求和操作,其求和的值再次压入操作数栈中
7: istore_3//出栈,将其存入局部变量表中索引为3的位置
8: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iload_3//获得局部变量表中索引为3的值,压入操作数栈中,执行输出操作
12: invokevirtual #22 // Method java/io/PrintStream.println:(I)V
15: return
栈顶缓存技术(Top-Of-Stack-Cashing)
前面提过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch) 次数和内存读/写次数。
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,Hotspot JVM的设计者们提出了栈顶缓存(ToS,Top-of-Stack Cashing) 技术,将栈顶元索全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
动态链接(Dynamic Linking)
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking) 。比如: invokedynamic指令。
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference) 保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
在栈桢中存在有常量池,当线程开始运行时,栈桢中的常量池会载入到方法区中,也就是其中的运行时常量池,每一个栈桢的常量池中的符号引用都可以引用运行时常量池中的方法,这样一来,就节省了内存空间。
方法的调用
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
●静态链接:
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。●动态链接:
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding) 。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
●早期绑定:
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪-一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。●晚期绑定:
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。
Java中任何一个普通的方法其实都具备虚函数(晚期绑定)的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虛函数的特征时,则可以使用关键字final来标记这个方法。
虚方法与非虚方法
非虚方法:
●如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
●静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
●其他方法称为虚方法。
子类对象的多态性的使用前提:①类的继承关系②方法的重写
虚拟机中提供了以下几条方法调用指令:
普通调用指令:
动态调用指令:
invokedynamic: 动态解析出需要调用的方法,然后执行前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。
关于invokedynamic指令
在class文件中,若使用了Lambda表达式来定义匿名方法,其在字节码中就会以invokedynamic修饰。
JVM字节码指令集一直比较稳定,直到Java7中才增加了一个invokedynamic指令,这是Jaya为了实现「动态类型语言」支持而做的一种改进。
但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。
Java7中增加的动态语言类型支持的本质是对Java虛拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。
动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。
说的再直白一点就是,静态类型语言是判断变量自身的类型信息:动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。
静态类型语言:Java: String info = "abcd";
动态类型语言:Js: var info="abcd" var info=1
动态类型语言:python: info=100.1
方法返回地址(Return Address)
作用:存放调用该方法的pc寄存器的值。(只保存正常退出的方法的pc寄存器的值)
一个方法的结束,有两种方式:
正常执行完成
出现未处理的异常,非正常退出
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
当一个方法开始执行后,只有两种方式可以退出这个方法:
执行引擎遇到任意一 一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口;
一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定;
在字节码指令中,返回指令包含ireturn (当返回值是boolean、 byte、char、short和int类型时使用)、lreturn(long类型)、 freturn(float类型)、 dreturn(double类型)以及areturn(引用类型:String、Date),另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用。
- 在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口。
- 方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。
class字节码中的异常处理表
Exception table
from to target type
4 16 19 any//在4-16行中的任何异常,由19行来处理
19 21 19 any
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
*一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。(不一定有)
本地方法栈
Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
本地方法栈,也是线程私有的。
允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java 虚拟机将会抛出一个stackoverflowError 异常。
如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虛拟机将会抛出一个outofMemoryError 异常。
本地方法是使用C语言实现的。
它的具体做法是Native Method Stack中 登记native方法,在Execution Engine执行时加载本地方法库。
当某个线程调用一一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虛拟机拥有同样的权限。
本地方法可以通过本地方法接0来访问虚拟机内部的运行时数据区。
它甚至可以直接使用本地处理器中的寄存器
直接从本地内存的堆中分配任意数量的内存。
并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
在Hotspot JVM中,直接将本地方法栈和虛拟机栈合二为一。
堆(heap)
核心概述
一个JVM实例只存在-一个堆内存,堆也是Java内存管理的核心区域。Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
堆内存的大小是可以调节的。
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB) 。
《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated )
我要说的是:“几乎”所有的对象实例都在这里分配内存。——从实际使用角度看的。
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
堆,是GC ( Garbage Collection, 垃圾收集器)执行垃圾回收的重点区域。
堆的内存细分
Java 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
➢Young Generation Space 新生区 Young/New
又被划分为Eden区和Survivor区
➢Tenure generation space 养老区 Old/ Tenure
➢Permanent Space 永久区 Perm
Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
➢Young Generation Space 新生区 Young/New
又被划分为Eden区和Survivor区
➢Tenure generation space 养老区 Old/Tenure
➢Meta Space 元空间 Meta
堆内存大小的设置与查看
Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,可以通过选项”-Xmx"和”-Xms"来进行设置。
“-Xms"用于表示堆区的起始内存,等价于-XX: InitialHeapSize
“-Xmx" 则用于表示堆区的最大内存, 等价于-XX :MaxHeapSize一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出OutOfMemoryError异常。
通常会将-Xms 和- -Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
1.设置堆空间大小的参数
-Xms. 用来设置堆空间(年轻代+老年代)的初始内存大小
-X是jvm的运行参数
ms是memory start
-Xmx用来设置堆空间(年轻代+老年代)的最大内存大小
2.默认堆空间的大小
初始内存大小:物理电脑内存大小/ 64
最大内存大小:物理电脑内存大小/ 4
3.手动设置: -Xms600m -Xmx600m
开发中建议将初始堆内存和最大的堆内存设置成相同的值。
4.查看设置的参数:
方式一(cmd): jps / jstat -gc进程id
方式二(编译器中设置): -XX: +PrintGCDetails
*OOM举例
public class OOMTest{
public static void main(String args[]){
ArrayList<Picture> list = enw ArrayList<>();
while(true){
list.add(new Picture(new Random().nextInt(1026*1024)));
}
}
}
//最后报出OOM
新生代与老年代
存储在JVM中的Java对象可以被划分为两类:
Java堆区进一步细分的话, 可以划分为年轻代(YoungGen) 和老年代(0ldGen)
其中年轻代又可以划分为Eden空间、Survivor0空间和Survivor1空间(有时也叫做from区、to区)
配置新生代与老年代在堆结构的占比。
默认-XX: NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
在同样内存下,占比少的部分GC也就更频繁。
在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1
当然开发人员可以通过选项“-XX: SurvivorRatio"调整这个空间比例。比如-XX: SurvivorRatio=8.
几乎所有的Java对象都是在Eden区被new出来的。
绝大部分的Java对象的销毁都在新生代进行了。
IBM公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的。
可以使用选项”-Xmn"设置新生代最大内存大小
这个参数一般使用默认值就可以了,一般开发中设置好了堆的大小和比例就等于是确定了新生代的大小。
ps:JVM规范中提到,新生代中各内存比例是8:1:1,但是实际运行中却是6:1:1.原因是其有一个自适应的内存分配策略(此策略是默认使用的)
可以手动设置"-XX: SurvivorRatio = 8"来实现8:1:1
-XX: -UseAdaptivesizePolicy :关闭自适应的内存分配策略( 暂时用不到)
对象分配的一般过程
为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。
针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to.
关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不在永久区/元空间收集。
本地方法(Native Method)
本地方法接口&本地方法库
简单地讲,一个Native Method就是一个Java调用非Java代码的接口。一个Native Method是这样-一个Java方法:该方法的实现由非Java语言实现,比如C。这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern "C"告知C++编译器去调用 一个c的函数。
"A native method is a Java method whose implementation is provided by non-java code."
在定义一个native method时, 并不提供实现体(有些像定义一个Javainterface),因为其实现体是由非java语言在外面实现的。本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。
Minor GC、Major GC、Full GC
JVM在进行GC时,并非每次都对,上面三个内存区域(指Eden s0 s1 )一起回收的,大部分时候回收的都是指新生代。
针对HotSpotVM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)
部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
新生代收集(Minor GC / Young GC) :只是新生代的垃圾收集
老年代收集(MajorGC/0ldGC):只是老年代的垃圾收集。
目前,只有CMS GC会有单独收集老年代的行为。
注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。
目前,只有G1 GC会有这种行为
整堆收集(Fu1l GC): 收集整个java堆和方法区的垃圾收集。
年轻代GC(Minor GC):
触发机制
当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GC。(每次 Minor GC会清理年轻代的内存。)
因为Java 对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。
Minor GC会引发STW, 暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。
老年代GC (Major GC/Fu1l GC)
触发机制:
指发生在老年代的GC,对象从老年代消失时,我们说“Major GC"或“Fu11 GC”发生了。
出现了Major GC,经常会伴随至少- -次的Minor GC (但非绝对的,在Paral1elScavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程)。
也就是在老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发Major GC
Major GC的速度- °般会比Minor GC慢10倍以上,STW的时间更长。
如果Major GC后,内存还不足,就报00M了。
Full GC
触发机制
(后面细讲)
触发Fu1l GC执行的情况有如下五种:
(1)调用System.gc()时,系统建议执行Fu11 GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、survivor space0 (From Space)区向survivor space1 (To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
说明: full gc是开发或调优中尽量要避免的。这样暂时时间会短一-些。
相关文章
- 暂无相关文章
用户点评