Java虚拟机内存管理(二)—堆的使用

Java 与 C++ 之间有一堵由内存动态分配和垃圾收集技术所围成的 “高墙”,墙外面的人想进去,墙里面的人却想出来。——《深入理解Java虚拟机:JVM高级特性与最佳时实践(第二版)》周志明

Java 虚拟机作为运行 Java 程序抽象出来的计算机,具有内存管理的能力,像内存分配、垃圾回收等这些相关的内存管理问题,Java 虚拟机都会帮我们解决,所以作为一个 Java 程序员要比 C++ 程序员幸福,但是内存方面一旦出现问题,如果对虚拟机怎样使用内存不了解,就很难排查错误。

这段时间看周志明先生的《深入理解Java虚拟机:JVM高级特性与最佳时实践(第二版)》,下面就对 Java 虚拟机对内存的管理做一个系统的整理,本篇文章是该专题的第二篇。

2、堆的使用

对 Java 堆使用,也即是对象创建时使用这一部分的内存,语言层面上,对象的创建只是一个 new 关键字,但是在 Java 虚拟机(这里指的是主流的 HotSpot 虚拟机)中的对象(这里讨论的对象不包括数组和 Class 对象)创建过程却不是这么简单的。

2.1 给对象分配内存

在虚拟机遇到一条 new 指令时,首先会去检查这个指令的参数是否能在方法区的常量池中找到这个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析(符号引用替换成直接引用)和初始化。如果没有,那必须执行相应的类加载过程。在类加载检查后,虚拟机才会为新生的对象在 Java 堆区域分配内存。

对象所需的内存大小在类加载完成后就可以确定下来,为对象分配内存空间相当于把一块确定大小的内存从 Java 堆中分出来。这个划分不一定是规整的,也即不是已经使用过的内存(已经分给对象的)放在一边,空闲的内存放在一边,已经使用过的内存和空闲的内存很可能是相互交错的,这就需要一个列表来维护,记录哪些内存块是可以用的,哪些内存块已经被对象占用了,在这个过程中很可能会产生一些内存碎片。根据 Java 堆中的内存是否规整,有“指针碰撞”和“空闲列表”两种分配方式,而 Java 堆是否规整,又是由所采用的垃圾收集器是否具有内存压缩整理功能决定的。对象内存的划分除了需要考虑内存空间外,还需要考虑引用对象的指针。在给一个对象A分配内存时使用的指针P,绝对不能被另外一个对象B同时使用来分配B对象所需的内存,也即是内存的分配需要是一个原子性操作,这样才能在并发情况下保证线程的安全。当然解决这个问题,也有不同的方法,一种是对分配内存空间的动作加锁进行同步处理,另一种是把内存分配的动作按照线程划分,每个线程在 Java 堆中都预先分配一小块内存区域,称为是“本地线程分配缓冲”(TLAB),哪个线程要给对象分配内存就在该线程的 TLAB 上分配。

2.2 初始化对象

给对象分配完内存后,虚拟机还需要将分配到的内存空间初始化为零(不包括对象头)。这一步操作保证了对象实例字段可以在不赋初值的情况下,就可以访问到这些字段的零值。对象的对象头中存放着这个对象是属于哪个类的实例,怎样找到类的元数据、对象的哈希码,对象在垃圾收集器中分代的年龄等信息。经过虚拟机为我们初始化对象后,一个对象可以说是诞生了,但是对于我们程序员来说,还需要根据我们在程序中编写的构造函数初始化。

2.3 对象在内存中存储

在 HotSpot 虚拟机中,对象在内存中的存储也是有规律的,存储的布局可以分为三块区域:对象头区域、实例数据区域和对齐填充区域。

对象头在前面已经说了,对象头的一部分用于存储对象自身运行时的数据,另外的部分是类型指针,即对象指向它类元数据的指针,虚拟机通过这个指针来确定这个对象是属于哪个类的。

实例数据区域是对象存储真正有效的信息,即在程序中定义的各种类型的字段数据。这一部分数据有从父类中继承下来的,也有在子类中定义的,总之都要被记录下来。

对齐填充并不一定是必然存在的,因为 HotSpot 虚拟机内存管理的要求是给对象分配内存的大小必须是 8 字节的整数倍,所以不够的部分才需要对齐填充。又因为对象头部分正好是 8 字节的倍数,所以对齐填充是为了补全实例数据区域的。对齐填充并没有特别的含义,仅仅是起到填充占位符的作用。

2.4 访问对象

我们创建对象的目的是为了使用对象,这就要牵扯到如何访问对象了。在前面对内存的划分中说到,Java 虚拟机栈的局部变量表,存放的有对象引用(reference)类型,这个类型在 Java 虚拟机规范中只是规定这是指向一个对象的引用,但并没有规定如何定位对象,访问对象在堆中的具体位置。在不同的虚拟机中,对象的访问方式也是不同的,主流的访问方式有使用句柄直接指针两种。

使用句柄:

通过句柄访问对象.jpg

如果使用句柄访问方式,Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的是对象的句柄地址,而句柄中包含的才是对象实例数据和类型数据各自的具体地址信息,所以说使用句柄是一种间接使用指针访问对象的方式。

直接指针:

通过直接指针访问对象.jpg

如果使用直接指针访问方式,Java 堆中对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的就是对象地址。

两种访问方式各有优势,使用句柄访问方式的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改。而使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的的时间开销。在 HotSpot 虚拟机中采用的是第二中访问方式,但使用句柄方式来访问的情况在软件开发中也很常见。

小结:

Java 堆就像是游戏玩家出生的新手村,承载着一个对象来到 Java 世界的梦想,资源的分配,初始的装备,以及对象的区分和查找,每一步都和对象的命运息息相关。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员互动联盟

【答疑解惑】常量字符串引发的“血案”

有朋友在《程序员互动联盟》QQ群里问了如下一个问题,见下的QQ截图图: ? 上图与下面这个图中,请注意main函数中s1和s2这两个变量。一个定义为指针,一个定...

3687
来自专栏全沾开发(huā)

拿Proxy可以做哪些有意思的事儿

2618
来自专栏Java 源码分析

Java 虚拟机运行时数据区

运行时数据区: Java 虚拟机的运行时数据区按照大的可以分为线程独立使用的数据区,和所有线程共享的数据区。 一.线程独立使用数据区 1.程序计数器 程序计数器...

3424
来自专栏Redis源码学习系列

Redis源码学习之对象系统

在前面的文章中,我介绍了Redis的底层数据结构,但Redis对外提供的命令并没有直接使用它们,而是基于它们构建更高级的数据对象,总共包括5中对象类型,分别为【...

1453
来自专栏思考的代码世界

Python基础学习06天

1554
来自专栏电光石火

null或空值的判断处理

1,错误用法一: if (name == "") {      //do something } 2,错误用法二: if (name.equals(""))...

17910
来自专栏Java面试通关手册

可能是把Java内存区域讲的最清楚的一篇文章

哈哈 皮一下!我自己开源的一个Java学习指南文档。一份涵盖大部分Java程序员所需要掌握的核心知识,正在一步一步慢慢完善,期待您的参与。Github地址:ht...

1032
来自专栏用户画像

浅析JAVA堆内存和栈内存的区别

堆内存:https://baike.baidu.com/item/%E5%A0%86%E5%86%85%E5%AD%98/7270805?fr=aladdin

1721
来自专栏黑泽君的专栏

java注解用法详解——@SuppressWarnings

  在java编译过程中会出现很多警告,有很多是安全的,但是每次编译有很多警告影响我们对error的过滤和修改,我们可以在代码中加上 @SuppressWarn...

1.8K3
来自专栏magicsoar

一个C++bug引入的许多知识

一、前言     假设我们有一个Car类,用了表示一个车,它有id,名字,牌照等许多东西,还有一个表示车的部件CarPart。     但出于某方面的考虑,我们...

2479

扫码关注云+社区

领取腾讯云代金券