1.jvm,jre,jdk关系
java8 官网地址 docs.oracle.com/javase/8/do…
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.
JDK 8是JRE 8的超集,包含JRE 8中的所有内容,以及开发小程序和应用程序所需的工具,例如编译器和调试器。JRE 8提供了库,Java虚拟机(JVM)和其他组件,以运行用Java编程语言编写的小程序和应用程序。请注意,JRE包含Java SE规范不需要的组件,包括标准和非标准Java组件。
2.源码到类文件
2.1源码
public class Person{
private Integer age;
private String name;
private Integer getAge(){
return age;
}
private String getName(){
return name;
}
private void setAget(Integer age){
this.age=age;
}
private void setName(String name){
this.name=name;
}
}
javac Person.java -> Person.class
编译过程:词法分析->语法分析->语义分析和中间代码生成->优化->目标代码生成
2.2类文件
2.2.1魔数和版本号
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];
}
每个Class文件的头4个字节被称为魔数(Magic Number) , 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。
紧接着魔数的4个字节存储的是Class文件的版本号: 第5和第6个字节是次版本号(MinorVersion) , 第7和第8个字节是主版本号(Major Version)
ultraEdit打开Person.class文件第一行截图如下
由上图可以分析出,魔数的值为CAFEBASE,MinorVersion为0,Major Version 34的十进制为52,也就是java8
2.2.2常量池
紧接着主、 次版本号之后的是常量池入口, 常量池可以比喻为Class文件里的资源仓库
由上面Person.class文件可以看到 常量池容量是 001D,十进制为29
这个容量计数是从1开始的。如下图所示:常量池容量(偏移地址: 0x00000008) 为十六进制数0x0013,则十进制为19,则这里有18个长常量,索引范围为1-18,在Class文件格式规范制定之时, 设计者将第0项常量空出来是有特殊考虑的, 这样做的目的在于, 如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义, 可以把索引值设置为0来表示。 javap -verbose反解析 生成汇编指令,下面图片可以看到Person常量池数量28个
2.3类加载
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们的顺序如下图所示:
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。
2.3.1加载
”加载“是”类加机制”的第一个过程,在加载阶段,虚拟机主要完成三件事:
(1)通过一个类的全限定名来获取其定义的二进制字节流
(2)将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
(3)在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。
相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段,因为程序员可以使用系统的类加载器加载,还可以使用自己的类加载器加载。我们在最后一部分会详细介绍这个类加载器。在这里我们只需要知道类加载器的作用就是上面虚拟机需要完成的三件事,仅此而已就好了
2.3.2验证
验证的主要作用就是确保被加载的类的正确性。也是连接阶段的第一步。说白了也就是我们加载好的.class文件不能对我们的虚拟机有危害,所以先检测验证一下。
2.3.3准备
准备阶段主要为类变量分配内存并设置初始值。这些内存都在方法区分配。
(1)类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中,
(2)这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值。
2.3.4解析
解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
2.3.5初始化
这是类加载机制的最后一步,在这个阶段,java程序代码才开始真正执行。我们知道,在准备阶段已经为类变量赋过一次值。在初始化阶端,程序员可以根据自己的需求来赋值了。一句话描述这个阶段就是执行类构造器< clinit >()方法的过程。
在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
①声明类变量是指定初始值
②使用静态代码块为类变量指定初始值
JVM初始化步骤
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
- 创建类的实例,也就是new的方式
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(如 Class.forName(“com.shengsiyuan.Test”))
- 初始化某个类的子类,则其父类也会被
- 初始化Java虚拟机启动时被标明为启动类的类( JavaTest),直接使用 java.exe命令来运行某个主类好了
2.4类加载器
2.4.1java系统自带的三个类加载器
- Bootstrap ClassLoader:最顶层的加载类,主要加载核心类库,也就是环境变量下面 %JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
- Extention ClassLoader :扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
- Appclass Loader:也称为SystemAppClass。 加载当前应用的classpath的所有类。
2.4.2类加载器的层次关系
2.4.3 类加载的三种方式
(1)通过命令行启动应用时由JVM初始化加载含有main()方法的主类。
(2)通过Class.forName()方法动态加载,会默认执行初始化块(static{}),但是Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块。
(3)通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。
2.4.4 双亲委派原则
当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。
采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。双亲委派原则归纳一下就是:
- 可以避免重复加载,父类已经加载了,子类就不需要再次加载
- 更加安全,很好的解决了各个类加载器的基础类的统一问题,如果不使用该种方式,name用户随意定义类加载器加载核心api,会带来相关隐患
2.4.5自定义类加载器
定义类加载器主要有两种方式
(1)遵守双亲委派模型:继承ClassLoader,重写findClass()方法。
(2)破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。 通常我们推荐采用第一种方法自定义类加载器,最大程度上的遵守双亲委派模型。
2.5运行时数据区 (Run-Time Data Areas)
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.
Java虚拟机定义了在程序执行期间使用的各种运行时数据区域。 其中一些数据区域是在Java虚拟机启动时创建的,仅在Java虚拟机退出时才被销毁。 其他数据区域是每个线程的。 创建线程时创建每个线程的数据区域,并在线程退出时销毁每个数据区域。
2.5.1方法区
Java虚拟机具有一个在所有Java虚拟机线程之间共享的方法区域。该方法区域类似于常规语言的编译代码的存储区域,或者类似于操作系统过程中的“文本”段。它存储每个类的结构,例如运行时常量池,字段和方法数据,以及方法和构造函数的代码,包括用于类和实例初始化以及接口初始化的特殊方法。
方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但是简单的实现可以选择不进行垃圾回收或对其进行压缩。该规范没有规定方法区域的位置或用于管理已编译代码的策略。方法区域可以是固定大小的,或者可以根据计算的需要进行扩展,如果不需要更大的方法区域,则可以缩小。方法区域的内存不必是连续的。
Java虚拟机实现可以为程序员或用户提供对方法区域初始大小的控制,并且在方法区域大小可变的情况下,可以控制最大和最小方法区域大小。
以下异常条件与方法区域相关联:
- 如果无法提供方法区域中的内存来满足分配请求,则Java虚拟机将抛出一个
OutOfMemoryError。
这时候把从class文件到装载的第一步和第二步合并起来理解的下,如下图
方法区在jdk8中就是Metaspace,在jdk6或者7中就是Perm space
2.5.2运行时常量区
运行时间常数池是的每个类或每个接口的运行时表示constant_pool在表class文件。它包含几种常量,范围从编译时已知的数字文字到必须在运行时解析的方法和字段引用。运行时常量池的功能类似于常规编程语言的符号表,尽管它包含的数据范围比典型的符号表还大。
每个运行时常量池都是从Java虚拟机的方法区域分配的。当Java虚拟机创建类或接口时,将为该类或接口构造运行时常量池。
以下异常条件与类或接口的运行时常量池的构造有关:
- 创建类或接口时,如果运行时常量池的构造需要的内存超过Java虚拟机的方法区域中可用的内存,则Java虚拟机将抛出
OutOfMemoryError。
2.5.3 程序计数器
我们都知道一个jvm进程中有多个线程在执行,而线程的内容是否能够拥有执行权是根据cpu调度来的
假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获取CPU执行权的时候,怎么能继续执行呢?这需要在线程中维护一个变量,记录线程执行到的位置
程序计数器占用的内存空间很小,由于java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时间的方式来实现的,在任意时刻,一个处理器只会执行一条线程的指令,因此为了线程切换后能够恢复到正确的执行位置,每条线程需要有一个独立的程序计数器
如果不是 native,则该pc寄存器包含当前正在执行的Java虚拟机指令的地址。如果线程当前正在执行的方法是native,则Java虚拟机的pc 寄存器值未定义。
2.5.4堆
Java虚拟机具有一个在所有Java虚拟机线程之间共享的堆。堆是运行时数据区,从中分配所有类实例和数组的内存。
堆是在虚拟机启动时创建的。自动存储管理系统(称为垃圾收集器)可以回收对象的堆存储;对象永远不会显式释放。Java虚拟机不假定特定类型的自动存储管理系统,并且可以根据实现者的系统要求选择存储管理技术。堆的大小可以是固定的,也可以根据计算要求进行扩展,如果不需要更大的堆,则可以将其收缩。堆的内存不必是连续的。
Java虚拟机实现可以为程序员或用户提供对堆的初始大小的控制,并且,如果可以动态扩展或收缩堆,则可以控制最大和最小堆大小。
以下异常情况与堆关联:
- 如果计算需要的堆多于自动存储管理系统可以提供的堆,则Java虚拟机将抛出一个
OutOfMemoryError。
2.5.5虚拟机栈
Java虚拟机线程都有一个私有Java虚拟机堆栈,与该线程同时创建。Java虚拟机堆栈存储框架(第。Java虚拟机堆栈类似于常规语言(例如C)的堆栈:它保存局部变量和部分结果,并在方法调用和返回中起作用。因为除了推送和弹出框架外,从不直接操纵Java虚拟机堆栈,所以可以为堆分配帧。Java虚拟机堆栈的内存不必是连续的。
该规范允许Java虚拟机堆栈具有固定大小,或者根据计算要求动态扩展和收缩。如果Java虚拟机堆栈的大小固定,则在创建每个Java虚拟机堆栈时可以独立选择它们的大小。
Java虚拟机实现可以为程序员或用户提供对Java虚拟机堆栈初始大小的控制,并且在动态扩展或收缩Java虚拟机堆栈的情况下,可以控制最大和最小大小。
以下异常条件与Java虚拟机堆栈相关:
- 如果线程中的计算需要比允许的Java虚拟机更大的堆栈,则Java虚拟机将抛出
StackOverflowError。 - 如果可以动态扩展Java虚拟机堆栈,并尝试进行扩展,但是可以提供足够的内存来实现扩展,或者如果没有足够的内存来为新线程创建初始Java虚拟机堆栈,则可以使用Java虚拟机机器抛出一个
OutOfMemoryError。
Java虚拟机栈(Java Virtual Machine Stacks)是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:栈帧(Stack Frame)是用于支持Java虚拟机进行方法调用和执行的数据结构,它是虚拟机栈中的栈元素。每个方法在执行的同到都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟栈中从入栈到出栈的过程(说人话就是要执行一个方法,将该方法的栈帧压入栈顶,方法执行完成其栈帧出栈)。在JVM里面,栈帧的操作只有两种:出栈和入栈。正在被线程执行的方法称为当前线程方法,而该方法的栈帧就称为当前帧,执行引擎运行时只对当前栈帧有效
2.5.5.1局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时就在方法的code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
局部变量不像的类成员变量那样存在"准备阶段"。我们知道类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的初始值。因此,即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样,如果一个局部变量定义了但没有赋初始值是不能使用的,不要认为Java中任何情况下都存在诸如整型变量默认为0,布尔型变量默认为false等这样的默认值。
2.5.5.2 操作数栈
操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出(Last In First out,LIFO)栈。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写人和提取内容,也就是出栈/入栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。举个例子,整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存人了两个int型的数值,当执行这个指令时,会将这两个int值出栈并相加,然后将相加的结果入栈。
2.5.5.3 动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存人口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
Math math=new Math();
math.compute();//调用实例方法compute()
以上面两行代码为例,解释一下动态连接:math.compute()调用时compute()叫符号,需要通过compute()这个符号去到常量池中去找到对应方法的符号引用,运行时将通过符号引用找到方法的字节码指令的内存地址。
2.5.5.4方法的返回地址
当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。
无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压人调用者栈帧的操作数栈中,调整pc计数器的值以指向方法调用指令后面的一条指令等。
2.5.6本地方法堆栈
Java虚拟机的实现可以使用传统的堆栈(俗称“ C堆栈”)来支持native方法(以Java编程语言以外的语言编写的方法)。解释器的实现也可以使用诸如C之类的语言来解释Java虚拟机的指令集,以使用native 本机方法栈。无法加载方法并且自身不依赖于常规堆栈的Java虚拟机实现无需提供本机方法栈。如果提供,通常在创建每个线程时为每个线程分配本机方法堆栈。
- 如果线程中的计算需要比允许的更大的本机方法堆栈,则Java虚拟机将抛出
StackOverflowError。 - 如果可以动态扩展本机方法堆栈并尝试进行本机方法堆栈扩展,但可以提供足够的内存,或者可以提供足够的内存来为新线程创建初始本机方法堆栈,则Java虚拟机将抛出
OutOfMemoryError。