前言

这个问题我相信对于很多Java程序猿来说也是一个比较难回答的问题,说实话,我刚开始被人问到这个问题的时候也不是特别清楚,首先脑子里很难一下想起或者猜测这个内存占用值到底是多少,因此着手去查之,有不少文章阐述这个问题。下面就这个问题展开探索,学习了也便于我们日后进行内存优化相关的工作不是?

环境基础java version "1.8.0_181"

Java当中数据类型有哪些

从大的角度可以分为两类:基础数据类型

引用类型

基础数据类型又分为哪些byte(占用1字节)

short(占用2字节)

int(占用4字节)

long(占用8字节)

float(占用4字节)

double(占用8字节)

char(占用2字节)

boolean(占用1字节)

基础数据类型所占用的内存空间大小是确定的。

基础数据类型在哪里存储分配如果在方法体内定义的,这时候就是在栈上分配的

如果是类的成员变量,这时候就是在堆上分配的

如果是类的静态成员变量,在方法区上分配的

public class Pig {
//堆上分配
private String name;
//方法区分配
private static int age;
private int getPigWeight() {
//栈上分配
int wieght = 100;
return wieght;
}
}

引用类型的存储大小和存储位置

引用类型除对象本身之外,还有一个指向这个对象的指针,在64位虚拟机环境下,这个指针通常占用4个字节,因为默认开启了指针压缩,如果不开启指针压缩的话就占用8个字节。存储位置还是看下面代码吧。

public class Pig {
//'name'和它指向的对象都存储在堆上
private String name = new String();
//'color'和它所指向的对象都存储在方法区上
private static String color = new String();
private void show() {
//temp则在栈上,它所指向的对象可能在堆上也可能在栈上
String temp = new String();
System.out.println(temp);
}
}

Java中对象到底占多大内存

Java中对象分为三种类型:类对象

数组对象

接口对象

对象由三个部分组成:对象头

实例数据

填充数据(padding,由于Java虚拟机规范要求对象所占用内存空间大小需要是8的倍数,所以才有这东西)

对象头

对象头同样由两部分组成:Markword

类指针(kclass)

Markword

按照官方文档对Markword的定义,Markword由64位组成,有下面四种情况:unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)

JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
size:64 ----------------------------------------------------->| (CMS free block)

其中:normal object(对应刚new出来的对象)

biased object(当某个对象被作为同步锁对象时,会有一个偏向锁,其实就是存储了持有该同步锁的线程 id)

CMS promoted object不清楚是啥

CMS free block 不清楚是啥

我们主要关注 normal object, 这种类型的 Object 的 Markword 一共是 8 个字节,由以上定义,我们可以知道 normal object 的Markword组成是这样的:开始25位没有使用

31 位存储对象的 hash 值(这里存储的 hash 值对根据对象地址算出来的 hash 值,不是重写 hashcode 方法里面的返回值)

中间1位同样没有使用

4 位存储对象的 age(分代回收中对象的年龄,超过 15 晋升入老年代)

最后3位表示偏向锁标识和锁标识,主要就是用来区分对象的锁状态(未锁定,偏向锁,轻量级锁,重量级锁)

小扩展

什么时候normal object会转变为biased object呢?

// 无其他线程竞争的情况下,由normal object变为biased object
synchronized(object)

类指针

类指针存储的是该对象所属的类在方法区的地址, Jvm 默认对指针进行了压缩,用 4 个字节存储,如果不压缩就是 8 个字节,主要是出于提高内存分配效率的考虑。

分析个实例

class Animal extends Object {
private int size;
}
Object object = new Object();

Animal animal = new Aniaml();object对象:8个字节的Markword+4个字节的类指针+4个字节的padding=16字节

animal对象:8个字节的Markword+4个字节的类指针+4个字节的int类型数据=16字节

分析检验

import org.openjdk.jol.info.ClassLayout;
public class Animal extends Object {
private int size;
public static void main(String[] args) {
System.out.println(ClassLayout.parseClass(Object.class).toPrintable());
System.out.println(ClassLayout.parseClass(Animal.class).toPrintable());
}
}

这里用到了一个包:

org.openjdk.jol
jol-core
0.10

输出结果

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
com.admin.hellotest.Animal object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 int Animal.size N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

可以看出和我们的分析结果是吻合的。类对象和接口对象和我们上面分析的方法一致,数组对象稍微不同,数组类型的对象除了上面表述的字段外,还有 4 个字节存储数组的长度(所以数组的最大长度是 Integer.MAX)。所以一个数组对象占用的内存是 8 + 4 + 4 = 16 个字节,当然这里不包括数组内成员的内存。

再看个例子

public class Dog extends Animal {
private String name;
private Dog brother;
private long createTime;
public static void main(String[] args) {
System.out.println(ClassLayout.parseClass(Dog.class).toPrintable());
}
}

输出结果:

com.admin.hellotest.Dog object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 int Animal.size N/A
16 8 long Dog.createTime N/A
24 4 java.lang.String Dog.name N/A
28 4 com.admin.hellotest.Dog Dog.brother N/A
Instance size: 32 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

对象真的全部分配在堆里吗

来看这样一段代码

public class Dog extends Animal {
private String name;
private Dog brother;
private long createTime;
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
newDog();
}
System.out.println("消耗时间:" + (System.currentTimeMillis() - start) + " ms");
}
private static void newDog() {
new Dog();
}
}

那么按照前面的分析,一个Dog对象将占用32个字节,这里创建1亿个,估算一下,大概就是3G内存占用,加上虚拟机选项配置:-XX:+PrintGC来看看输出结果。

//没有GC输出日志

消耗时间:4 ms

Process finished with exit code 0

为什么会这样呢?主要是由于Dog对象并不是在堆上分配的,而是在栈上分配的,专业名词叫做指针逃逸,newDog()方法创建的对象并没有被外部使用,所以被优化为栈上分配,可以加上下面两个虚拟机参数禁止指针逃逸

// 虚拟机关闭指针逃逸分析
-XX:-DoEscapeAnalysis
// 虚拟机关闭标量替换
-XX:-EliminateAllocations

好了,以上就是java对象内存占用的全部分析内容了,大家get了多少?