前言:java作为一门面向对象的语言,学习或者工作中最常用的一个关键字莫过于
new
。比如
new
一个对象,
new
一个数组…那么有没有想过创建出来的东西位于内存的哪个地方呢?JVM又是如何替我们管理内存的呢?
本文带大家探索对象这个家伙的生活地。
介绍内存模型之前,先给大家一个简单的例子:
例子主要目的是给一个客户创建两个会员卡,可以先了解一下下面的程序,待后面介绍完了内存各个模型,思考每个元素位于内存何处?
public class Person {
/**
* 将卡片给予客户
*/
public void havaCard(){
MemberCard cardOne = new MemberCard(1234, "八折会员卡");
MemberCard cardTwo = new MemberCard(5678, "满100减30会员卡");
List<MemberCard> cardList = new ArrayList <>();
for(int i = 0; i < 2; i++){
cardList.add(cardOne);
cardList.add(cardTwo);
}
}
}
/**
* 会员卡对象
*/
@Data
class MemberCard{
private int cardNo;
private String cardName;
public MemberCard(int cardNo, String cardName){
this.cardNo = cardNo;
this.cardName = cardName;
}
}
1. 内存管理
先从宏观上看一下,java内存各个区域包含的内容及其作用。
如果是new一个对象,虚拟机首先会去方法区中的常量池中检查这个指令的参数能否定位到一个类的符号引号,并且检查这个符号引号代表的类是否被加载、解析、初始化。在一个new之前,必须进行这些操作。然后才在堆内存中存储一个该对象的实例。
那么对象实例又是如何在内存中存储呢,一个对象实例又包含哪些内容呢?
对象实例在内存中创建之后,虚拟机需要给实例加一下信息。比如:如果找到这个类的元数据信息、对象的哈希吗、对象的GC分带信息之类的。这些信息都是存储在对象实例的不同的部分。类似于将建房子,每个房子都有自己的结构,卧室在哪儿?厨房在哪儿?…等等,这些都需要JVM分配好。
2. 对象的布局
每个对象在内存中包含的信息如下图所示,现阶段可以忽略到每部分包含的内容,后面的文章会继续更新该部分的作用。
每个对象在内部内中包括:
- 对象头。
- 实例数据。类实例对象最根本的内容,比如每个类的字段,比如上面的例子中,cardNo、cardName即存储到该部分。
- 填充内容,该部分可有可无,只有当对象大小不足8个字节的倍数的时候,才需要该部分填充一些内容,占位符的作用。
2. 对象访问是如何做到的?
上面例子中这行代码,java虚拟机是如何根据栈上的cardOne数据找到位于堆上的new MemberCard(1234, "八折会员卡")
对象的呢?
cardList.add(cardOne);
目前虚拟机主要采用如下两种方式:
- 利用句柄池。从JVM堆中划分一部分内存作为句柄池,虚拟机栈的reference中存储的为该对象的句柄地址。
- 利用指针。虚拟机栈的reference中存储的直接是对象的地址。
两者互存,肯定各自有自己的优点。
句柄池:
句柄池的地址是稳定的,可以让虚拟机栈中的reference不用一直改变。对象移动的时候只需要更改句柄池中的对象实例数据指针即可。
指针:
指针直接获取对象,速度快,节省了一次指针定位的过程。
图1:句柄池定位到对象
图2:指针直接定位到对象实例数据
3. 总结
本章从宏观上介绍了JVM中内存的分布情况、每个对象的结构、系统是如何根据栈中的引用找到对应的对象。
思考:试着分析上面例子中每一步变量或者对象的分配过程。