JVM小结


JVM是Java虚拟机的缩写。引入java语言虚拟机后,java语言在不同平台上运行时不需要重新编译。java语言使用了java虚拟机屏蔽了与具体平台相关的信息,使得java语言编译编译程序只需要生成在java虚拟机上运行目标代码(字节码),就可以在多种平台上不加修改的运行。

1.简介

1.1jvm内存

  • 堆(Heap)
  • 方法区(Method Area)
  • 程序计数器(Program Counter Register)
  • 虚拟机栈(VM Stack)
  • 本地方法栈(Native Methid Stack)

1.2JVM(hotspot) 结构

在这里插入图片描述

灰色部分(java栈,本地方法栈和程序计数器)是线程私有,不存在线程安全的问题。橙色部分(方法区和堆),为线程共享区。

2.程序计数器

程序计数器又叫做PC寄存器。每个线程都有一个程序计数器,是线程私有的,是一块比较小的内存空间。
程序计数器是一个指针,指向方法区的方法字节码,用来存储指向下一条指令的地址,也即将要执行的指令代码,由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
如果执行的是Native方法,那么这个计数器的值为undefined。

为什么需要程序计数器呢?
由于java虚拟机的多线程是通过线程轮流切换并分配处理器的执行时间的方式来实现的,在任何一个确定的时刻,一个处理器内核都会只执行一条线程中的命令。CPU需要不断切换各个线程,有了程序计数器后,当CPU切换回来以后,我们就可以知道接着从哪开始继续执行程序了。

public class Test { 
	public static void main(String[] args) { 
		int a = 1; 
		int b = 2; 
		int c = a + b; 
		System.out.println(c); 
		} 
	}

查看其字节码:javap-c xxx.class

在这里插入图片描述

假如当前线程的程序计数器存储的指令地址为6,这时候CPU切换到别的线程中处理工作;一段时间后,当前线程重新获得cpu时间片继续执行时,根据程序计数器存储的6就可以知道,当前需要执行iadd(即a+b操作)执行。执行引擎就会将这条指令翻译成机器指令,然后CPU执行该运算操作。

3.虚拟机栈(Java栈)

虚拟机栈也称为java栈,每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个栈帧(Stack Frame)。每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧。

  • Java虚拟机栈是线程私有的,它的生命周期与线程相同(创建-就绪-运行-阻塞-死亡)
  • 栈帧包括局部变量(8种基本数据类型,对象引用地址),操作数栈,动态链接,方法返回地址和一些附加信息。
  • 每个方法被调用直至执行完毕的过程,就对应这一个栈帧在虚拟机栈中从入栈到出栈的过程。

在这里插入图片描述

jvm对虚拟机栈的操作只有入栈和出栈。在一个活动线程中,一个时间点只会有一个活动的栈帧,即当前正在执行方法对应的栈帧(当前栈帧);如果一个方法调用了另一个方法,那么对应的新的栈帧也会被创建出来,放在栈顶,成为新的当前栈帧。
Java方法执行结束正常退出和异常抛出都会导致栈帧被弹出。

3.1栈帧内部结构

每个栈帧包含5个组成部分:局部变量表,操作数栈,动态链接,方法返回地址和一些附加信息。
在这里插入图片描述

3.2 局部变量表

局部变量表是一个数字数组,用于存储方法参数和方法体内的局部变量。

3.3 操作数栈

每一个独立的栈帧中除了包含局部变量外,还包含一个FILO的操作数栈,用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。

public class Test { 
	public void test() { 
		int a = 1; 
	} 
}

javap -v xxx.class
在这里插入图片描述

3.4 动态链接

在java源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里,动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

public class Test { 
	public void hello() { 
		System.out.println("hello"); 
	} 
}

在这里插入图片描述

3.5 方法返回地址

存放调用该方法的pc寄存器的值,一个方法的结束分为以下两种方式:

  • 正常执行结束
  • 出现未处理异常,非正常退出

无论是那种方法退出,在方法退出后都返回到该方法被调用的位置

  • 方法正常退出时,调用者的pc寄存器的值作为返回地址,即调用该方法的指令的下一条指令地址
  • 异常退出时,返回地址需要通过异常表来确定

3.6 一些附加信息

比如对程序调试提供的支持信息

3.7 虚拟机栈大小调整

Java虚拟机规范允许虚拟机栈的大小固定不变或者动态扩展。

  • 固定情况下:如果线程请求分配的栈容量超过java虚拟机允许的最大容量,则抛出StackOverflowError异常
  • 可动态扩展情况下:尝试扩展的时候无法申请到足够的内存;或者在创建新的线程时没有足够的内存区创建对应的虚拟机栈,则会抛出OutOfMemortError异常

不同平台的虚拟机栈默认大小不同:

  • Linux:1024kb
  • macOS:1024kb
  • Windows:取决于虚拟内存

通过-Xss设置虚拟机栈大小,默认单位为字节

  • Xss1m
  • Xss1024k
  • Xss1048567

虚拟机栈越大,方法调用深度越深

4. 本地方法接口

本地方法接口的作用时融合不同的编程语言为Java所用,它的初衷时融合C/C++程序。于是就在内存中专门开辟了一块区域处理标记为native的代码。

5. 本地方法栈

虚拟机栈用于管理Java方法的调用,而本地方法栈则是用于管理本地方法的调用。

6. 堆(Heap)

一个jvm实例只存在一个堆内存,堆内存的大小是可以调节的。堆中保存着所有引用类型的真实信息(即对象),以方便执行器执行。

6.1 堆分区

堆在逻辑上分为三个区域:

java7
在这里插入图片描述

java8:
在这里插入图片描述

java8之后的堆物理只分为新生区和养老区,元空间不占用堆内存,而是直接使用物理内存。

6.2 堆为什么要分区

java程序中不同对象的生命周期不同。70%-90%对象都是临时对象,这类对象在新生区“朝生夕死”。如果没有分区,GC搜索垃圾时需要对整个堆内存进行扫描;分区后,回收这些“朝生夕死”的对象,只需要在小范围的区域中(新生区)搜集垃圾。所以,分区的唯一理由就是
为了优化GC性能。

6.3 java7和java8区别

在java7,堆分为新生代,老年代和永久代。在java8中,永久代已经被溢出,被一个称为元空间的区域所取代,元空间的本质和永久代类似。元空间和永久代的最大区别就是:永久代使用jvm的堆内存,但是元空间不在虚拟机中而是使用本机的物理内存。

堆大小=新生代+老年代

6.4 堆参数

参数含义
-Xms设置堆的初始内存大小,默认为物理内存的1/64
-Xmx设置堆的最大内存大小,默认为物理内存的1/4
-XX:Newratio设置新生区和养老区的比例,默认值为2
-XX:Surviorratio设置伊甸园区和一个幸存者区的比例,默认值为8
-Xmn设置新生区的内存大小(一般不使用)
-XX:MaxTenuringThreshold设置转入养老区的存活次数,默认值为15
-XX:+PrintFlagsInitial查看所有参数的默认初始值

7. 方法区

方法区并不是所谓的存储方法的区域,而是供各种线程共享的运行时内存区域。它存储了已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等。

7.1 方法区,堆,栈的关系

方法区和堆,栈的关系如下图所示:

public class Test { 
	public static void main(String[] args) { 
		Test test = new Test(); 
	} 
}

在这里插入图片描述

7.2方法区的内部结构

7.2.1 类型信息

对每个加载的类型(类Class,接口interface,枚举enum,注解annotation),jvm必须在方法区中存储以下类型信息:

  1. 每个类型的完整有效名称(包名.类名);
  2. 这个类型直接父类的完整有效名
  3. 这个类的修饰符(public,abstract,final)
  4. 这个类型直接接口的有序列表

7.2.2 方法信息

  1. 方法名称;
  2. 方法的返回值类型(没有返回值时void)
  3. 方法参数的数量和类型(有序)
  4. 方法的修饰符(public,final,synchronized,native等)
  5. 方法的字节码,操作数栈,局部变量表及其大小
  6. 异常表

7.2.3 域信息

域Field我们也称为属性,字段。域信息包含:

  1. 域的声明顺序
  2. 域的相关信息,包括名称,类型,修饰符

7.2.4 JIT 代码缓存

这部分在JIT编译器执行引擎中再做说明

7.2.5 运行时常量池

类字节码反编译后,会有一个Constant pool的结构,俗称常量池,所有的变量和方法引用都作为符号引用保存在class文件的常量池中。虚拟机栈的动态链接就是将符号引用转换为直接引用的过程。

这里说的常量包括:基本类型包装类(包装类不管理浮点型,整型只会管理-128到127)和String

7.2.6 静态变量

静态变量就是使用static修饰的域信息。静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。静态变量也称为类变量,类变量被类的所有实例共享,即使没有类实例时也可以访问.

7.3 方法区的演进

随着jdk的迭代升级,Hotspot中方法区的存储的内容

版本描述
jdk1.6及之前有永久代,静态变量放在永久代上
jdk1.7有永久代,但已逐步“去永久代”,字符串常量池,静态变量移除,保存在堆中
jdk1.8及之后无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池,静态变量依然保存在堆中
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值