JVM-721-日记版

153 阅读16分钟

1-25不需要看

Java分成了 J2EE、J2SE、J2ME,这表明 Java开始向企业,桌面应用和移动设备应用3大领域挺进

Java HotSpot、JRockit、G9(一次编译,处处运行)

虚拟机

1、系统虚拟机:模拟操作系统的

2、程序虚拟机:运行程序的,Java虚拟机

JVM的架构模型

1、基于栈的指令集架构

2、基于寄存器的指令集架构

javap -v 类名.class

类加载子系统

image.png

类的加载过程一:Loading

1、通过一个类的全限定名获取定义此类的二进制字节流

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

3、在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口

xzh:将类加载到方法区,然后生成这个类的class对象

类的加载过程二:Linking

1、验证:目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。

主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

2、准备:为类变量分配内存并且设置该类变量的默认初始化值,即零值。

这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化

这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

3、解析:将常量池内的符号引用转换为直接引用的过程。

事实上解析操作往往会伴随着JVM在执行完初始化之后再执行。

符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。

类的加载过程三:Initialization

初始化:

初始化阶段就是执行类构造方法()的过程。

此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。

构造器方法中指令按语句在源文件中出现的顺序执行。

()不同于类的构造器。(关联:构造器是虚拟机视角下的())

若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。

虚拟机必须保证一个类的()方法在多线程下同步加锁。

idea插件:jclasslib Bytecode Viewer(查看class文件的二进制流)

类加载器的分类

JVM支持两种类型的类加载器,分别为引导类加载器自定义类加载器

引导类加载器:

1、C和C++编写,Java的核心类库都是使用引导类加载器进行加载的,比如String。

扩展类加载器:

1、从JDK的安装目录的jre/lib/ext子目录下加载类库,如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

系统类加载器:

1、对于用户自定义类来说,默认使用系统类加载器进行加载。

双亲委派机制

工作原理

1、如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。

2、如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。

3、如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子类加载器才会尝试自己去加载,这就是双亲委派模式。

xzh:当类加载器加载一个类时并不会自己去加载而是委托其父类加载器去加载,如果父类加载器还存在其父类加载器,最终会到达引导类加载器加载,如果这个父类加载器可以加载这个类,就成功返回,否则就向下委托。

优势

1、避免类的重复加载

2、保护程序安全,防止核心API被随意篡改

自定义类:java.lang.String

运行时数据区

image.png

image.png

程序计数器(PC寄存器)

image.png

作用

xzh:PC寄存器用来存放下一条指令的地址

PC寄存器两个面试问题

**1、使用PC寄存器存储字节码指令地址有什么用呢? **

**2、为什么使用PC寄存器记录当前线程的执行地址呢? **

xzh:因为CPU需要不停的切换线程,这个时候切换回来以后,就得知道接着从哪开始继续执行。

3、PC寄存器为什么被设置成线程私有的?

xzh:因为CUP在切换线程的时候,当切换回来以后就得知道从哪继续执行,如果是线程共享的,原来的值就会被切换过的线程值覆盖,当切换回来以后就不知道该从哪开始执行。

虚拟机栈(stack)

Java虚拟机栈是什么?

Java虚拟机栈,每个线程在创建的时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的Java方法调用。

线程私有的、没有GC有OOM、访问速度仅次于程序计数器

作用

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

栈的特点(优点)

1、栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。

2、JVM直接对Java栈的操作只有两个

每个方法执行,伴随着进栈(入栈、压栈) 执行结束后的出栈工作

xzh:先进后出

3、对于栈来说不存在垃圾回收问题

虚拟机栈的常见异常与如何设置栈大小

StackOverflowError

-Xss256k

image.png

栈帧的内部结构

每个栈帧中存储着:

局部变量表、操作数栈、动态链接、方法返回地址、一些附加信息

image.png

局部变量表

xzh:局部变量表,定义为一个数字数组,主要包括方法参数、定义在方法内的局部变量。

局部变量表的大小在编译期就已经确定下来了

image.png

image.png

变量槽solt的理解与演示

局部变量表,最基本的存储单位是solt(变量槽)

局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型。

在局部变量表里,32位以内的类型只占用一个slot,64位的类型(long和double)占用两个slot。

xzh:当访问局部变量表中一个64bit的局部变量值时候,只需要使用前一个索引即可。

xzh:如果当前帧是由构造方法或者实例方法创建的,那么该对象应用this将会存放在index为0的solt处。

Slot的重复利用

静态变量与局部变量的对比及小结

变量的分类:按照数据类型分:基本数据类型、引用数据类型

按照在类中声明的位置分:成员变量、局部变量

成员变量:

1、类变量:linking的准备阶段,给类变量默认赋值 ---> initial阶段:给类变量显示赋值即静态代码块赋值

2、实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值

局部变量:

在使用前,必须要进行显示赋值的。否则,编译不通过。

xzh:因为局部变量是存放在局部变量表中的,而局部变量表是一个数字数组,因此必须在使用前先赋值。

操作数栈

xzh:栈可以使用数组和链表来实现,也可以称为表达式栈

  • 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。

  • 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

  • 当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的

  • 其所需的最大深度在编译期就定义好了

  • 32bit的类型占用一个栈单位深度,64bit的类型占用两个栈单位深度

  • 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问。

涉及操作数栈的字节码指令执行分析

xzh:byte、short、char、boolean:都以int类型保存在局部变量表

image.png

动态链接的理解与常量池的作用

xzh:动态链接,指向运行时常量池的方法引用。

为什么需要常量池呢?

常量池的作用,就是为了提供一些符号和常量,便于指令的识别。

image.png

方法的绑定机制

在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。

  • 静态链接:

当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。

  • 动态链接:

如果被调用的方法在编译期无法被确定下来,也就是说,只能在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。

对应的方法的绑定机制为:早期绑定和晚期绑定。绑定是一个字段,方法或者类在符号应用被替换为直接引用的过程,这仅仅发生一次。

  • 早期绑定:

早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。

  • 晚期绑定:

如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。

4种方法调用指令区分非虚方法与虚方法

非虚方法:

如果方法在编译期就确定了具体调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。

静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。

其他方法称为虚方法。

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

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

  • invokevirual:调用所有虚方法

  • invokeinterface:调用接口方法

动态调用指令:

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

前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本,其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(fanal修饰的除外)称为虚方法。

invokedynamic指令的使用

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

动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。

说的再直白一点就是,静态类型语言是判断变量自身的类型信息:动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。

方法重写的本质与虚方法表的使用

Java语言中方法重写的本质

1、找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。

2、如果在过程结束:如果不同类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过,则返回java.lang.IllegalAccessError异常

3、否则,按照继承关系从下往上依次对C的各个父类进行第2步搜索和验证过程。

4、如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

IllegalAccessError介绍:

程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个引起编译器异常,这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。

虚方法表:

  • 在面向对象的编程中,会很频繁的使用到动态分配,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表来实现,使用索引表来代替查找。

  • 每个类中都有一个虚方法表,表中存放着各个方法的实际入口。

  • 那么虚方法表什么时候被创建?

虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准别完成之后,JVM会把该类的方发表也初始化完毕。

方法返回地址

  • 存放调用该方法的pc寄存器的值。

  • 一个方法的结束,有两种方式:

正常执行完成

出现未处理的异常,非正常退出

  • 无论通过哪种方式退出,在方法退出后都返回到该方法被调用额位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法指令的下条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

正常完成出口和异常完成出口的区别在于:通过异常完成出口退出 的不会给他的上层调用者产生任何的返回值

image.png

一些附加信息

栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。列如对程序调试提供支持的信息。

本地方法接口的理解

image.png

xzh:在Java中只有方法的申明,方法的实现使用其他语言实现(c和c++),本地方法使用native修饰。

本地方法栈的理解

  • Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。

  • 本地方法栈,也是线程私有的。

  • 允许被实现成固定或者是可动态扩展的内存大小(在内存溢出方面是相同的)

  • 本地方法是使用C语言实现的。

  • 他的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。

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

    本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。 它甚至可以直接使用本地处理器中的寄存器 直接从本地内存的堆中分配任意数量的内存

  • **并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法的使用语言、具体实现方式、数据结构等。**如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。

  • 在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。

  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。

  • Java堆区在JVM启动的时候即被创建,其空间大小也就确定了,是JVM管理的最大一块内存空间。(堆内存的大小是可以调节的)。

  • 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该视为连续的。

  • 所有的线程共享Java堆,在这里还可以划分线程私有的缓存区。

  • -Xms和Xmx,分别用来设置堆的最小值和最大值。

  • 所有的对象实例以及数组都应当在运行时分配在堆上。

  • 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。

  • 在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

image.png

堆的细分内存结构

  • Java 7及之前堆内存逻辑上分为三部分:新生区 + 养老区 + 永久区

  • Java 8及之后堆内存逻辑上分为三个部分:新生区 + 养老区 + 元空间

    Young Generation Space 新生区

    又被划分为Eden区和Survivor区
    

    Tenure generation space 养老区

    Permanent Space 养老区、Meta Space 元空间

    约定:新生区 = 新生代 = 年轻代 养老区 = 老年区 = 老年代

  • -XX:+PrintGCDetails 打印垃圾回收的一些细节