Java程序运行原理(JVM虚拟机笔记)

2024-10-12 19:14
469
1

Java程序运行原理

Java编译器(前端编译器,javac就是其中一种)把Java源代码编译成Java虚拟机识别的字节码class文件。

Java虚拟机就相当于一个字节码的执行引擎,Java虚拟机有多种,但都需要符合Java虚拟机规范。

Java虚拟机中主要采用解释编译

Java虚拟机规范》描述Java虚拟机的规范。

Java语言规范》描述Java语言的规范和特性。

Java语言和虚拟机规范官方文档:

https://docs.oracle.com/javase/specs/index.html

 

JVM运行时数据区

 

线程共享:所有线程能访问这块内存数据,随虚拟机或者GC而创建和销毁

线程独占:每个线程都会有它独立的空间,随线程生命周期而创建和销毁

 

虚拟机中垃圾回收,主要就是对线程共享部分进行回收

 

方法区

作用:存储加载的类信息、常量、静态变量、JIT编译后的代码等数据。(即存储的类的元信息)

JDK7中的永久代和方法区是否等价?JDK1.8方法区就没有了?

不等价,方法区是Java虚拟机规范中定义的。只是JDK7中通过永久代实现,JDK8中通过元数据区实现。

 

GC方法区存在垃圾回收,但回收频率低;

回收主要针对常量池的回收,和类型的卸载;

当方法区无法满足内存需求时,报OOM

 

类信息可以理解为Java反射中的XXX.class中的数据。

方法区中的类什么时候会被回收?

条件1class对应的实例都被回收了。

条件2:加载类的ClassLoder 被回收了。

条件3:类已经没有被其他实例引用。

 

存在方法区中的类信息有什么呢?

常量池、方法字节码指令、堆内存中的对象引用、其他

 

堆内存

作用:唯一的目的就是存放对象实例,几乎所有的对象、数组都在这儿离存放

堆内存中存的是实例字段(field、数组的元素

方法区存的是类信息、堆内存中存的是对象的数据。

对于大多数应用来说,堆是JVM管理的内存中最大的一块内存区域,也是最容易OOM的区域。

大多数JVM都将堆实现为大小可扩展的(通过-Xmx-Xms控制)

 

对新生代、老年代、永久代概念的解释:

有的概念中会说堆内存中分为新生代和老年代,方法区有有永久代。

方法区、堆内存是Java虚拟机规范中的概念。

而新生代和老年代是Hotspot实现垃圾回收(GC)算法时提出的概念。

永久代也是方法区实现中提出的一些概念,JDK1.7实现方法区用的是永久代,JDK1.8用的是元数据区。

 

 

堆中的对象什么时候被回收?怎么知道堆中的对象不再被使用了

 

对象释放

 

怎么判断对象可以被释放?

1、可以引用计数器算法,在对象中保存一个计数器记录引用数,但存在两个对象相互引用的问题。(所以JavaC#里都不是通过引用计数器实现的)

2通过可达性分析算法:主流的商用程序语言(JavaC#)都是通过可达性分析算法来判定对象是否存活的。

如下图,判断对象通过引用是否可达Gc roots,如果不可达则回收,可达则保留。

GC Roots可以是:

  1. 虚拟机栈;
  2. 方法区中静态属性引用的对象;
  3. 方法区中常量引用的对象;
  4. Native方法引用的对象。

 

然后对线程独占的的内存区域进行分析,里面有什么?

虚拟机栈

虚拟机栈:线程中Java方法执行的模型每个方法执行时,就会在虚拟机栈中创建一个栈帧每个方法从调用到执行的过程,就对应着栈帧在虚拟机栈中从入栈到出栈的过程。

栈帧:虚拟机栈由多个栈帧(Stack Fframe)组成。

一个线程会执行一个或多个方法,一个方法对应一个栈帧

怎么理解虚拟机栈是线程中方法执行的模型?

比如:主线程中的main方法、线程new Thread执行的run方法,通俗一点就是线程在执行一个个方法。所以也可以理解为虚拟机栈就是一个线程执行的模型。

 

所以虚拟机栈是怎么运行的?

比如一个简单的Java程序,先执行main方法,然后main方法中调用了test方法,那么虚拟机栈中会先入栈main方法的栈帧,然后继续入栈test方法栈帧。然后test方法执行完后,test栈帧出栈,然后main方法栈帧出栈。

 

如上也能发现,通过递归方式循环调用方法存在弊端,会让虚拟机栈消耗过多资源。

所以尽量不用递归,递归的逻辑一般都可以通过while实现。

 

局部变量先是存在方法区中的一个字节码指令,方法使用时,方法栈帧加入虚拟机栈中执行才给局部变量赋值(八大基本类型的默认初值是在方法区中),先把初值放入操作数栈,然后赋值给局部变量表中对应变量。

 

栈帧中还有什么?

  • 局部变量表
  • 操作数栈:用于字节码指令的执行时对数据的操作。  
  • 动态链接
  • 返回值地址
  • 其他信息

https://www.cnblogs.com/jhxxb/p/11001238.html

指令的参数必须来自操作数栈,而不能来之局部变量表。指令的执行结果也要先放入操作数栈,然后再存入局部变量表。设计就是这样。

以前刚学的时候老师所说的堆栈,栈可能指的就是局部变量表。

 

动态链接,链接的是什么?

方法区中存了类的信息,类的信息里当然包含有方法的信息(方法的二进制字节码指令),动态链接就是可以帮助找到此方法的二进制字节码指令。执行方法其实在虚拟机中就是执行这些二进制字节码指令。

返回值地址的作用?

方法执行完后出栈,返回值地址告诉当前方法,出栈后返回到哪里去(上一个方法里)。

 

本地方法栈

本地方法栈:和虚拟机栈功能类似,虚拟机栈是为虚拟机执行Java方法而准备的,本地方法栈是为虚拟机使用Native本地方法而准备的。

Java虚拟机规范》中对Native方法使用的语言、使用方式与数据结构没有强制规定。

HotSpot虚拟机中虚拟机栈和本地方法栈合二为一。

 

程序计数器

程序计数器(Program Counter Register)记录当前线程执行字节码的位置,字节码解释器工作时,就是通过改变计数器的值来选取下一条需要执行的字节码指令。

如果执行Java方法,存储的是字节码指令地址,如果执行native 方法,则计数值为空。

程序计数器是唯一一个在《JVM规范》中没有任何OOM的区域。

 

程序计数器用来记录方法中的字节码指令执行到哪个位置了。

程序计数器记录的是字节码指令的内存地址。

为什么要记录?

因为多线程中CPU会切换线程。

 

直接内存(不属于JVM内存区域)

直接内存不是JVM运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域,是我们常说的堆外内存。

JDK1.4引用NIO,它可以使用Native函数库,直接分配堆外内存,然后通过一个存在Java堆中德尔DirectByteBuffer对象作为操作这块内存区域的引用进行操作。

直接内存不受Java堆大小限制,当内存总和大于及其物理内存时,会OOM

 

JIT 即时编译

https://www.jianshu.com/p/169d6a50284a

 

Java类文件结构

示例代码:思考这一段代码,如何在JVM中执行?

public class Demo1 {

    final int NUMBER 20;
    private static Student stu new Student();

    public static void main(String args[]){
        int x = 500;
        int y = 100;
        int a = x/y;

        String envName = "JAVA_HOME";
        stu.age = a;

        String path = System.getenv(envName);
    }
}

 

class文件编译之后,可以通过javap -v -p Demo1.class class反编译出来,查看其中的指令。

 

Class文件结构

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4..3.2

class文件包含JAVA程序执行的字节码;数据严格按照格式紧凑排列在class文件中的二进制流(16进制来描述,如下图),中间无任何分隔符;u1u2u44个字节的数字)、u8都是无符号数(数字),info结尾都是表,要搞懂这个PPT,可以看《深入理解Java虚拟机》第六章。

看右边的表,u4表示4个字节的数字,对应magic;(魔数),左边前4个字节 ca fe ba be(咖啡贝比),也就是Java虚拟机会首先看前面4个字节来识别是否为虚拟机支持的class类文件。

后面2个字节就是小版本号 minor_version

继续2个字节就是主版本号,00 34 转换成10进制是52,是JDK的版本号。JDK1.6501.7511.852

继续又是个u2,constant_pool_count,表示常量池的count,左上图是 00 1b转换10进制是27

继续cp_info表示具体的常量,constant_pool_count值有多少,后面就会接上这些常量。这些常量占多少个字节类似如下图表所示,这里只有部分。

TIP:常量池不只是Java代码里定义的常量,类名称、方法名称等都会存在常量池中。

 

access_flags:访问标记符,有类的访问修饰符,还有方法的访问修饰符,如下图表所示类的访问标志:

如果是public final则把上图中两个数字相加。

 

this_class:当前类的名字(值是一个常量池的常量,CONSTANT_Class_info型常量的结构)

super_class:父类的名字

interface_count:实现的接口数

interface:接口数有多少个,后面会有多少个接口也是CONSTANT_Class_info型常量的结构

fields_count:字段数

fields:字段表结构,如下图:

methods_count:方法数

methods:方法

 

名词解释

全限定名:把类名中的.替换成/,例如com.cyx替换为com/cyx

简单名称:是指没有类型和参数修饰的方法或者字段,如public int test(){},简单名称就是inc, public int age字段的简单名称就是age

描述符:比较复杂,描述符的偶用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。如“int[]”将被标记为“[Ijava.lang.String[][] ,被标记为“[[Ljava/lang/String;;java.lang.StringtoString() 的描述符为()Ljava/lang/String

 

属性表集合

Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

不像其他表,属性表的排列没有严格的顺序,只要不与已有属性名重复即可,

任何人实现的编译器都可以向属性表中写入自己的属性信息,但JVM只认识自己预定义的21种属性,JVM会忽略不认识的属性。

详见《深入理解Java虚拟机》P180 6-13

 

class文件运行

 

一个OjbectJVM所占空间多大?

64位操作系统,开启指针压缩,指针头16个字节,开启指针压缩12字节,

然后会加上Java8个基本类型:boolean(1)byte(1)short(2)char(2)int(4)float(4)long(8)doubble(8)

即:12+1+1+2+2+4+4+8+8=42个字节。然后会进行字节对齐,填充6位,42+6=48字节。

 

全部评论