首页 小编推荐 正文

再次学习Java——一个Java对象需要多少内存-必威体育betwayAPP_betway体育手机版_betway必威体育官网

内存是程序员逃不开的论题,当然Java由于有GC使得咱们不必手动请求和开释内存,可是了解Java内存分配是做内存优化的根底,假如不了解Java内存分配的常识,可能会带偏咱们内存优化的方向。所以这篇文章咱们以“一个目标占多少内存”为引子来谈谈Java内存分配。 文章依据JDK版别:1.8.0_191

文章标题提出的问题是”一个目标究竟占多少内存“,看似很简略,但想说清楚并不简略,期望本文的评论能让你有收成。

在开端之前我仍是决议先提一个从前阴魂不散,困扰我好久的问题,了解这个问题的答案有助于咱们了解接下来的内容。

Java虚拟机怎么在运转时知道每一块内存存储数据的类型的?

  • 咱们知道Java中int占4个字节,short占2个字节,引证类型在64位机器上占4个字节(不敞开指针紧缩是8个字节,指针紧缩是默许敞开的),那JVM怎么在运转时知道某一块内存存的值的类型是int仍是short或许其他根底类型,亦或许是引证的地址?比方以int为例,4个字节只够存储int数据本身,并没有剩余的空间存储数据的类型!

想答复这个问题,需求从字节码下手,还需求咱们了解一些Java虚拟机标准的常识, 来看再次学习Java——一个Java目标需求多少内存-必威体育betwayAPP_betway体育手机版_betway必威体育官网一个简略的比方

public class Apple extends Fruit{
private int color;
private String name;
private Apple brother;
private long create_time;
public void test() {
int color = this.color;
String name = this.name;
Apple brother = this.brother;
long create_time = this.create_time;
}
}

很简略的一个Apple类,承继于Fruit,有一个test办法,将类成员变量赋值给办法本地变量,仍是老套路,javac,javap 查看字节码

javac Fruit.java Apple.java
javap -verbose Apple.class
// 输出Apple字节码
public class com.company.alloc.Apple extends com.company.alloc.Fruit
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#25 // com/company/alloc/Fruit."":()V
#2 = Fieldref #8.#26 // com/company/alloc/Apple.color:I
#3 = Fieldref #8.#27 // com/company/alloc/Apple.name:Ljava/lang/String;
#4 = Fieldref #8.#28 // com/company/alloc/Apple.brother:Lcom/company/alloc/App再次学习Java——一个Java目标需求多少内存-必威体育betwayAPP_betway体育手机版_betway必威体育官网le;
#5 转正请求= Fieldref #8.#29 // com/company/alloc/Apple.create_time:J
// 省掉......
{
// 省掉......
public void test();
descriptor: ()V
flags: ACC_356mmPUBLIC
Code:
stack=4, locals=6, args_size=1
0: aload_0
1: getfield #2 // Field color:I
4: iconst_1
5: iadd
6: istore_1
7: aload_0
8: getfield #3 // Field name:Ljava/lang/String;
11: astore_2
12: aload_0
13: getfield #4 // Field brother:Lcom/company/alloc/Apple;
16: astore_3
17: aload_0
18: getfield #5 // Field create_time:J
21: ldc2_w #6 // long 3l
24: lsub
25: lstore 4
27: return
// 省掉......
}

咱们要点看Apple类的test办法,我现已增加了注释

 // 加载Apple目标本身到栈
0: aload_0
// 获取字段再次学习Java——一个Java目标需求多少内存-必威体育betwayAPP_betway体育手机版_betway必威体育官网,#2 对应常量池中的序列,
// #2 = Fieldref #8.#26 // com/company/alloc/Apple.color:I
// 存储的类型是int类型
1: getfield #2 // Field color:I
// 加载1这个常量进栈
4: iconst_1
// 履行加法
5: iadd
// 将栈顶的值存到本地变量表1的位一顾清辰置
6: istore_1
// 加载Apple目标本身到栈
7: aload_0
// 获取字段,#3 对应常量池中的序列,
8: getfield #3 // Field name:Ljava/lang/String;
// 将栈邵阳天气预报顶的值存到本地变量表2的方位
11: astore_2
// .......

能够看到关于目标的成员变量,会存在一个常量池,保存该目标所属类的一切字段的索引表,依据这个常量池能够查询到变量的类型,而字节码指令关于操作各种类型都有专门的指令,比方存储int是istore,存储目标是astore,存储long是lstore,所以指令是编译期现已确认了,虚拟机只需求依据指令履行就行,底子不关怀它操作的这个地址是什么类型的,所以也就不必额定的字段去存类型了,答复咱们前面提的问题!

咱们开端步入正题,要说内存分配,首要就要了解咱们分配的目标,那Java中分配的目标有哪些类型呢?

Java数据类型有哪些

在Java中数据类型分为二大类。

  • 根底数据类型(primitive type)
  • 引证类型 (reference type)

根底数据类型

Java中根底数据类型有8种,别离是byte(1), short(2), int(4), long(8), float(4), double(8), char(2), boolean(1), 括号里边是它们占用的字节数,所以关于根底数据类型,它们所占用的内存是很确认的,也就没什么好说的, 简略的回忆一下每种类型存储所需的字节数即可。

Java中根底数据类型是在栈上分配仍是在堆上分配? 咱们持续深究一下,根本数据类占用内存巨细是固定的,那详细是在哪分配的呢,是在堆仍是栈仍是办法区?咱们无妨想想看! 要答复这个问题,首要要看这个数据类型在哪里界说的,有以下三种状况。

  • 假如在办法体内界说的,这时分便是在栈上分配的
  • 假如是类的成员变量,这时分便是在堆上分配的
  • 假如是类的静态成员变量,在办法区上分配的

引证类型

引证类型跟根底数据类型不相同,除了目标本身之外,还存在一个指向它的引证(指针),指针占用的内存在64位虚拟机上8个字节,假如敞开指针紧缩是4个字节,默许是敞开了的。 为了便利阐明,仍是以代码为例

class Kata {
// str1和它指向的目标 都在堆上
String str1 = new String();
// str2和它指向的目标都在办法区上
static String str2 = new String();

public void methodTest() {
// str3 在栈上,它指向的目标在堆上(也有可能在栈上,后边会阐明)
String str3 = new String();
}
}

Java目标究竟占多大内存?

指针的长度是固定的,不去说它了,要点看它所指向的目标在内存中占多少内存。 Java目标有三大类

  • 类目标
  • 数组目标
  • 接口目标

Java虚拟机标准界说了目标类型在内存中的存储标准,由于现在根本都是64位的虚拟机,所以后边的评论都是依据64位虚拟机。 首要记住公式,目标由 目标头 + 实例数据 + padding填充字节组成,虚拟机标准要求目标所占内存有必要是8的倍数,padding便是干这个的

目标头

而Java中目标头由 Markword + 类指针kclass(该指针指向该类型在办法区的元类型) 组成。

Markword

Hotspot虚拟机文档 “oops/oop.hp”有对Markword字段的界说

 64 bits:
--------
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)

这儿简略解说下这几种object

  • normal object,初始new出来的目标都是这种状况
  • biased object,当某个目标被作为同步锁目标时,会有一个倾向锁,其实便是存储了持有该同步锁的线程id,关于倾向锁的常识这儿就不再赘述了,咱们能够自行查阅相关材料。
  • CMS promoted object 和 CMS free block 我也不清楚究竟是啥,可是看姓名好像跟CMS 废物收回器有关,这儿咱们也能够暂时疏忽它们

咱们首要重视normal object, 这种类型的Object的 Markword 一共是8个字节(64位),其间25位暂时没有运用,31位存储目标的hash值(留意这儿存储的hash值对依据目标地址算出来的hash值,不是重写hashcode办法里边的返回值),中心有1位没有运用,还有4位存储目标的age(分代收回中目标的年纪,超越15晋升入老时代),最终三位表明倾向锁标识和锁标识,首要便是用来区别目标的锁状况(未确定,倾向锁,轻量级锁,重再次学习Java——一个Java目标需求多少内存-必威体育betwayAPP_betway体育手机版_betway必威体育官网量级锁)

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

biased object的目标头Markword前54位来存储持有该锁的线程id,这样就没有空间存储hashcode了,所以 关于没有重写hashcode的目标,假如hashcode被再次学习Java——一个Java目标需求多少内存-必威体育betwayAPP_betway体育手机版_betway必威体育官网计算过并存储在目标头中,则该目标作为同步锁时,不会进入倾向锁状况,由于现已没当地存倾向thread id了,所以咱们在挑选同步锁目标时,最好重写该目标的hashcode办法,使倾向锁能够收效。

kclass

kclass存储的是该目标所属的类在办法区的地址,所以是一个指针,默许Jvm对指针进行了紧缩,用4个字节存储,假如不紧缩便是8个字节。 关于Compressed Oops的常识,咱们能够自行查阅相关材料来加深了解。 Java虚拟机标准要求目标所占空间的巨细有必要是8字节的倍数,之所以有这个规矩是为了进步分配内存的功率,咱们经过实例来做阐明

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

有一个Fruit类承继了Object类,咱们别离新建一个object和fruit,那他们别离占用多大的内存呢?

  • 先来看object目标,经过上面的常识,它的Markword是8个字节,kclass是4个字节, 加起来是12个字节,加上4个字节的对齐填充,所以它占用的空间是16个字节。
  • 再来看fruit目标,相同的,它的Markword是8个字节,kclass是4个字节,可是它还有个size成员变量,int类型占4个字节,加起来刚好是16个字节,所以不需求对齐填充。

那该怎么验证咱们的定论呢?究竟咱们仍是信任眼见为实!很走运Jdk供给了一个东西jol-core能够让咱们来剖析目标头占用内存信息。 jol的运用也很简略

// 打印目标头信息代码
System.out.println(ClassLayout.parseClass(Object.class).toPrintable());
System.out.println(ClassLayout.parseC郭博雄lass(Fruit.class).toPrintable());
//红花油 输出成果
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.aliosuwang.jol.Fruit object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 int Fruit.size N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

能够看到输出成果都是16 bytes,跟咱们前面的剖析成果共同。 除了类类型和接口类型的目标,Java中还有数组类型的目标,数组类型的目标除了上面表述的字段外,还有4个字节存储数组的长度(所以数组的最大长度是Integer.MAX)。所以一个数组目标占用双星之阴阳师的内存是 8 + 4 + 4 = 16个字节,当然这儿不包括数组内成员的内存。 咱们也运转验证一下。

String[] strArray = new String[0];
System.out.println(ClassLayout.pars滚滚红尘eClass(strArray.getClass()).toPrintable());
// 输出成果
[Ljava.lang.String; object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 16 (object header) N/A
16 0 java.lang.String String;. N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

输出成果object header的长度也是16,跟咱们剖析的共同。到这儿目标头部分的内存分配咱们就了解的差不多了,接下来看目标的实例数据部分。

目标的实例数据(成员变量)的分配规矩

为了便利阐明,咱们新建一个Apple类承继上面的Fruit类

public class Apple extends Fruit{
private int size;
private String name;
private Apple brother;
private long create_time;
}
// 打印Apple的目标散布信息
System.out.println(ClassLayout.parseClass(Apple.class).toPrintable());
// 输出成果
com.aliosuwang.jol.Apple object internals:
OFFSET SIZE TYPE DE花心SCRIPTION VALUE
0 12 (object header) N/A
12 4 int Fruit.size N/A
16 8 long Apple.create_time N/A
24 4 int Apple.size N/A
28 4 java.lang.String Apple.name N/A
32 4 com.company.alloc.Apple Apple.brother N/A
36 4 (loss due to the next object alignment)
Instance size: 40 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

能够看到Apple的目标头12个字节,然后别离是从Fruit类承继来的size特点(尽管Fruit的size是private的,仍是会被承继,与Apple本身的size共存),还有自己界说的4个特点,根底数据类型直接分配,目标类型都是存的指针占4个字节(默许都是敞开了指针紧缩),最终是40个字节,所以咱们new一个Apple目标,直接就会占用仓库中40个字节的内存,清楚目标的内存分配,让咱们在写代码时心中有数,应当时刻有内存优化的知道! 这儿又引出了一个小常识点,上面其完成已标示出来了。

父类的私有成员变量是否会被子类承继?

答案当然是必定的,咱们上面丝袜av剖析的Apple类,父类Fruit有一个private类型的size成员变量,Apple本身也有一个size成员变量,它们能够共存。留意划要点了,类的成员变量的私有拜访控制符private,仅仅编译器层面的约束,在实践内存中不论是私有的,仍是揭露的,都按规矩寄存在一起,对虚拟机来说并没有什么别离!

办法内部new的目标是在堆上仍是栈上?

咱们惯例的知道是目标的分配是在堆上,栈上会有个引证指向该目标(即存储它的地址),究竟是不是呢,咱们来做个实验! 咱们在循环内创立一亿个Apple目标,并记载循环的履行时刻,前面现已算过1个Apple目标占用40个字节,一共需求4GB的空间。

public static void main(String[] args) {
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
newApple();
}
System.out.println("take time:" + (System.currentTimeMillis() - startTim前妻的男人e) + "ms");
}
public static void newApple() {
new Apple();
}

咱们给JVM增加上-XX:+PrintGC运转装备,让编译器履行过程中输出GC的log日志

// 运转成果,没有输出任何gc的日志
take time:6ms

1亿个目标,6ms就分配完结,并且没有任何GC,明显假如目标在堆上分配的话是不行能的,其实上面的实例代码,Apple目标全部都是在栈上分配的,这儿要提出一个概念指针逃逸,new斗破天穹动漫Apple办法中新建的目标Apple并没有在外部被运用,所以它被优化为在栈上分配,咱们知道办法履行完结后该栈帧就会被清空,所以也就不会有GC。 咱们能够设置虚拟机的运转参数来测验一下。

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

在VM options里边增加上面二个参数,再运转一次

[GC (Allocation Failure) 236984K->440K(459776K), 0.0003751 secs]
[GC (Allocation Failure) 284600K->440K(516608K), 0.0004272 secs]
[GC (Allocation Failure) 341432K->440K(585216K), 0.0004835 secs]
[GC (Allocation Failure) 410040K->440K(667136K), 0.0004655 secs]
[GC (Allocation Failure) 491960K->440K(645632K), 0.0003837 secs]
[GC (Allocation Failure) 470456K->440K(625152K), 0.0003598 secs]
take time:5347ms

能够看到有许多GC的日志,并且运转的时刻也比之前长了许多,由于这时分Apple目标的分配在堆上,而堆是一切线程同享的,所以分配的时分必定有同步机制,并且触发了许多的gc,所以功率低许多。 总结一下: 虚拟机指针逃逸剖析是默许敞开的,目标不会逃逸的时分优先在栈上分配,否则在堆上分配。 到这儿,关于“一个目标占多少内存?”这个问题,现已能答复的适当全面了。可是究竟咱们剖析的仅仅Hotspot虚拟机,咱们无妨延伸一下,看在Android ART虚拟机上面的分配状况

获取Android ART虚拟机上面的目标头巨细

咱们前面运用了jol东西来输出目标头的信息,可是这个jol东西只能用在hotspot虚拟机上,那咱们怎么在Android上面获取目标头巨细呢?

办法的创意来历

办法必定是有的,我这儿介绍的办法,创意的主角便是AtomicInteger,我是遭到它的启示,这个类咱们知道是线程安全的int的包装类。它的完成原理是利用了Unsafe包供给的CAS才干,无妨看下它的源码完成

 private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
private static final long VALUE;
static {
try {
VALUE = U.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (ReflectiveOperationException e) {
throw new Error(e);
}
}
private volatile int value;
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1);
}

咱们知道一般int目标的++操作不是原子性的,AtomicInteger供给了getAndIncrement()它却能确保原子性,这一部分常识不是咱们这篇要讲的常识点,就不去说它们了。 getAndIncrement()办法内部调用了Unsafe目标的getAndAddInt()办法,第二个参数是VALUE,这个VALUE大有玄机,它表明成员变量在目标内存中的偏移地址,依据前面的常识,一般目标的结构 便是 目标头+实例数据+对齐字运城李明虎节,那假如咱们能获取到第一个实例数据的偏移地址,其实便是获得了目标头的字节巨细。

怎么拿到并运用Unsafe

由于Unsafe是不行见的类,并且它在初始化的时分有查看当时类的加载器,假如不是体系加载器会报错。可是好消息是,AtomicInteger中界说了一个Unsafe目标,并且是静态的,咱们能够直接经过反射来得到。

 public static Object getUnsafeObject() {
Class clazz = AtomicInteger.class;
try {
Field uFiled = clazz.getDeclaredField("U");
uFiled.setAccessible(true);
return uFiled.get(null);
}大中华1895 catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}

拿到了Unsafe,咱们就能够经过调用它的objectFieldOffset静态办法来获取成员变量的内存偏移地址。

 public static long getVariableOffset(Object target, String variableName) {
Object unsafeObject = getUnsafeObject();
if (unsafeObject != null) {
try {
Method method = unsafeObject.getClass().getDeclaredMethod("objectFieldOffset", Field.class);
method.setAccessible(true);
Field targetFiled = target.getClass().getDeclaredField(variableName);
return (long) method.invoke(unsafeObject, targetFiled);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (Invocatio再次学习Java——一个Java目标需求多少内存-必威体育betwayAPP_betway体育手机版_betway必威体育官网nTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldExceptio街坊也张狂n e) {
e.printStackTrace();
}
}
ret再次学习Java——一个Java目标需求多少内存-必威体育betwayAPP_betway体育手机版_betway必威体育官网urn -1;
}
public static void printObjectOffsets(Object target) {
Class targetClass = target.getC邓拥军lass();
Field[] fields = targetClass.getDeclaredFields();
for (Field field : fields) {
String name = field.getName();
Log.d("offset", name + " offset: " + getVariableOffset(target, name));
}
}

咱们来运用上面的东西测验打印之前的Fruit和Apple,

 Log.d("offset", "------start print fruit offset!------");
Utils.printObjectOffsets(new Fruit());
Log.d("offset", "------start print apple offset!------");
Utils.printObjectOffsets(new Apple());
// 输出成果 (Android 8.0模拟器)
offset: ------start print fruit offset!------
offset: size offset: 8
offset: ------start print apple offset!------
offset: brother offset: 12
offset: create_time offset: 24
offset: id offset: 20
offset: name offset: 16

经过输出成果,看出在 Android8.0 ART 虚拟机上,目标头的巨细是8个字节,这跟hotspot虚拟机不同(hotspot是12个字节默许敞开指针紧缩),依据输出的成果现在只发现这一点不同,各种数据类型占用的字节数都是相同的,比方int占4个字节,指针4个字节,long8个字节等,都一王效政样。

总结

全文咱们总结了以下几个常识点

  • Java虚拟机经过字节码指令来操作内存,所以能够说它并不关怀数据类型,它仅仅按指令行事,不同类型的数据有不同的字节码指令。
  • Java中根本数据类型和引证类型的内存分配常识,要点剖析了引证类型的目标头,并介绍了JOL东西的运用
  • 延伸到Android渠道,介绍了一种获取Android中目标的目标头信息的办法,并对比了ART和Hotspot虚拟机目标头长度的不同。

了解这些并不是为了装逼炫技,说实话,写代码做工程的没什么好装的,用的都是他人的轮子,我只会感谢我知道这些还不算太晚,所以我把它们写出来共享给咱们。

最终仍是那句话:只要充沛的了解Java的内存分配机制,才干正确的去做内存优化!!