JVM探究

如果希望在java领域研究的更深入,则jvm是绕不过去的理论体系,接下来我们就从官网开始,对其进行一下探究学习。

01 官网

1.1 从JDK8开始

https://docs.oracle.com/javase/8/

1.2 jdk,jre,jvm三者关系

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

打开此官网连接页,很清晰的介绍了三者之间的关系

Oracle has two products that implement Java Platform Standard Edition (Java SE) 8: Java SE Development Kit (JDK) 8 and Java SE Runtime Environment (JRE) 8.

JDK 8 is a superset of JRE 8, and contains everything that is in JRE 8, plus tools such as the compilers and debuggers necessary for developing applets and applications. JRE 8 provides the libraries, the Java Virtual Machine (JVM), and other components to run applets and applications written in the Java programming language. Note that the JRE includes components not required by the Java SE specification, including both standard and non-standard Java components.

02 源码文件编译成类文件

2.1源码编译

javac HelloWord.java –> HelloWord.class

1
2
3
4
5
6
7
8
9
cafe babe 0000 0034 0027 0a00 0600 1809
0019 001a 0800 1b0a 001c 001d 0700 1e07
001f 0100 046e 616d 6501 0012 4c6a 6176
612f 6c61 6e67 2f53 7472 696e 673b 0100
0361 6765 0100 0149 0100 0761 6464 7265
7373 0100 0568 6f62 6279 0100 0d43 6f6e
7374 616e 7456 616c 7565 0800 2001 0006
......

2.2类文件(.class文件)

官网https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

对class文件的格式做了定义解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

03类加载机制

3.1装载

​ 类加载器ClassLoader来加载类

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

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

​ 3.在java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

3.2链接(Link)

3.2.1验证

保证被加载类的正确性

​ 文件格式验证

​ 元数据验证

​ 字节码验证

​ 符号引用验证

3.2.2准备

为类的静态变量分配内存,并将其初始化为默认值

3.2.3解析

把类中符号引用转化成直接引用

3.3初始化(Initialize)

对类的静态变量,静态代码块执行初始化操作

mark

04类加载器(ClassLoader)

4.1分类

mark

4.2加载机制

一个类的加载检查顺序自下往上进行,从上图的Custom ClassLoader到BootStrap ClassLoader逐层check,只要某个ClassLoader已加载,就视作已加载此类,保证此类只被上述ClassLoader加载一次

双亲委派机制

定义:如果一个类加载器在接收到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父亲加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终达到顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回假若父类加载器无法完成此加载任务则向下让子加载器才会尝试自己去加载,这就是双亲委派机制。

大白话:每个儿子都很懒,每次干活都抛给父亲去干,直到父亲说干不了,儿子才自己想办法去完成。

优点:采用双亲委派机制好处是java类随着它的类加载器一起具备一种带有优先级的层次关系,通过这种层级关系避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

安全因素,java核心api中定义的类型不会被随意替换,例如java.lang.Integer的类,通过双亲委派模式传递到启动类加载器,而启动类加载器在核心java API中发现有这个名字的类,发现该类已经被加载,并不会重新加载这个java.lang.Integer,而直接返回已贾在国的Integer.class,这样变可以防止核心API库被随意的篡改。

双亲委派机制的破坏:破坏方法有多种

如可以继承ClassLoader。然后重写其中loadClass方法。

05 运行时数据区(Run-Time Data Areas)

在装载的步骤中

  • 将这个字节流所代表的静态存储结构转化成方法区的运行时数据结构
  • 在java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

大白话就是类文件被类加载器加载进来之后,类中的内容(变量,常量,方法,对象等)这些数据要有一个生活环境,也就是存储起来,存储的位置肯定是在jvm中对应的空间

5.1官网定义

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

The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits. Other data areas are per thread. Per-thread data areas are created when a thread is created and destroyed when the thread exits.

示意图:

mark

5.2概念大概理解

5.2.1 Method Area(方法区)

方法区是各个线程共享的内存区域,在虚拟机启动的时候创建

用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名Non-Heap(非堆),目的是与java堆区分开来

当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常

其它说明

1)方法区在JDK8中就是Metaspace,在JDK6或者7中就是Perm Space 永久代

Jdk1.6及以前:有永久代,常量池1.6在方法区

​ Jdk1.7:有永久代,但已经逐步去永久代,常量池1.7在堆

Jdk1.8及以后:无永久代,常量池1.8在元空间

2)Run-Time Constant Pool 运行时常量池

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放

官网中运行时常量池描述:
Each run-time constant pool is allocated from the Java Virtual Machine’s method area (§2.5.4). The run-time constant pool for a class or interface is constructed when the class or interface is created (§5.3) by the Java Virtual Machine

5.2.2 Heap(堆)

java堆是java虚拟机所管理内存中最大的一块,在虚拟机启动时创建,被所有线程锁共享

java对象实例以及数组都在堆上分配

5.2.3 Java Virtual Machine Stacks(虚拟机栈)

类加载过程完成,后续怎么使用呢?如通过main函数调用其他方法,这种方式实际上是mian线程执行之后调用的方法,即要想使用里面的各种内容,得要以线程为单位,执行相应的方法才行。

那么一个线程执行的状态如何维护?一个线程可以执行多少个方法?这样的关系怎么维护呢?

虚拟机栈是线程执行的区域,保存着一个线程中方法的调用状态。

一个java线程运行状态有一个虚拟机栈来保存,所以虚拟机栈是私有的,随着线程的创建而创建。

每一个被线程执行的方法,为改栈中的栈帧,即每个方法对应一个栈帧。

调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。

5.2.4 Native Method Stacks(本地方法栈)

如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行

5.2.5 The pc Register(程序计数器)

一个JVM进程中有多个线程在执行,而线程中内容是否能够拥有执行权是有CPU来调度的

如线程A正在执行到某个地方,突然失去了CPU的执行权限,切换到线程B了,然后线程A再次获得CPU的执行权的时候怎么从上次执行位置继续执行呢?这就需要再线程中维护一个变量记录线程的执行到的位置

程序计数器占用的内存空间很小,java虚拟机的多线程是通过线程切换,并分配处理器执行时间的方式来实现的,在任意时刻,一个处理器置灰执行一条线程中指令。因此在线程切换后能够恢复到正确的位置继续执行,每条线程需要一个独立的程序计数器

线程计数器是线程私有的。

如果线程正在执行java方法,则计数器记录的是正在执行的虚拟机字节码执行的地址

如果正在执行的是Native方法,则这个计数器为空。

未完待续