Android工程师学习JVM(五)-内存分配基础知识

861 阅读7分钟

前言

在学习JVM这个系列文章中,已经讲解了JVM规范、Class文件格式以及如何阅读字节码、ASM字节码处理、类的生命周期及自定义类加载器等。本篇将和大家一起学习内存分配相关知识。这部分知识对于深入理解java执行过程非常有帮助,如Person person = new Person(),这简单的一行代码中,所包含的数据在内存中究竟是怎样存储的。

如果你对JVM、字节码、Class文件格式、ASM字节码处理、类加载及自定义类加载器有兴趣的话,可以看之前的文章哈,相信会收获更多哦

Android工程师学习JVM(四)-类加载、连接、初始化、卸载

Android工程师学习JVM(三)-字节码框架ASM使用

Android工程师学习JVM(二)-教你阅读Java字节码

Android工程师学习JVM(一)-JVM概述

1、JVM简化架构

回顾一下JVM的整体架构,第一步是将字节码文件(class文件)通过类加载器加载到内存当中,然后通过执行引擎执行对应指令,在这个过程中执行引擎要和内存进行一系列的交互。而后执行引擎再和计算机本地库接口进行交互,最终实现效果。

上篇文章中讲述了类加载器最终是将字节码文件读成二进制流再加载到内存当中。本篇咱们来分析下运行时数据区这个部分。

2、运行时数据区

运行时数据区包含:PC寄存器Java虚拟机栈本地方法栈java堆方法区运行时常量池

下面咱们来挨个介绍下。

2.1、运行时数据区简要介绍

PC寄存器:

1、每个线程拥有一个PC寄存器,是线程私有的,用来存储指向下一条指令的地址

2、在创建线程的时候,创建相应的PC寄存器

3、执行本地方法时,PC寄存器的值为undefined

4、PC寄存器是一块较小的内存空间,是不会OutOfMemoryError的内存区域

Java栈:

栈由一系列帧组成(Java栈又称为帧栈),是线程私有的

帧是用来保存一个方法的局部变量、操作数栈、常量池指针、动态链接、方法返回值等

每一次方法调用都会创建一个帧,并压栈,退出方法的时候,修改栈顶指针就可以把栈帧中的内存销毁

局部变量表存放了编译期可知的各种基本数据类型和引用类型,每个slot存放32位的数据,long、double占两个槽位

栈的优点:存取速度比堆快,仅次于寄存器

栈的缺点:存在栈中的数据大小、生存期是在编译期决定的,缺乏灵活性

Java堆

用来存放应用系统创建的对象和数组,是线程共享的

堆的优先:运行期动态分配内存大小,自动进行垃圾回收

堆的缺点:效率相对较慢

方法区

通常用来保存装载的类的结构信息,是线程共享的

运行时常量池(方法区分配的)

是Class文件中每个类或接口的常量池表,在运行期间的表示形式,通常包括:类的版本、字段、方法、接口等信息

本地方法栈

JVM中用来支持native方法执行的栈就是本地方法栈

2.2、栈、堆、方法区交互关系

如代码中 User user = new User()。包含了user对象引用、user对象实例数据、User类定义。这三者的存放位置分别为:栈、堆、方法区。

在上文中,我们介绍到栈是线程私有的,而堆和方法区是线程共享的。多线程情况下,不同线程可以创建多个user对象引用,指向堆中的同一个对象,共享数据。

堆中可以有多个User实例对象,每个User实例对象的元数据信息是相同的,只保留元数据信息的引用,而将类定义、方法、字段放到方法区。这样每个实例对象就可以共享类定义、方法、字段信息。

因此,这样的设计极大的节省了数据空间。

3、Java堆内存模型和分配

从上面的讲述中,我们了解到只有堆是运行时动态分配的。下面将从堆的结构和对象的内存布局来介绍

3.1、Java堆内存概述

1、Java堆用来存放应用系统创建的对象和数组,是所有线程共享的

2、Java堆是在运行时动态分配内存大小,自动进行垃圾回收

3.2、Java堆的内存结构

Java堆是分代的,分为新生代和老年代

新生代:分为Eden区和存活区。Eden区存放新创建的对象,存活区分为From区和To区。新生代中经过垃圾回收没有回收掉的对象,被复制到老年代

老年代:存储对象比新生代存储对象年龄大得多,同时存储一些大对象

这里有两条值得注意的公式:

1、整个堆大小=新生代 + 老年代

2、新生代=Eden + 存活区

3.3、对象的内存布局

对象在内存中存储的布局(这里以HotSpot虚拟机为例),分为:对象头、实例数据和对齐填充

对象头包含两个部分:

1、Mark Word: 存储对象自身的运行数据,如:HashCode、GC分代年龄、锁状态等

2、类型指针:对象指向它的类元数据的指针

实例数据:

真正存放对象实例数据的地方

对齐填充:

仅仅是占位符,无特殊含义(HotSpot虚拟机要求对象起始地址都是8字节的整数倍,如果不是就对齐)

4、对象的访问定位

第三节我们讲述了实例对象在堆中的存放,2.2节中我们介绍了栈和堆之间的交互关系。本节将具体讲述从栈中具体怎么找到堆中的对象实例数据的。

对象的访问方式在JVM规范中并没有规定具体实现,当前主流的实现有两种,一种是使用句柄,另一种是使用指针

4.1、使用句柄定位

这种方式中,Java堆中会划分出一块内存来作为句柄池,reference中存储句柄的地址,句柄中存储对象的实例数据和类元数据的地址,如下图所示:

在Java栈本地变量表中,reference引用指向Java堆中句柄池的某个对象指针(包含实例数据指针和类型数据指针),实例数据指针再指向对象实例数据,对象类型指针指向方法区中的对象类型数据

4.2、使用指针定位

Java栈本地变量表中,reference引用指向Java堆中实例数据所在区域(该区域中又存在指向对象类型数据的指针)

4.3、句柄定位和指针定位优缺点

使用句柄定位:优点是当对象实例发生变化,修改的是句柄池中的对象实例数据指针,而reference不需要修改,是一种间接引用。缺点是速度较慢,因为是间接定位

使用指针定位:优点是速度较快,比使用句柄定位少了一次索引。但在对象实例发生变化时,reference需要变化

5、小结

1、简要介绍Jvm的简化结构,了解内存区域和类加载器、执行引擎、垃圾回收器等的关系

2、重点介绍运行时数据区的划分和各个部分的作用。其中PC寄存器、栈、本地方法栈是线程私有的,方法区和堆是线程共享的

3、介绍创建对象时,对象引用、实例数据、对象类型数据这几个部分存放的位置及其交互关系

4、介绍堆内存结构,简要说明了新生代和老年代(这部分会在后续学习垃圾回收时再重点介绍),介绍一个对象实例在堆内存中存放了对象头、实例数据、可能存在数据对齐。其中对象头中存放了指向实例类型数据的指针

5、最后介绍了对象的常见定位方式分为句柄定位和指针定位,以及这两种方式之间的不同。HotSpot使用的是指针定位方式