$ 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对象分配的具体细节。
实例分配原则
- 尝试栈上分配
-
基于逃逸分析和标量替换,将线程私有对象直接分配在栈上
-
在函数调用完毕后自动销毁对象,不需要GC回收
-
栈空间很小,默认108K,不能分配大对象
- 尝试TLAB
-
判断是否使用TLAB(Thread Local Allocation Buffer)技术
-
虚拟机参数 -XX:+UseTLAB,-XX:-UseTLAB,默认开启
-
虚拟机参数-XX:TLABWasteTargetPercent 来设置TLAB占用eEden空间百分比,默认1%
-
虚拟机参数-XX:+PrintTLAB 打印TLAB的使用情况
-
TLAB本身占用eEden区空间,空间很小不能存放大对象,
-
每个线程在Java堆中预先分配了一小块内存,当有对象创建请求内存分配时,就会在该块内存上进行分配
-
使用线程控制安全,不需要在Eden区通过同步控制进行内存分配
- 尝试老年代分配(堆分配原则)
- 如果可以直接进入老年代,直接在老年代分配
- 以上都失败时(注意分配对象时很容易触发GC,堆分配原则)
-
内存连续时:使用指针碰撞(Serial、ParNew等带Compact过程的收集器)
-
分配在堆的Eden区,该区域内存连续
-
指针始终指向空闲区的起始位置。
-
在新对象分配空间后,指针向后移动了该对象所占空间的大小个单位,从而指向新的空闲区的起始位置
-
对象分配过程中使用了CAS加失败重试的方式来保证线程安全(CAS即原子操作)
-
如果成功:则进行对象头信息设置
-
内存不连续时:使用空闲列表(CMS这种基于Mark-Sweep算法的收集器)
-
如果堆空间不是连续的,则JVM维护一张关系表,来使内存逻辑上连续从而达到对象分配的目
堆分配原则:
-
优先在Eden(伊甸园)区进行分配
-
可通过-XX:SurvivorRation=8来确定Eden与Survivor比例为 8:1
-
新生代存在2个Survivor区域(From和To),当新生代10份时,Survivor共占2份,Eden占8份
-
新建对象会先在Eden中分配
-
空间足够时直接分配
-
当Eden空间不足时
-
将Eden内的对象进行一次Minor Gc 回收准备放入进入From类型的Survivor区
-
From类型的Survivor区
-
空间足够时,放置GC对象时将GC对象回收进来
-
空间不足时,将GC对象直接放入老年代中
-
Minor GC后Eden空间仍然不足
-
新建对象直接进入老年代
-
长期存活的对象移交老年代(永久代)
-
在Eden的对象经过一次Minor GC进入Survivo 区后,对象的对象头信息年龄字段Age+1
-
Survivor区对象每经过一次Minor GC对象头信息年龄字段Age+1
-
会在From Survivor和ToSurvivor 区进行来回GC(复制算法)
-
当对象的年龄达到一定值(默认15岁)时就会晋升到老年代
-
-XX:MaxTenuringThreshold=15设置分代年龄为15
-
大对象直接进入老年代(永久代)
-
大对象为占用堆内大量连续空间的对象(数组类、字符串)
-
-XX:MaxTenuringThreshold=4M 可以设置大于4M的对象直接进入老年代
-
动态年龄判断
-
GC回收对象时并不一定必须严格要求分代年龄进行晋升老年代
-
当Survivor区的同年龄对象的总和大于Survivor空间1/2时
-
年龄大于等于该年龄(相同年龄)的对象都可以直接进入老年代
-
老年代对象分配使用空间分配担保
-
新生代所有对象大小小于老年代可用空间大小时,Minor GC是安全的
-
相当于新生代所有对象都可以放到老年代里面,因而不会出现溢出等现象
-
相反,Minor GC是不安全的
-
相当于新生代对象只能有一部分可以放入老年代,另一部分会因为空间不足而放入失败
-
安全措施-XX:HandlePromotionFailure=true,允许担保失败
-
发生MinorGC之前,JVM会判断之前每次晋升到老年代的平均大小是否大于老年代剩余空间的大小
-
若小于于并且允许担保失败则进行一次Minor GC
-
对象GC预测平稳,不会发生大量对象突然进入老年代导致其空间不足而溢出
-
若小于并且不允许担保失败则进行一次full GC
-
即使对象GC预测平稳,但是不保证不会激增,所以安全点还是先去Full GC下
-
回收所有区域,给老年代清理出更多空间
-
若小于即使允许担保失败也进行一次full GC
-
- 即Minor GC后的存活对象数量突然暴增,即使允许担保失败但是还是极大可能是不安全的
-
回收所有区域,给老年代清理出更多空间
-
对象头
-
MarkWord(必须)
-
类型指针:指向对象的类元数据(非必须)
-
数组长度(数组类型对象才有)
-
实例数据
-
对象的字段属性,方法等,存储在堆中
-
数据填充
-
JVM要求java的对象占的内存大小应该是8bit的倍数
-
实例数据有可能不是8的倍数,需要使用0进行填充对齐
MarkWord结构
由于对象初始化涉及到类加载,这里不多描述
-
分配到的空间设置为0
-
数据填充0,8字节对齐
-
对象头信息设置
-
调用进行初始化(类的实例化)
给个示例先体会下
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
}
-
普通对象
-
4或8字节(MarkWord)+4或8字节(klass Reference)+实例数据长度+ 0填充(Padding)
-
数组对象
-
4或8字节(MarkWord)+4或8字节(klass Reference)+4字节(ArrayLength)+实例数据长度+0填充(Padding)
-
其它说明:
-
对象头(MarkWord)在32位JVM中为4字节,在64位JVM中为8字节
-
为了节约空间,使用了指针压缩技术:
-
JDK6开始对类型指针(Reference)进行压缩,压缩前8字节,压缩后4字节
-
参数 -XX:+UseCompressedOops
-
JDK8开始新增元数据空间metaSpace,于是新增参数来控制指针压缩:
-
-XX:+UseCompressedClassPointers(指针压缩开关,堆内存>=32G时,自动关闭)
-
-XX:CompressedClassSpaceSize (Reference指向的类元数据空间大小,默认1G,上限32G)
-
数据填充(Padding)为保证对象大小为8的整数倍的数据填充,使数据对齐
-
常用数据类型大小
java源码中调用对象在JVM中是通过虚拟机栈中本地变量标的reference来指向对象的引用来定位和访问堆中的对象的,访问方式存在主流的2种
-
句柄访问
-
jvm堆中单独维护一张reference与对象实例数据(实例化数据)和对象类型数据(ClassFile数据)的关系表
-
通过该关系表来查找到java实例对象
-
直接访问(Sun HotSpot 采用该方式)
-
reference直接指向了java堆中对象的实例数据(实例化数据),该实例对象的类型指针(Reference)指向类型数据(ClassFile数据)
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
堆内存查看