一.虚拟机介绍
JVM是Java VirtualMachine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。
二.虚拟机内存模型
2.1 方法区(method area)
方法区是所有线程共享的,它是在JVM启动的时候创建的。它保存所有被JVM加载的类和接口的运行时常量池,成员变量以及方法的信息,静态变量以及方法的字节码。JVM的提供者可以通过不同的方式来实现方法区。在Oracle 的HotSpot JVM里,方法区被称为永久区或者永久代(PermGen)。是否对方法区进行垃圾回收对JVM的实现是可选的。
2.2 虚拟机栈(VM stack )
每个线程启动的时候,都会创建一个JVM线程栈。它是用来保存栈帧的。JVM只会在JVM线程栈上对栈帧进行push和pop的操作。如果出现了异常,线程栈跟踪信息的每一行都代表一个栈帧立的信息,这些信息是通过类似于printStackTrace()这样的方法来展示的。
2.3.本地方法栈(Native methodstack)
供用非Java语言实现的本地方法的堆栈。换句话说,它是用来调用通过JNI(JavaNative Interface Java本地接口)调用的C/C++代码。根据具体的语言,一个C堆栈或者C++堆栈会被创建。
2.4 堆(heap)
用来保存实例或者对象的空间,而且它是垃圾回收的主要目标。当讨论类似于JVM性能之类的问题时,它经常会被提及。JVM提供者可以决定怎么来配置堆空间,
2.5 程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)
参数设置
-Xms设置堆的最小空间大小。
-Xmx设置堆的最大空间大小。
-XX:NewSize设置新生代最小空间大小。
-XX:MaxNewSize设置新生代最大空间大小。
-XX:PermSize设置永久代最小空间大小。
-XX:MaxPermSize设置永久代最大空间大小。
-Xss设置每个线程的堆栈大小。
三.类加载
虚拟机吧描述类的数据从Class文件加载到内存,并对数据进行校验转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机类加载机制.
3.1 类加载时期
按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下 7 个阶段
其中,加载,验证,准备,初始化,卸载几个步骤是固定的必须按照这个步骤开始,但是解析阶段是不一定的,某些情况下在初始化阶段后在进行解析,这是为了支持java语言的运行时绑定。
加载阶段虚拟机规范中并没有强制约束,由虚拟机具体实现控制。初始化阶段则有明确规定,以下四种情况必须进行初始化
1.使用new关键字实例化对象时,读取或设置一个静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外),以及调用类的静态方法的时候
2.使用java.lang.reflect包的方法对类进行反射调用时,如果类没有进行初始化则需要先触发初始化一次
3.初始化一个类的时候,发现其父类没有初始化,则其父类需要进行一次初始化
4.虚拟机启动的时候,需要指定一个包含main方法的类,虚拟机会先初始化这个类
3.2 类加载过程
加载
加载阶段,虚拟机完成以下三件事
1.通过一个类的全限定名来获取定义此类的二进制字节流
2.将这个字节流代表的静态存储结构转化为方法去运行时数据结构
3.在java堆中生成一个代表这个类的Class对象,作为方法区这些数据访问的一个入口
验证
验证是为了保证Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全
验证阶段分为以下四个过程
1.文件格式验证
2.元数据验证
3.字节码验证
4.符号引用验证
准备
准备阶段是正式为类变量分配内存设置初始值的阶段,这些内存都将在方法去进行分配,需要强调的是这个时候进行内存分配仅包括被static修饰的变量不包括实例变量,实例变量会在对象初始化的时候在java堆中进行分配
例如
public static int value = 123
在准备阶段过后被赋予的初始值是0而不是123,value=123需要到初始化阶段才会被执行
解析
解析过程就是虚拟机将常量池中的符号引用转化为直接引用的过程
符号引用和直接引用区别
1.符号引用
符号引用以一组符号来描述引用的目标,符号可以是任意形式的字面量,只要使用时能无歧义的定位到目标即可
2.直接引用
直接引用时可以直接指向目标的指针,偏移量,或是一个能直接定位到目标的句柄
例子
char b='a';
a是在内存中的常量池中一个字符,它的引用名为b,转换为的Ascll码为:97,这里说的97就是字符a在常量池中的直接引用。
解析主要针对类或接口,字段,类方法,接口方法四类引用进行,分别对应常量池的CONSTANT_class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info
初始化
把类中的变量初始化成合适的值。执行静态初始化程序,把静态变量初始化成指定的值。
3.3 类加载器
四.运行时数据区
4.2 虚拟机栈
4.2.1 栈结构
4.2.2 栈帧
4.3 虚拟机堆
4.4 方法区
4.5常量池
4.5.1 常量池结构
1.魔数
4个字节,jvm识别class文件标识
2.版本号
4字节,版本号分主版本号(2字节)和次版本号(2字节),jdk8对应主版本号是52,jdk9是53,jdk10是54,jdk11是55
3.常量池
常量池分常量池计数器(2字节)和常量池数据区(长度不固定),其中数据区是个cp_info数组,计数器统计数据长度
4.5.2 常量池数据区
4.5.3 常量池数据项
JVM虚拟机规定了不同的tag值和不同类型的字面量对应关系如下:
定义类型后的常量池如下图
4.5.4 常量池CP_INFO
int和float数据类型的常量在常量池中是怎样表示和存储的?
Java语言规范规定了 int类型和Float类型的数据类型占用 4 个字节的空间。那么存在于class字节码文件中的该类型的常量是如何存储的呢?相应地,在常量池中,将 int和Float类型的常量分别使用CONSTANT_Integer_info和 Constant_float_info表示,他们的结构如下所示:
举例:建下面的类 IntAndFloatTest.java,在这个类中,我们声明了五个变量,但是取值就两种int类型的10 和Float类型的11f。
public class IntAndFloatTest {
private final int a = 10;
private final int b = 10;
private float c = 11f;
private float d = 11f;
private float e = 11f;
}
查看class文件后知道虽然我们在代码中写了两次10 和三次11f,但是常量池中,就只有一个常量10 和一个常量11f,实际上他们是不同的引用指向同一个地址
long和 double数据类型的常量在常量池中是怎样表示和存储的?
Java语言规范规定了long 类型和double类型的数据类型占用8 个字节的空间。那么存在于class 字节码文件中的该类型的常量是如何存储的呢?相应地,在常量池中,将long和double类型的常量分别使用CONSTANT_Long_info和Constant_Double_info表示,他们的结构如下所示:
举例:建下面的类 LongAndDoubleTest.java,在这个类中,我们声明了六个变量,但是取值就两种Long类型的-6076574518398440533L 和Double 类型的10.1234567890D。
public class LongAndDoubleTest {
private long a = -6076574518398440533L;
private long b = -6076574518398440533L;
private long c = -6076574518398440533L;
private double d = 10.1234567890D;
private double e = 10.1234567890D;
private double f = 10.1234567890D;
}
String类型的字符串常量在常量池中是怎样表示和存储的?
在编译器编译的时候,都会将这些字符串转换成CONSTANT_String_info结构体,然后放置于常量池中。其结构如下所示:
如上图所示的结构体,CONSTANT_String_info结构体中的string_index的值指向了CONSTANT_Utf8_info结构体,而字符串的utf-8编码数据就在这个结构体之中。如下图所示:
测试案例
public class StringTest {
private String s1 = "JVM原理";
private String s2 = "JVM原理";
private String s3 = "JVM原理";
private String s4 = "JVM原理";
}
常量池中信息如下图
在面的图中,我们可以看到CONSTANT_String_info结构体位于常量池的第#15个索引位置。而存放"Java虚拟机原理" 字符串的 UTF-8编码格式的字节数组被放到CONSTANT_Utf8_info结构体中,该结构体位于常量池的第#16个索引位置。上面的图只是看了个轮廓,让我们再深入地看一下它们的组织吧。请看下图:
4.6 程序计数器
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
程序计数器的特点
是一块较小的存储空间线程私有。每条线程都有一个程序计数器。是唯一一个不会出现OutOfMemoryError的内存区域。生命周期随着线程的创建而创建,随着线程的结束而死亡。
说明:
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
五.对象访问
Object obj = new Object(); 其中Object obj 语义反映到java栈的本地变量表中,作为一个reference数据类型,而new Object() 语义反应到堆中,形成一块存储了Object类型的实例数据的结构化内存。
不同虚拟机实现对象访问的方式不同,主流一般分为两种
1.使用句柄
2.直接指针
1.句柄访问
java堆中划分一块内存作为句柄池,reference中存储的就是对象句柄池的地址,句柄池包含了对象实例数据和类型数据的各自具体地址信息。
2.直接指针
referehce直接存储的是对象地址,在java堆中的对象还设置了对象类型数据的相关信息,如下图
3.优缺点对比
使用句柄,reference中存储的是稳定的句柄地址,对象被移动时只要改变句柄中的实例对象指针地址,而reference不需要改变
使用指针就是速度快,节省了指针定位时间
六 垃圾回收算法
6.1 引用计数算法
给对象添加一个引用计数器,每当有一个地方引用它时,就加一,引用失效时减一,任何时刻计数器为0的对象是不可能被引用的
引用计数算法的问题
对象之间可能存在相互引用,导致他们的计数器都不为0,无法进行GC回收
6.2 根搜索算法
通过一系列名为GC ROOT的对象作为起点,从这些节点开始往下搜索,搜索走过的路径称为引用链,当一个对象到GC root没有任何引用链相连,则说明此对象时不可用的。
java语言中作为GC Root对象的有以下几种
1.虚拟机栈中(栈帧中的本地变量表)的引用的对象
2.方法区中静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI的引用对象
6.3 再谈引用
6.3.1 强引用
类似Object obj = new Object(),只要强引用还在,被引用对象永远不会被回收
6.3.2 软引用
对象还有用,但是非必须,在系统将要发生内存溢出时会把这类对象列入回收范围,如果下次GC后仍然内存不足就会将回收范围内的软引用对象回收掉,如果回收还是没有足够内存,则抛出内存溢出异常,JDK1.2以后提供SoftReference类实现软引用,如下
6.3.3 弱引用
对象还有用,但是非必须,被弱引用关联的对象只能存活到下一次垃圾回收之前,无论内存是否足够都会回收弱引用对象。JDK1.2以后使用WeakReference类实现弱引用。
6.3.4 虚引用
无法通过虚引用来获取一个对象,虚引用的目的是在对象被回收时能收到一个系统通知,JDK1.2以后提供PhantomReference实现虚引用。
6.4 回收方法区
永久区回收主要包含两类
1.废弃常量
当前系统中没有任何一个对象引用常量池的常量,也没有其他地方引用了这个字面量,如果这个时候进行GC且必须,这个时候该变量会被回收
2.无用类
1.该类所有实例都被回收了
2.加载该类的ClassLoader 也被回收
3.该类的class对象没有被任何对方引用,也没有任何对方通过反射来获取该对象
七 垃圾回收算法
7.1 标记清除算法
标记清除算法分为两步骤,先标记需要回收对象,标记完成后统一回收所有被标记的对象,该算法有两个问题,1是标记和回收效率低,需要便利所有对象,2.标记回收会产生内存碎片,碎片太多有可能出发再次GC
7.2 复制算法
将内存划分为两个相等的区域,每次只使用其中一块,当这块用完了就将存活的对象复制到另外一块,然后把使用过的内存空间一次清理掉。现代商业虚拟机都采用这种算法回收新生代
缺点:可用内存缩小了一半
新生代回收:
新生代分为一块较大的Eden区和两块较小的Survivor区,大小比例时8:1:1,每次使用的时Eden区和一块Survivor区,回收时存活的对象会被拷贝到另一块Survivor区,当Survivor不够时,存活对象会拷贝进老年区
缺点:存活对象较多时,进行复制操作效率会很低
7.3 标记整理算法
根据老年代的特点,标记-整理算法同样先进行存活对象的标记,然后将存活对象向一端移动,再清理掉边界以外的对象。
7.4 分代回收算法
根据对象对象存活周期不同,将堆内存划分为新生代和老年代,根据各个年代采用不同的回收算法,新生代在每次回收都有大两对象死去,少量存活,适合复制算法,而老年代对象存活率较高,没有额外担保空间,就必须使用标记-清除或者标记-整理算法。
现代商业虚拟机垃圾回收都采用分代收集算法。
八 垃圾收集器
8.1 Serial 收集器
1.jdk1.3.1 之前都是使用这个收集器
2.垃圾回收器会停止所有用户线程
3.单线程处理垃圾回收
4.新生代使用复制算法,老年代使用标记-整理 算法
8.2 ParNew 收集器
1.使用多线程处理垃圾回收
8.3 CMS收集器
cms收集器时为了获取最短回收停顿时间为目的的收集器,基于标记-清除算法实现,主要分为以下4个步骤
1.初始标记 :停止用户线程,标记GC-Roots能直接关联的对象,速度很快
2.并发标记:就是roots-tracing(寻根)过程
3.重新标记:修正并发标记过程中用户线程运行导致标记变动的对象的再次标记,停止用户线程,时间较长
4.并发清除:多线程清除回收对象
缺点:
1.对CPU资源敏感,默认启动线程为(CPU数量+3)/ 4 占用较多cpu资源
2.无法处理浮动碎片(并发过程用户线程产生的新垃圾)
3.会产生内存碎片
8.5 G1收集器
九 内存分配策略和回收策略
9.1 对象优先在Eden分配
对象在新生代Eden区中分配,当Eden区没有足够空间进行分配,虚拟机会进行一次Minor GC.
Minor GC 和 Full GC 区别
Minor GC: 新生代垃圾回收,会频繁进行且回收速度比较快
Full GC: 老年代垃圾回收,Minor GC 后内存还是不足,会进行full gc ,速度慢10倍以上
9.2 大对象直接进入老年代
大对象指的时需要大量连续内存空间的java对象,典型的大对象就是很长的字符串或者数组。
可以通过设置-XX:PretenureSizeThreshold 参数设定大对象的阈值
9.3 长期存活对象将进入老年区
虚拟机会给每个对象定义一个对象年龄计数器,在Eden区每经过一次Minor GC存活对象,并且Survivor区能个容纳这些存活对象,被移动到Survivor区后,年龄计数器就会+1 默认是15,超过这个阈值就会进入到老年区。
可以通过参数-XX:MaxTenuringThreshold 来设置这个阈值
9.4 空间分配担保
发生Minor GC 时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代剩余空间大小,如果大于,则直接进行一次Full GC ,如果小于则查看HandlePromotionFailure设置是否允许担保失败,如果允许,只进行MinorGC ,反之进行Full GC.