划分对象两种方式:
1.指针碰撞: Java堆内存规整的情况下使用
2.空闲列表:Java堆内存不规整的情况下使用
JVM中分配对象:
本地线程分配缓冲
Thread Local Allocation Buffer, TLAB (Eden 1%)
栈----堆中预先分配一块很小私有区域。
CAS比较和交换,确保原子性问题。
对象内存布局
在HostSpot虚拟机中,对象在内存中存储布局可以分为3个部分:对象头(Header)、实例数据(Instance Data)和对其填充(Padding)。
对象头:包括两部分信息,
第一部分存储本身运行时数据,如哈希码(hashcode)、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等。
另一部分是类类型指针,及对象指向类元素的指针,虚拟机通过这个指针确定这个类是哪个对象的实例。
对其填充:对象的大小必须是8个字节。实例数据7个字节 填充1个,1个填充7个
对象访问方式
1.句柄(特殊的指针),移动方便,访问效率低
2.直接指针(HotSpot中使用)
堆内存分配策略
新生代Eden区
Survivor(from)区,设置Survivor是为了减少送到老年代的对象
Survivor(to)区
设置两个Survivor区是为了解决碎片化问题(复制回收算法)
1.对象优先在Eden区分配
-Xms:20m堆空间20m
-Xmx:20m 堆最大空间20m
-Xmn:10m 新生代(Eden区)10m
-XX:+PrintGCDetails 打印GC日志
-XX:+UseSeriolGC 一个垃圾回收器种类
-XX:PretenureSizeThreshold=2m 超过2M的对象可以直接进入老年代
注:大多数情况对象是在Eden区分配,当Eden区空间不足,虚拟机将会发起一次Minor GC。
2.大对象直接进入老年代
目的:1.避免大量内存复制2.避免提前进行垃圾回收,明明内存有空间分配
3.长期存活对象进入老年代
Eden区8m占满,再分配1m对象,Eden区会发生MinGC.
存活对象进入from区,年龄+1,再来垃圾回收则进入to区,年龄再+1。
再次进行垃圾回收,对象返回from区,年龄再+1, from和to区反复,因为from和to区采用复制回收算法的原因。
年龄达到15岁(默认),属于长期存活对象,进入老年代。
4.动态年龄判断
from和to区年龄所有对象大小加起来大于from年龄的一半,年龄大于等于该年龄的对象就可以直接进入老年代。
5.空间分配担保
HandlePromotionFailure, 不用考虑老年代空间不够,不用考虑发生FullGC,如果担保失败或内存不够也会进行一次FullGC
FullGC:当老年代空间不足时候,有from或to区升级进入老年代的时候,将会执行FullGC
在进行Minor GC之前,JVM首先会检查【老年代最大连续空闲空间】是否大于【当前新生代所有对象占用的总空间】
如果是,那么说明此次的Minor GC是安全的,可以放心的进行Minor GC
如果不是,则JVM会去查看HandlePromotionFailure参数的值是否为true(表示是否允许担保失败)
如果不允许担保失败,则此时就会进行一次Full GC 以腾出老年代更多的空间
实战分析
通过上面这些内容介绍,就是尽可能让对象都在新生代里分配和回收,尽量别 让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃 圾回收。
1. 什么是java对象的指针压缩?
- jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩
- jvm配置参数:UseCompressedOops,compressed压缩、oop(ordinary object pointer)对象指针
- 启用指针压缩:XX:+UseCompressedOops(默认开启),禁止指针压缩:XX:UseCompressedOops
为什么要进行指针压缩?
- 在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据, 占用较大宽带,同时GC也会承受较大压力
- 为了减少64位平台下内存的消耗,启用指针压缩功能
- 在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得jvm 只用32位地址就可以支持更大的内存配置(小于等于32G)
- 堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
- 堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内 存不要大于32G为好
2. 逃逸分析、标量替换
对象逃逸分析: 就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参 数传递到其他地方中。
public User test1() {
User user = new User();
user.setId(1);
user.setName("andy");
//TODO 保存到数据库
return user;
}
// 没有逃出本方法外部,即为逃逸
public void test2() {
User user = new User();
user.setId(1);
user.setName("andy");
//TODO 保存到数据库
}
很显然test1方法中的user对象被返回了,这个对象的作用域范围不确定,test2方法中的user对象我们可以确定当方法结 束这个对象就可以认为是无效对象了,对于这样的对象我们其实可以将其分配在栈内存里,让其在方法结束时跟随栈内 存一起被回收掉。
JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优 先分配在栈上(栈上分配),JDK7之后默认开启 逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)
标量替换: 通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该 对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就 不会因为没有一大块连续空间导致对象内存不够分配。开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认 开启 。
栈上分配示例:
/**
* 栈上分配,标量替换
* 代码调用了1亿次alloc(),如果是分配到堆上,大概需要1GB以上堆空间,如果堆空间小于该值,必然会触发GC。
*
* 使用如下参数不会发生GC
* ‐Xmx15m ‐Xms15m ‐XX:+DoEscapeAnalysis ‐XX:+PrintGC ‐XX:+EliminateAllocations
* 使用如下参数都会发生大量GC
* ‐Xmx15m ‐Xms15m ‐XX:‐DoEscapeAnalysis ‐XX:+PrintGC ‐XX:+EliminateAllocations
* ‐Xmx15m ‐Xms15m ‐XX:+DoEscapeAnalysis ‐XX:+PrintGC ‐XX:‐EliminateAllocations
*/
public class AllotOnStack {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
alloc();
}
long end = System.currentTimeMillis();
System.out.println(end ‐ start);
}
private static void alloc() {
User user = new User();
user.setId(1);
user.setName("andy");
}
}
栈上分配依赖于逃逸分析和标量替换