深入JVM虚拟机系列
1、深入JVM虚拟机系列——内存结构分析(入门)
2、深入JVM虚拟机系列——垃圾回收器(进阶)
3、待更新…
文章目录
前言
本篇文章用图文并茂的形式让大家了解JVM,本文使用到的知识来自《深入了解jvm虚拟机》《Java 编程思想》以及维基百科第一章:JVM虚拟机内存结构图分析
一 、JVM是什么?
Java虚拟机(英语:Java Virtual Machine,缩写为JVM),一种能够运行Java bytecode的虚拟机,以堆栈结构机器来进行实做。最早由Sun微系统所研发并实现第一个实现版本,是Java平台的一部分,能够运行以Java语言写作的软件程序。
甲骨文公司的一款Java虚拟机名为HotSpot;另一款自BEA Systems继承而来的名为JRockit。净室设计版Java实现有Kaffe、IBM J9及Skelmir’s CEE-J。甲骨文公司拥有Java商标权,且可能将其用于认证其他实现是否能完全匹配甲骨文的技术规范。 ——来自维基百科
由上图可知,JVM 仅仅只是jdk中的一小部分,与其说它是什么什么东西,我更愿意理解它为Oracle公司开放出来的一种标准,总所周知jdk并非只有Oracle一家,IBM、微软、淘宝、亚马逊等等都有开发自己的Java jdk 而可以知道的是这些不同版本的jdk都是由Oracle提供的标准来实现的,正应了那句“一流公司卖标准”。
1、JVM生命周期
Java实例对应一个独立运行的Java程序(进程级别)
(1)启动
启动一个Java程序,一个JVM实例就产生。拥有public static void main(String[] args)函数的class可以作为JVM实例运行的起点。
(2)运行
main()作为程序初始线程的起点,任何其他线程均可由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM使用,程序可以指定创建的线程为守护线程。
(3)消亡
当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。JVM执行引擎实例则对应了属于用户运行程序线程它是线程级别的。
二、JVM结构图
从上图可知,JVM可分为3大区域:类装载子系统、执行引擎、运行时数据区,本次我们专门分析运行时数据区域的内存划分结构
1、程序计数器
-
作用
1、记录当前线程执行代码行数
-
特点
唯一一个不会出现OOM的内存区域
2、本地方法栈
-
定义:记录与执行本地 方法的区域
-
什么叫做本地方法?
Java调用第三方库(其他语言)方法的方法
-
特点
Hotshot将Java虚拟机栈和本地方法栈合二为一
3、方法区(元空间)
- 是什么?
是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 特点
1、并非数据进入了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载
2、方法区也会抛出OutofMemoryError,当它无法满足内存分配需求时
3、主要存储常量、静态变量
4、虚拟机栈
虚拟机栈,因为是线程独享的也被成为线程栈,之所以是线程独享是因为每一次线程进来JVM都会为这块线程开辟一个新的空间,这块空间就叫栈。
1、特点
- 线程独享,每一个线程都有其独立的内存空间
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常,默认来说一般jvm给栈内存划分的内存大小是1M
- 垃圾回收不涉及此部分内存、线程进入会产生独有空间-栈帧
2、空间划分——栈帧划分
- 局部变量表
-作用:存储局部变量
- 操作数栈
- 变量的临时存储区域
- 动态链接
- 内存标记地址
- 方法出口
- 出栈标识以及记录出栈位置
栈的执行顺序FILO
5、虚拟机堆
- 作用
存储对象实例以及对象内部的实例变量,数组等等 - 区域划分
年轻代:Eden区域和survivor区域
老年代:old区域 - OOM
当垃圾无法被回收,此时old区域已经被占满,就会出现OOM内存溢出
第二章:虚拟机栈
1、原理图
内存结构图:
2、栈内存划分分析
- 局部变量——存储局部变量
- 操作数栈——变量临时存储空间
- 动态链接——存储内存地址
- 方法出口——出栈标识以及记录出栈位置
- 栈帧——方法执行空间
代码如下(示例):
public class StackTest {
private int a ;
private int b;
public void fun1(int a, int b){
int c = a+b;
System.out.println(c);
}
public static void main(String[] args) {
StackTest test = new StackTest();
test.fun1(1,2);
}
}
反汇编代码如下(示例):
public class concurrency.StackTest {
public concurrency.StackTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void fun1(int, int);
Code:
0: iload_1
1: iload_2
2: iadd
3: istore_3
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: iload_3
8: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
11: return
public static void main(java.lang.String[]);
Code:
0: new #4 // class concurrency/StackTest
3: dup
4: invokespecial #5 // Method "<init>":()V
7: astore_1
8: aload_1
9: iconst_1
10: iconst_2
11: invokevirtual #6 // Method fun1:(II)V
14: return
}
3、jvm指令集
变量到操作数栈:iload,iload_,lload,lload_,fload,fload_,dload,dload_,aload,aload_
操作数栈到变量:istore,istore_,lstore,lstore_,fstore,fstore_,dstore,dstor_,astore,astore_
常数到操作数栈:bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_,lconst_,fconst_,dconst_
加:iadd,ladd,fadd,dadd
减:isub,lsub,fsub,dsub
乘:imul,lmul,fmul,dmul
除:idiv,ldiv,fdiv,ddiv
余数:irem,lrem,frem,drem
取负:ineg,lneg,fneg,dneg
移位:ishl,lshr,iushr,lshl,lshr,lushr
按位或:ior,lor
按位与:iand,land
按位异或:ixor,lxor
类型转换:i2l,i2f,i2d,l2f,l2d,f2d(放宽数值转换)
i2b,i2c,i2s,l2i,f2i,f2l,d2i,d2l,d2f(缩窄数值转换)
创建类实便:new
创建新数组:newarray,anewarray,multianwarray
访问类的域和类实例域:getfield,putfield,getstatic,putstatic
把数据装载到操作数栈:baload,caload,saload,iaload,laload,faload,daload,aaload
从操作数栈存存储到数组:bastore,castore,sastore,iastore,lastore,fastore,dastore,aastore
获取数组长度:arraylength
检相类实例或数组属性:instanceof,checkcast
操作数栈管理:pop,pop2,dup,dup2,dup_xl,dup2_xl,dup_x2,dup2_x2,swap
有条件转移:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull,if_icmpeq,if_icmpene,
if_icmplt,if_icmpgt,if_icmple,if_icmpge,if_acmpeq,if_acmpne,lcmp,fcmpl
fcmpg,dcmpl,dcmpg
复合条件转移:tableswitch,lookupswitch
无条件转移:goto,goto_w,jsr,jsr_w,ret
调度对象的实便方法:invokevirtual
调用由接口实现的方法:invokeinterface
调用需要特殊处理的实例方法:invokespecial
调用命名类中的静态方法:invokestatic
方法返回:ireturn,lreturn,freturn,dreturn,areturn,return
异常:athrow
finally关键字的实现使用:jsr,jsr_w,ret
栈内存剖析:
- 当每一次线程进入程序时jvm会为其开辟一块区域我们称之位线程栈里面包含了程序计数器、本地方法栈(Hotspot把本地方法栈和虚拟机栈合二为一)、虚拟机栈
- 调用方法时会出现一块特殊区域——栈帧,每一个方法都有一块独立的栈帧
- 每一个栈帧都划分为局部变量表、操作数栈、动态链接、方法出口
- 当线程执行进入方法,局部变量会开始加载进入局部变量表,然后装载参数到操作数栈,最后赋值给局部变量
- 线程每执行一行都会由程序计数器记录执行的代码行数并保存方便当CPU被挂起时或者线程暂停执行后继续当前位置运行
- 每次执行完毕后都会调用方法出口回到线程执行位置
4、高性能内存之逃逸分析——栈上分配
1、 什么是逃逸分析?
逃逸分析——基本行为就是分析对象的动态作用域
- 方法逃逸:一个对象在方法中被定义后,可能被外部方法所引用,这种现象称之为方法逃逸
- 线程逃逸:一个对象在方法中被定义后,被外部线程访问,例如 复制给类变量这种可以被其他线程发现的变量或者赋值给其他线程的实例变量这种叫做线程逃逸
2、栈上分配——为什么需要逃逸分析?
一般内存分配都是在堆中进行,但是jdk1.8开始默认开启了逃逸分析,如果对象没有发生逃逸分析就把没发生逃逸的对象放置到栈空间进行存储,因为栈是出栈后就会进行一个销毁所以比堆内存维护更加的高效。
3、参数配置
-XX:-DoEscapeAnalysis 不做逃逸分析
-XX:+DoEscapeAnalysis 做逃逸分析(默认)
逃逸分析代码演示:
public class TestEscape {
private static Object object ;
//线程逃逸
public void variableEscape(){
object = new Object();
}
//方法逃逸
public Object threadEscape(){
return new Object();
}
//没有发生逃逸
public static void alloc(){
byte[] bytes = new byte[2];
bytes[0] = 2;
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
for(int i =0;i<10000000;i++){
alloc();
}
long end = System.currentTimeMillis();
System.out.println("执行时间:"+(end-start)+"ms");
}
}
结果分析
开启逃逸分析
关闭逃逸分析
第三章:虚拟机堆
堆内存结构图
1、内存分配
-Xms20M 设置JVM初始内存20M
-Xmx20M 设置JVM最大内存为20M
-Xmn10M 设置年轻代内存大小为10M
-XX:PretenureSizeThreshold=3145728 设置最大对象内存3M
-XX:+UseSerialGC 使用serialGC收集器
2、区域划分
- 年轻代——1/3堆内存区域
- Eden区 ——8/10年轻代区域
- survivor区——2/10年轻代区域
- from区——1/10
- to区——1/10
- 老年区——2/3堆内存区域
名词解释:
Minor GC:也被称之为young GC 发生在年轻代的垃圾回收,轻量级,特点:时间极短
FULL GC: 发生在old区域的GC 对老年代区域的所有垃圾进行回收 特点:回收范围广,回收时间长
3、堆内存分配演示
代码示例
public class HeapTest {
byte[] a = new byte[1024*100];
public static void main(String[] args) throws InterruptedException {
List<HeapTest> list = new ArrayList<>();
while (true){
list.add(new HeapTest());
Thread.sleep(50);
}
}
}
控制终端使用命令:jvisualVM 然后运行程序
结果图分析
从上图大概可知堆内存分配情况:
- 新创建的对象首先进入Eden区域,但是由于Eden区域空间有限,随着内存的不断使用达到Eden所能存储的上限就会发生一次GC(轻量级)随之剩下的存活对象就被分配到survivor区这里就进行了一次垃圾回收(其实Eden区域的存活对象不一定会到survivor区,有一定可能直接到old区域,这里涉及到old区域的分配担保(Handle promotion)机制)
- 存活对象进入到survivor区会进行一个from to 的swap阶段也叫分代年龄增长的一个过程
- 最后survivor区存活的对象分代年龄达到15后会被直接送入old区域等待回收
- 其中survivor from区域并非是一个固定的区域而是随着内存的变化而变化的,也就是说这次收集结束后from区(空白区域)会变成to区域,to区域就变成了新的from区域
1、分代年龄是什么?——涉及对象结构以及对象header
2、对象内存分配流程图
图示:
第四章、堆、栈、方法区(元空间)之间的联系
图示:
1、原理
栈内存中的应用指向堆内存对象,堆内存对象中存在一个对象类型指针保存着该类的唯一标志同时指向方法区中加载的类元信息
2、对象的访问方式
- 直接访问
- 由栈引用指向堆内存对象中的对象类型数据,然后判断该数据是否还有效,如果有效则获取该对象类型指针然后指向方法区中的对象类型数据(元数据)
- 句柄池访问
- 由栈引用指向堆内存中的对象句柄池中的对象实例数据引用,然后句柄池根据对象实例数据查询是否存在对应对象类型数据指针,如果有则直接访问方法区中的类型数据
图示: