天天都是面对对象编程,你真的了解你的对象吗?
Java是一种面向对象的编程语言,详细自己对对象的理解是否只有一句话来描述:一切皆对象,new出来的对象都在堆上!等等,这不是2句话?不,后面这句只是我写这篇文章的原由。初学Java大家都说new出来的对象都在堆上,对此深信不疑!但是后续越发对这句话产生怀疑,想想每个类的toString方法都会new一个StringBuffer,这样做堆内存岂不是增大一倍?For循环中创建对象为什么没有堆溢出?创建的对象到底在堆中占用多少内存?怀着以上疑问往下看,本篇文章作为Java对象的综合整理来描述何谓对象。
Java中一切皆对象,对象的创建主要如下:
People people = new People();
现在面试都是各种文字坑,例如:问这个对象是否在堆上分配内存?怎么回答,是?不是?
这个问题,要根据上下文来回答,就是要根据这行代码所处的环境来回答,何谓环境:运行环境JRE、书写位置,不同环境结果不一样。想知道结果,先Get到以下知识点:
逃逸分析是JDK6+版本后默认开启的技术(现在都JDK15了,都是旧技术了==!),主要分析方法内部的局部变量的引用作用域,用于做后续优化。逃逸分析之后一个方法内的局部变量被分为3类逃逸对象
- 全局逃逸对象: 对外部而言,该对象可以在类级别上直接访问到(调用类获取对象实例)
- 参数逃逸对象:对外部而言,该对象可以在方法级别上直接访问到(调用方法获取对象实例)
- 未逃逸对象:对外部而言,该对象仿佛不存在一样,不可嗅探
后续优化指的是对未逃逸的优化,主要分为标量替换和锁消除
标量替换:在Java中8种基本数据类型已经是可以直接分配空间的,不可再被细化,称为标准变量,简称标量。对象的引用是内存地址也不可再被细化,也可以称为标量。而Java对象则是由多个标量聚合而来,称为聚合量。按照这种标准将Java对象的成员变量拆分替换为标量的过程,称为标量替换。这个过程会导致对象的分配不一定在堆中,而是在栈上或者寄存器中。
锁消除:Java锁是针对多线程而使用的,当在单线程环境下使用锁后被JIT编译器优化后就会移除掉锁相关代码,这个过程就是锁消除(属于优化,不影响对象)。
指针压缩:32位机器对象的引用指针使用32位表示,在64位使用64位表示,同样的配置而内存占用增多,这样真的好吗?JDK给出指针优化技术,将64位(8字节)指针引用(Refrence类型)压缩为32位(4字节)来节省内存空间。
对象的逃逸
一个标准大小=32byte的Java对象(后面会写如何计算)
class People {
int i1;
int i2;
int i3;
byte b1;
byte b2;
String str;
}
未逃逸对象
public class EscapeAnalysis {
public static void main(String[] args) throws IOException {
// 预估:在不发生GC情况下32M内存
for (int j = 0; j < 1024 * 1024; j++) {
unMethodEscapeAnalysis();
}
// 阻塞线程,便于内存分析
System.in.read();
}
/**
* people对象引用作用域未超出方法作用域范围
*/
private static void unMethodEscapeAnalysis() {
People people = new People();
// do something
}
}
未开启逃逸分析
启动JVM参数
-server -Xss108k -Xmx1G -Xms1G -XX:+PrintGC -XX:-UseTLAB -XX:-DoEscapeAnalysis -XX:-EliminateAllocations
启动控制台:无输出:未发生GC
堆内存查看
$ jps
3024 Jps
16436 EscapeAnalysis
24072 KotlinCompileDaemon
$ jmap -histo 16436
num #instances #bytes class name
----------------------------------------------
1: 1048576 33554432 cn.tinyice.demo.object.People
2: 1547 1074176 [B
3: 6723 1009904 [C
4: 4374 69984 java.lang.String
此时堆中共创建了1024*1024个实例,每个实例32byte,共32M内存
开启逃逸分析
启动JVM参数
-server -Xss108k -Xmx1G -Xms1G -XX:+PrintGC -XX:-UseTLAB -XX:-DoEscapeAnalysis -XX:-EliminateAllocations
启动控制台:无输出:未发生GC
堆内存查看
$ jps
3840 Jps
24072 KotlinCompileDaemon
25272 EscapeAnalysis
$ jmap -histo 25272
num #instances #bytes class name
----------------------------------------------
1: 1048576 33554432 cn.tinyice.demo.object.People
2: 1547 1074176 [B
3: 6721 1009840 [C
4: 4372 69952 java.lang.String
此时与未开启一致,仍然是在堆中创建了1024*1024个实例,每个实例32byte,共32M内存
开启逃逸分析和标量替换
启动JVM参数
-server -Xss108k -Xmx1G -Xms1G -XX:+PrintGC -XX:-UseTLAB -XX:+DoEscapeAnalysis -XX:+EliminateAllocations
堆内存查看
$ jps
7828 Jps
21816 EscapeAnalysis
24072 KotlinCompileDaemon
$ jmap -histo 21816
num #instances #bytes class name
----------------------------------------------
1: 92027 2944864 cn.tinyice.demo.object.People
2: 1547 1074176 [B
3: 6721 1009840 [C
4: 4372 69952 java.lang.String
此时堆中仅创建了92027个实例,内存占用少了11倍。
启动控制台:无输出:未发生GC,说明实例的确未分配到堆中
未分配到堆中,是因为一部分分配到了栈中,这种未逃逸对象如果分配到栈上,则其生命周期随栈一起,使用完毕自动销毁。下面为java对象分配的具体细节。
对象的内存分配
实例分配原则
-
尝试栈上分配
-
尝试TLAB
-
尝试老年代分配(堆分配原则)
-
以上都失败时(注意分配对象时很容易触发GC,堆分配原则)
堆分配原则:
-
优先在Eden(伊甸园)区进行分配
-
长期存活的对象移交老年代(永久代)
-
在Eden的对象经过一次Minor GC进入Survivo 区后,对象的对象头信息年龄字段Age+1
-
Survivor区对象每经过一次Minor GC对象头信息年龄字段Age+1
-
当对象的年龄达到一定值(默认15岁)时就会晋升到老年代
-
-XX:MaxTenuringThreshold=15设置分代年龄为15
-
大对象直接进入老年代(永久代)
-
动态年龄判断
-
老年代对象分配使用空间分配担保
对象实例组成
-
对象头
-
MarkWord(必须)
-
类型指针:指向对象的类元数据(非必须)
-
数组长度(数组类型对象才有)
-
实例数据
-
数据填充
MarkWord结构
|
25Bit
|
4bit
|
1bit
|
2bit
|
锁状态
|
|
23bit
|
2bit
|
偏向锁
|
锁标志
|
|
哈希码
|
分代年龄 |
0
|
01
|
无锁
|
|
指向锁记录的指针
|
00
|
轻量锁
|
|
指向重量锁的指针
|
10
|
重量锁
|
|
空
|
11
|
GC标记
|
|
线程ID
|
时间戳
|
分代年龄
|
1
|
01
|
偏向锁
|
对象的初始化
由于对象初始化涉及到类加载,这里不多描述
-
分配到的空间设置为0
-
数据填充0,8字节对齐
-
对象头信息设置
-
调用<init>进行初始化(类的实例化)
给个示例先体会下
public class ClinitObject {
static ClinitObject clinitObject;
static {
b = 2;
clinitObject = new ClinitObject();
System.out.println(clinitObject.toString());
}
int a = 1;
static int b;
final static int c = b;
final static String d = new String("d");
String e = "e";
String f = "f";
public ClinitObject() {
e = d;
a = c;
}
@Override
public String toString() {
return "ClinitObject{" + "\n" +
"\t" + "a=" + a + "\n" +
"\t" + "b=" + b + "\n" +
"\t" + "c=" + c + "\n" +
"\t" + "d=" + d + "\n" +
"\t" + "e=" + e + "\n" +
"\t" + "f=" + f + "\n" +
'}';
}
public static void main(String[] args) {
System.out.println(clinitObject.toString());
}
}
控制台
ClinitObject{
a=0
b=2
c=0
d=null
e=null
f=f
}
ClinitObject{
a=0
b=2
c=2
d=d
e=null
f=f
}
对象的大小计算
|
数据类型
|
占用空间(byte)
|
|
byte
|
1
|
|
short
|
2
|
|
int
|
4
|
|
long
|
8
|
|
char
|
2
|
|
float
|
4
|
|
double
|
8
|
|
boolean
|
1或4 ,计算大小时为1,判断真假时为4(底层为int常量0,1)
|
|
Object(存储的是引用指针)
|
由计算机位数和是否指针压缩决定 4或8 字节
|
对象的定位
java源码中调用对象在JVM中是通过虚拟机栈中本地变量标的reference来指向对象的引用来定位和访问堆中的对象的,访问方式存在主流的2种
-
句柄访问
-
直接访问(Sun HotSpot 采用该方式)
指针压缩示例
public class CompressedClassPointer {
public static void main(String[] args) throws IOException {
People people=new People();
System.in.read();
}
}
启用指针压缩(默认)
JVM参数
-server -XX:+UseCompressedOops -XX:+UseCompressedClassPointers -XX:CompressedClassSpaceSize=1G
堆内存查看
$ jps
11440
11744 RemoteMavenServer
14928 KotlinCompileDaemon
15540 Launcher
15908 Jps
9996 CompressedClassPointer
$ jmap.exe -histo 9996
num #instances #bytes class name
----------------------------------------------
...
233: 1 32 cn.tinyice.demo.object.People
关闭指针压缩
JVM参数
-server -XX:-UseCompressedOops
堆内存查看
$ jps
11440
11744 RemoteMavenServer
14928 KotlinCompileDaemon
8448 CompressedClassPointer
$ jmap.exe -histo 8448
num #instances #bytes class name
----------------------------------------------
...
254: 1 40 cn.tinyice.demo.object.People
示例解析
示例中开启之后对象大小会减少8byte。而指针压缩是8字节变4字节,按理说应该少4字节即32位,为什么这个样子?
开启压缩指针时的对象大小计算
/**
* Size(People) =
* 8(mark word)+4(klass reference)+ 4(i1)+4(i2)+4(i2)+1(b1)+1(b2)+4(str reference) + 2(padding)
* |----------------------------------- 30 byte ---------------------------------|----00-------/
* |---------------------------------------- 32 byte ------------------------------------------/
*/
关闭压缩指针时的对象大小计算
/**
* Size(People) =
* 8(mark word)+8(klass reference)+ 4(i1)+4(i2)+4(i2)+1(b1)+1(b2)+8(str reference) + 2(padding)
* |----------------------------------- 38 byte ---------------------------------|----00-------/
* |---------------------------------------- 40 byte ------------------------------------------/
*/
这里就看到区别了,是数据填充造成的,java为了便于数据管理,于是对象都是8字节对齐的,不足的使用0进行填充(padding)。
至于对象的实例化,会在写类加载流程是再做描述。
原文地址: 程序员微录 天天都是面对对象编程,你真的了解你的对象吗?