深入理解Java虚拟机(一)——JVM内存结构

说明:深入理解Java虚拟机系列是对《深入理解Java虚拟机——JVM高级特性与最佳实践》第二版一书的总结与概要

1 什么是jvm

JVM(Java Virtual Machine)是Java程序运行的平台,负责执行Java编译好的字节码文件。JVM具有非常严格的实现规范,大多数操作系统都可以安装JVM,为Java语言的跨平台性起到了关键的作用。

2 jvm内存区域

2.1 jvm运行时数据区域

java虚拟机运行时数据区
2.1.1 程序计数器

  • 内存空间小,可以看作为当前程序执行的行号指示器,虚拟机通过改变计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等都需要依赖计数器完成。
  • 处于线程独占区,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储。
  • 如果线程执行的为java方法计数器记录的为正在执行的虚拟机字节码指令的地址。如果正在执行native方法,这个计数器的值为undefined。
  • 程序计数器是唯一一个在java虚拟机规范中没有规定任OutOfMemoryError情况的区域

2.1.2 虚拟机栈

  • 虚拟机栈描述的是java方法执行的动态内存模型,每个方法执行都会创建一个栈帧,伴随着方法从创建到执行完成。用于存储局部变量表,操作数栈,动态链接,方法出口等。
  • 局部变量表:存放编译期可知的各种基本数据类型,引用类型,returnAddress类型。
  • 当虚拟机栈分配的空间放不下栈帧时,方法依旧再调用,不断进栈,则会抛出StackOverflowError异常,如果不设置栈的大小,则会一直申请jvm内存、机器内存直至抛出OutOfMemoryError异常。
/**
 * @desc 栈溢出,递归生成栈帧 不断进栈
 * VM Args: -Xss128k
 * -Xss 设置栈的大小
 * @author lb
 * @date 2019年3月15日
 */
public class StackTest {
	public static void main(String[] args) {
		loop();
	}

	private static void loop() {
		while (true) {
			loop();
		}
	}
}

2.1.3 本地方法栈

  • 与虚拟机栈功能基本相同,它是虚拟机执行native方法的服务
  • 与本地方法栈一样,也会抛出StackOverflowError和OutOfMemoryError异常

2.1.4 Java堆

  • Java堆是所有线程共享的一块区域,存放实例对象和数组。
  • Java堆是垃圾收集器管理的主要区域
  • Java堆可分为:新生代(Eden、Survivor、Tenured Gen)、老年代
/**
 * 
 * @desc 堆内存溢出,不断创建对象进入堆中,直至溢出
 * VM Args: -Xms20M -Xmx20M
 * -Xms 程序启动时配置的堆最小内存
 * -Xmx 堆最大内存
 * @author lb
 * @date 2019年3月15日
 */
public class HeapTest {
	public static void main(String[] args) {
		List<HeapTest> list = new ArrayList<>();

		while (true) {
			list.add(new HeapTest());
		}
	}
}

2.1.5 方法区

  • 存储虚拟机加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码等数据

2.1.6 运行时常量池

  • 用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。
  • jdk1,6常量池放在方法区,jdk1.7常量池放在堆内存,jdk1.8放在元空间里面,和堆相独立。
XX:PermSize=M -XX:MaxPermSize=M
XX:PermSize 设置方法区(永久代)大小
jdk1.8以后设置
-XX:MetaspaceSize=M -XX:MaxMetaspaceSize=M

2.2 对象的创建

对象的创建过程
2.2.1 为对象分配内存

  • 分配方式:内存的分配方式是由java的堆是否规整决定,是否规整是由垃圾回收器决定,垃圾回收器如果带有压缩整理功能,将堆划分为有规则的空间,则使用指针碰撞。否则使用空闲列表。
    指针碰撞方式
    空闲列表方式

2.2.2 对象的内存分布

  • 在HotSpot虚拟机中,对象分为三块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
    • Header(对象头):
      (1)自身运行时的数据:哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。。
      (2)类型指针:对象指向它的类原数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
    • nstanceData(实例数据):存储对象的信息,代码中所定义的类型的字段内容
    • Padding(占位符):对象的大小必须时8个字节的整数倍,Header刚好为8个字节,padding为填充内存作用,无其它实际作用

2.2.3 对象的访问定位

扫描二维码关注公众号,回复: 5579761 查看本文章
  • Java通过栈上的reference数据来操作堆上的具体对象,主流的访问有句柄和直接指针两周方式。

通过句柄访问,堆会开辟一块内存作为句柄池,reference中存储对象的句柄地址,句柄中包含对象的实例数据及类型数据各自的地址。如下图:
通过句柄访问对象
通过直接指针访问,堆对象就必须存储访问类型数据的相关信息,reference存储的就是对象地址。如下图:
通过直接指针访问对象
比较:
句柄方式访问对象,reference存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,不用改变reference。
直接指针方式访问对象,速度更快,节省了一次指针定位的开销。
如果对象GC频繁则使用句柄方式,如果对象被频繁访问则使用直接指针方式。

猜你喜欢

转载自blog.csdn.net/qq_34929019/article/details/88569768
今日推荐