JVM——Java虚拟机,是Java实现平台无关性的基石。
基本概念:JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、 一个垃圾回收,堆 和 一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接 的交互。
Java程序运行的时候,编译器将Java文件编译成平台无关的Java字节码文件(.class),接下来对应平台JVM对字节码文件进行解释,翻译成对应平台匹配的机器指令并运行。
同时JVM也是一个跨语言的平台,和语言无关,只和class的文件格式关联,任何语言,只要能翻译成符合规范的字节码文件,都能被JVM运行。
JVM通过类的加载机制将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
通过双清委派机制,避免了类的重复加载,保护程序的安全性,防止核心的API被修改。
JVM原理:通过类加载器(启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)|平台类加载器(Platform ClassLoader)、应用程序类加载器(Application ClassLoader)、自定义类加载器(User ClassLoader))及其双亲委派机制,经过加载、验证、准备、解析、初始化、使用、卸载等步骤,实现java类加载到虚拟机内存、转换为二进制执行,到卸载出内存。
JDK>JRE>JVM
1.8同1.7相比,最大的差别就是元空间取代了永久代。元空间的本质和永久代类似,都是堆JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不存在虚拟机中,而是使用本地内存。
JAVA1.7内存模型 JAVA1.8内存模型 JVM内存分为线程私有区和线程共享区,其中方法区
和堆
是线程共享区,虚拟机栈
、本地方法栈
和程序计数器
是线程隔离的数据区。
JVM 中的程序计数寄存器(Program Counter Register)中的 Register 命名源于 CPU 的寄存器,寄存器存储指令相关的现场信息。CPU 只有把数据装载到寄存器才能运行。这里,并非是广义上所指的物理寄存器,或许将其翻译为 PC 计数器(或指令计数器) 会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM 中的 PC 寄存器是对物理 PC 寄存器的一种抽象模拟。它是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。 正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。
程序计数器用来存储下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。每个线程启动的时候,都会创建一个PC寄存器。 PC寄存器的内容总是指向下一条将被执行指令的地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。
Java虚拟机栈(Java Virtual Machine Stack),和程序计数器一样,也是线程私有的,它的生命周期和线程相同。
由于跨平台性的设计,Java 的指令都是根据栈来设计的。不同平台 CPU 架构不同,所以不能设计为基于寄存器的,基于栈的指令设计优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样功能需要更过的指令集。
栈是运行时的单位
堆是存储的单位。
即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
堆解决的是数据存储的问题,即数据怎么放,放在哪儿。
主管 Java 程序的运行,保存方法的局部变量(8 种基本数据类型,对象的引用地址),部分结果,并参与方法的调用和返回。
虚拟机栈是每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每调用一个方法会创建一个栈帧(Stack Frame),栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。当栈调用深度大于JVM所允许的范围,会抛出StackOverflowError的错误,不过这个深度范围不是一个恒定的值。
每个方法被调用和完成的过程,都对应一个栈帧从虚拟机栈上入栈和出栈的过程。虚拟机栈的生命周期和线程是相同的。
虚拟机栈是一个后入先出(FILO)的栈。栈帧是保存在虚拟机栈中的,栈帧是用来存储数据和存储部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。线程运行过程中,只有一个栈帧是处于活跃状态,称为“当前活跃栈帧”,当前活动栈帧始终是虚拟机栈的栈顶元素。
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
每个栈帧中存储着:
局部变量表(Local Variable Table)是一组局部变量值存储空间,用于存放方法参数和方法内部定义的局部变量。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量, 则存的是指向对象的引用。
局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。
在Java文件编译为Class文件时,就在方法表的Code属性的max_locals数据项中确定了该方法需要分配的最大局部变量表的容量。(最大Slot数量)
一个局部变量可以保存一个类型为boolean、byte、char、short、int、float、reference和returnAddress类型的数据。reference类型表示对一个对象实例的引用。returnAddress类型是为jsr、jsr_w和ret指令服务的,目前已经很少使用了。
虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量。如果Slot是32位的,则遇到一个64位数据类型的变量(如long或double型),则会连续使用两个连续的Slot来存储。
操作数栈(Operand Stack)也常被称为操作栈,它是一个后入先出栈(LIFO)。JVM底层字节码指令集是基于栈类型的,所有的操作码都是对操作数栈上的数据进行操作,对于每一个方法的调用,JVM会建立一个操作数栈,以供计算使用。
和局部变量一样,操作数栈的最大深度也是编译的时候写入到方法表的code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long、double。
32位数据类型所占的栈容量为1,
64位数据类型所占的栈容量为2。
栈容量的单位为“字宽”,
对于32位虚拟机来说,一个“字宽”占4个字节,
对于64位虚拟机来说,一个“字宽”占8个字节。
栈最典型的一个应用就是用来对表达式求值。主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。
当一个方法开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是出栈和入栈操作。程序中的所有计算过程都是在借助于操作数栈来完成的。
例如整数加法(2+3)的字节码指令iadd,这条指令在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入了int类型的数据,当执行这个指令时,会把这两个int值出栈并相加,然后将相加的结果重新入栈。
在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的,但是大多数虚拟机的实现里都会作一些优化处理,令两个栈帧出现一部分重叠。让下栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用返回时就可以共用一部分数据,而无须进行额外的参数复制传递了。
将符号引用转化为直接引用的过程。
因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义地定位到目标即可。
直接引用:直接引用可以是直接指向目标的指针,也可以是能间接定位到目标的句柄,还可以是相对偏移量。
每个栈帧都包含一个指向运行时常量池(在方法区中,后面介绍)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。
当一个方法被执行后,有两种方式退出这个方法:
第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法方式称为正常完成出口(Normal Method Invocation Completion)。
另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口(Abrupt Method Invocation Completion)。
一个方法使用异常完成出口的方式退出,是不会给它的调用都产生任何返回值的。
无论采用何种方式退出,在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。 方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与高度相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其它附加信息全部归为一类,称为栈帧信息。
堆(Heap)是线程共享的内存区域,是虚拟机管理内存中最大的一块,是一种常规用途的内存池(也在RAM(随机存取存储器 )区域)。此区域的唯一目的就是存放对象实例,Java世界里几乎所有的对象实例都在这里分配内存。当堆中没有内存分配给对象实例时,会抛出OutOfMemoryError。
所有的类对象都是通过new方法创建,创建后,在stack(栈)会创建类对象的引用(内存地址)。
“内存堆”或“堆”最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用new命令编辑相应的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间。
JVM将所有对象的实例(即用new创建的对象)(对应于对象的引用(引用就是内存地址))的内存都分配在堆上,堆所占内存的大小由-Xmx指令和-Xms指令来调节。
Java堆是垃圾收集器管理的内存区域,也被称作“GC堆”(Garbage Collected Heap)。Heap堆区又分为新生代和老年代,Heap堆区是垃圾收集器GC管理的主要区域。从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆中经常会出现新生代、老年代、Eden空间、From Survivor空间、To Survivor空间等名词,需要注意的是这种划分只是根据垃圾回收机制来进行的划分,不是Java虚拟机规范本身制定的。
堆内存模型从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
- Eden 区 是Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老 年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行 一次垃圾回收。
- ServivorFrom 是上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
- ServivorTo 保留了一次 MinorGC 过程中的幸存者。
- 把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年 龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不 够位置了就放到老年区)。
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
本地方法栈是为虚拟机使用到的本地Native方法服务。是一个后入先出(LIFO)栈。
由于是线程私有的,生命周期随着线程,线程启动而产生,线程结束而消亡。
本地方法栈会抛出 StackOverflowError 和 OutOfMemoryError 异常。
Java 虚拟机规范允许本地方法栈被实现成固定大小的或者是根据计算动态扩展和收缩的。
JVM的底层实际上使用了很多C语言的函数库,Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。
一个线程可能在整个生命周期中都执行Java方法,操作它的Java栈;或者它可能毫无障碍地在Java栈和本地方法栈之间跳转。
例如:
该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。假设这是一个C语言栈,其间有两个C函数,第一个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过本地方法接口回调了一个Java方法(第三个Java方法),最终这个Java方法又调用了一个Java方法(它成为图中的当前方法)。
方法区(method)又叫静态区,即常说的永久代(Permanent Generation),是线程共享的内存区域。
用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。所有的类(class),静态变量(static变量),静态方法,常量和成员方法都存放在此。比如spring 使用IOC或者AOP创建bean时,或者使用cglib,反射的形式动态生成class信息等)。
它有一个别名叫Non-Heap。JDK1.8元空间是方法区的实现。
方法区的大小由-XX:PermSize和-XX:MaxPermSize来调节,类太多有可能撑爆永久代。静态变量或常量也有可能撑爆方法区。
HotSpot VM把GC分代收集扩展至方法区, 即使用Java 堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存, 而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型 的卸载, 因此收益一般很小)。
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版 本、字段、方法、接口等描述等信息外,还有一项信息是常量池 ,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。 Java 虚拟机对 Class 文件的每一部分(自然也包括常量 池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会 被虚拟机认可、装载和执行。
Java中的字节码需要数据支持,通常这种数据很大以至于不能直接存到字节码里面,所以就存到常量池,而字节码文件存储的就是指向常量池的引用,在动态链接的时候会用到运行时常量池。
在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池表的运行时表示形式,在类或接口被加载到JVM后,对应的运行时常量池就被创建出来,常量池表的字面量与符号引用就会放到运行时常量池。
该区域存放类和接口的常量,除此之外,它还存放成员变量和成员方法的所有引用。当一个成员变量或者成员方法被引用的时候,JVM就通过运行常量池中的这些引用来查找成员变量和成员方法在内存中的的实际地址。
不仅是运行时常量池,常量池一般分为三个:Classs常量池,运行时常量池和字符串常量池。
我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References); 每个class文件都有一个class常量池。
字面量包括:
- 文本字符串
- 八种基本类型的值
- 被声明为final的常量等;
符号引用包括:
- 类和方法的全限定名
- 字段的名称和描述符
- 方法的名称和描述符。
运行时就是class常量池被加载到内存之后的版本。它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用。
JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。
字符串常量池又称为:字符串池,全局字符串池,英文也叫String Pool。 在工作中,String类是我们使用频率非常高的一种对象类型。JVM为了提升性能和减少内存开销,避免字符串的重复创建,其维护了一块特殊的内存空间,这就是我们今天要讨论的核心:字符串常量池。字符串常量池由String类私有的维护。(享元模式的一种体现)
在JDK7之前字符串常量池是在永久代里边的,但是在JDK7之后,把字符串常量池分进了堆里边。
堆里边的字符串常量池存放的是字符串的引用或者字符串(两者都有)
字符串池的实现有一个前提条件:String对象是不可变的。因为这样可以保证多个引用可以同时指向字符串池中的同一个对象。如果字符串是可变的,那么一个引用操作改变了对象的值,对其他引用会有影响,这样显然是不合理的。
在Java中两种创建字符串对象的方式:
- 采用字面值的方式赋值
- 采用new关键字新建一个字符串对象。这两种方式在性能和内存占用方面存在着差别。
采用字面值的方式创建一个字符串时,JVM首先会去字符串池中查找是否存在这个对象,如果不存在,则在字符串池中创建这个对象,然后将池中对象的引用地址返回给字符串常量,这样字符串会指向池中的这个字符串对象;如果存在,则不创建任何对象,直接将池中对象的地址返回,赋给字符串常量。
采用new关键字新建一个字符串对象时,JVM首先在字符串常量池中查找有没有这个字符串对象,如果有,则不在池中再去创建这个对象了,直接在堆中创建一个字符串对象,然后将堆中的这个对象的地址返回赋给引用,这样,字符串就指向了堆中创建的这个字符串对象;如果没有,则首先在字符串常量池池中创建一个字符串对象,然后再在堆中创建一个字符串对象,然后将堆中这个字符串对象的地址返回赋给引用,这样,字符串指向了堆中创建的这个字符串对象。
字符串池的优点就是避免了相同内容的字符串的创建,节省了内存,省去了创建相同字符串的时间,同时提升了性能;另一方面,字符串池的缺点就是牺牲了JVM在常量池中遍历对象所需要的时间,不过其时间成本相比而言比较低。
元数据区也叫元空间,是方法区的一种实现。存储的是类的元数据信息。在 Java8 中,永久代已经被移除,被“元数据区”(元空间)所取代。元空间的本质和方法区/永久代类似。
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制。
去永久化的原因:
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难。
- 静态区中类太多有可能撑爆永久代。静态变量或常量也有可能撑爆方法区。
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
还可以设置两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
相比永久代有撑爆的风险,元空间的生命周期与类加载器一致,类加载的时候进行空间分配,就可以更好的控制存储空间。
直接内存并不是虚拟机运行时数据区的一部分。但是这部分内存也经常被使用到,也有可能导致OutOfMemoryError出现。
在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(channel)与缓冲区(Buffer)的I/O方式,它可以用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,避免了在Java堆和Native堆中来回复制数据。