对象创建与内存分配机制深度剖析
创建对象流程图
类加载检查
在代码中创建对象时,都是使用的new关键字,new 关键字对应的指令首先会去常量池中定位对应类的符号引用,并检查这个符号引用的类是否被加载过
分配内存
通过类加载检查后,接下来虚拟机将为新生的对象分配内存,对象所需要的内存在类加载完成后即可确定,分配对象的内存,即就是在堆区划分相同大小的内存出来。
对象分配内存存在两个问题:
1.是以什么方式进行分配内存?
2.为对象分配内存时如何解决并发问题?
分配内存方法:
1.指针碰撞:
指针碰撞,指的是Java堆的绝对规整,已经被使用的内存区间放在一边,未使用的内存区间放在另外一边,两块区域由指针分割,在为对象分配内存空间只需要修改指针的位置
2.空闲列表:
空闲列表指的是,Java堆内存空间并不规整,存储对象比较分散,虚拟机必须维护一张空闲列表,记录哪些空间可以使用,哪些空间已被使用,在对象分陪内存时在列表中找到足够大小的内存空间进行存放,更新空闲列表的记录即可
解决并发问题方案
- CAS
使用cas来解决分配内存并发问题 - 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
每个线程在堆中分配一个对应的内存空间,用于存放新new出的对象,该内存空间是线程私有的,互不影响 -XX:TLABSize 指定TLAB大小
初始化
初始化指的是为对象中的属性赋数据类型所对应的零值,以供在不赋初始值也可以提供使用,如果使用的TLAB,初始化阶段提前至TLAB分配时进行
对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对 象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data) 和对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈 希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分 是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
执行 init 方法
执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋 零值不同,这是由程序员赋的值),和执行构造方法。
对象大小与指针压缩
对象大小可以用jolcore包查看,引入依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol‐core</artifactId>
<version>0.9</version>
</dependency>
package com.fas.system;
import org.openjdk.jol.info.ClassLayout;
public class Test {
public static void main(String[] args) {
ClassLayout layout = ClassLayout.parseInstance(new Object());
System.out.println(layout.toPrintable());
System.out.println();
ClassLayout layout1 = ClassLayout.parseInstance(new int[]{});
System.out.println(layout1.toPrintable());
System.out.println();
ClassLayout layout2 = ClassLayout.parseInstance(new A());
System.out.println(layout2.toPrintable());
}
public static class A {
//8B mark word 26 //4B Klass Pointer 如果关闭压缩‐XX:‐UseCompressedClassPointers或‐XX:‐UseCompressedOops,则占用8B 27
int id; //4B
String name; //4B 如果关闭压缩‐XX:‐UseCompressedOops,则占用8B
byte b; //1B
Object o; //4B 如果关闭压缩‐XX:‐UseCompressedOops,则占用8B 31
}
}
//执行结果(开启指针压缩)
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
[I object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800016d
12 4 (array length) 0
12 4 (alignment/padding gap)
16 0 int [I.<elements> N/A
Instance size: 16 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
com.fas.system.Test$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800cd26
12 4 int A.id 0
16 1 byte A.b 0
17 3 (alignment/padding gap)
20 4 java.lang.String A.name null
24 4 java.lang.Object A.o null
28 4 (object alignment gap)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total
//执行结果(关闭指针压缩)
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 8 (object header: class) 0x0000022dbd4b1c00
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
[I object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 8 (object header: class) 0x0000022dbd4b0b68
16 4 (array length) 0
16 8 (alignment/padding gap)
24 0 int [I.<elements> N/A
Instance size: 24 bytes
Space losses: 8 bytes internal + 0 bytes external = 8 bytes total
com.fas.system.Test$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 8 (object header: class) 0x0000022dbdb827a0
16 4 int A.id 0
20 1 byte A.b 0
21 3 (alignment/padding gap)
24 8 java.lang.String A.name null
32 8 java.lang.Object A.o null
Instance size: 40 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total
什么是java对象的指针压缩?
1.jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩
2.jvm配置参数:UseCompressedOops,compressed压缩、oop(ordinary object pointer)对象指针
3.启用指针压缩:XX:+UseCompressedOops(默认开启),禁止指针压缩:XX:UseCompressedOops
为什么要进行指针压缩?
1.在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据, 占用较大宽带,同时GC也会承受较大压力
2.为了减少64位平台下内存的消耗,启用指针压缩功能
3.在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得jvm 只用32位地址就可以支持更大的内存配置(小于等于32G)
4.堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
5.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内 存不要大于32G为好
对象内存分配
对象内存分配流程图:
栈上分配
栈上分配,顾名思义就是将对象分配在栈空间中,栈上分配,不是什么时候都会出现,JVM通过逃逸分析机制分析对象是否会逃逸,如果出现逃逸则不会进行栈上分配,是为了减少GC的压力,将一些临时对象分配在栈空间中,当方法调用完成临时对象也会被回收。
逃逸分析
逃逸分析即是分析对象是否逃离它的作用域,它可能被外部方法所引用,例如作为调用参 数传递到其他地方中
public class test{
//未出现逃逸
public void A(){
Test test = new Test();
}
//出现逃逸
public Test A1(){
Test test = new Test();
return test;
}
}
JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优先分配在栈上(栈上分配),JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)
标量替换
标量替换指的是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认开启。
例如:会将下面的Test类分解成 int a,int b 直接使用
public class Test{
int a ;
int b;
}
标量与聚合量
标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及 reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一 步分解的聚合量。
对象在Eden区分配
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。我 们来进行实际测试一下。 在测试之前我们先来看看 Minor GC和Full GC 有什么不同呢?
Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
Major GC/Full GC:一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢 10倍以上。
Eden与Survivor区默认8:1:1 大量的对象被分配在eden区,eden区满了后会触发minor gc,可能会有99%以上的对象成为垃圾被回收掉,剩余存活 的对象会被挪到为空的那块survivor区,下一次eden区满了后又会触发minor gc,把eden区和survivor区垃圾对象回 收,把剩余存活的对象一次性挪动到另外一块为空的survivor区,因为新生代的对象都是朝生夕死的,存活时间很短,所 以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可, JVM默认有这个参数
-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变 化可以设置参数-XX:-UseAdaptiveSizePolicy
package com.newtouch.cloud.platform.interceptor;
//添加运行JVM参数: ‐XX:+PrintGCDetails
public class Test {
public static void main(String[] args) {
byte[] allocation1, allocation2/*, allocation3, allocation4, allocation5, allocation6*/;
allocation1 = new byte[60000*1024];
//allocation2 = new byte[8000*1024]; /*allocation3 = new byte[1000*1024]; allocation4 = new byte[1000*1024]; allocation5 = new byte[1000*1024]; allocation6 = new byte[1000*1024];*/
}
}
//GC日志
Heap
PSYoungGen total 76288K, used 65536K [0x000000076b400000, 0x0000000770900000, 0x00000007c0000000)
eden space 65536K, 100% used[0x000000076b400000,0x000000076f400000,0x000000076f400000) from space 10752K, 0% used [0x000000076fe80000,0x000000076fe80000,0x0000000770900000)
to space 10752K, 0% used [0x000000076f400000,0x000000076f400000,0x000000076fe80000)
ParOldGen total 175104K, used 0K [0x00000006c1c00000, 0x00000006cc700000, 0x000000076b400000)
object space 175104K, 0% used [0x00000006c1c00000,0x00000006c1c00000,0x00000006cc700000)
Metaspace used 3342K, capacity 4496K, committed 4864K, reserved 1056768K 25 class space used 361K, capacity 388K, committed 512K, reserved 1048576K
我们可以看出eden区内存几乎已经被分配完全(即使程序什么也不做,新生代也会使用至少几M内存)。假如我们再为 allocation2分配内存会出现什么情况呢?
package com.newtouch.cloud.platform.interceptor;
//添加运行JVM参数: ‐XX:+PrintGCDetails
public class Test {
public static void main(String[] args) {
byte[] allocation1, allocation2/*, allocation3, allocation4, allocation5, allocation6*/;
allocation1 = new byte[60000*1024];
allocation2 = new byte[8000*1024]; /*allocation3 = new byte[1000*1024]; allocation4 = new byte[1000*1024]; allocation5 = new byte[1000*1024]; allocation6 = new byte[1000*1024];*/
}
}
//GC日志
[GC (Allocation Failure) [PSYoungGen: 65253K‐>936K(76288K)] 65253K‐>60944K(251392K), 0.0279083 secs] [Times: user=0.13 sys=0.02, real=0.03 secs]
Heap
PSYoungGen total 76288K, used 9591K [0x000000076b400000, 0x0000000774900000, 0x00000007c0000000)
eden space 65536K, 13% used [0x000000076b400000,0x000000076bc73ef8,0x000000076f400000)
from space 10752K, 8% used [0x000000076f400000,0x000000076f4ea020,0x000000076fe80000)
to space 10752K, 0% used [0x0000000773e80000,0x0000000773e80000,0x0000000774900000)
ParOldGen total 175104K, used 60008K [0x00000006c1c00000, 0x00000006cc700000, 0x000000076b400000) object space 175104K, 34% used [0x00000006c1c00000,0x00000006c569a010,0x00000006cc700000)
Metaspace used 3342K, capacity 4496K, committed 4864K, reserved 1056768K 26 class space used 361K, capacity 388K, committed 512K, reserved 1048576K
简单解释一下为什么会出现这种情况: 因为给allocation2分配内存的时候eden区内存几乎已经被分配完了,我们刚刚讲 了当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,GC期间虚拟机又发现allocation1无法存入 Survior空间,所以只好把新生代的对象提前转移到老年代中去,老年代上的空间足够存放allocation1,所以不会出现 Full GC。执行Minor GC后,后面分配的对象如果能够存在eden区的话,还是会在eden区分配内存。可以执行如下代码
package com.newtouch.cloud.platform.interceptor;
//添加运行JVM参数: ‐XX:+PrintGCDetails
public class Test {
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6;
allocation1 = new byte[60000*1024];
allocation2 = new byte[8000*1024];
allocation3 = new byte[1000*1024];
allocation4 = new byte[1000*1024];
allocation5 = new byte[1000*1024]; allocation6 = new byte[1000*1024];
}
}
//GC日志
[GC (Allocation Failure) [PSYoungGen: 65253K‐>952K(76288K)] 65253K‐>60960K(251392K), 0.0311467 secs] [Times: user=0.08 sys=0.02, real=0.03 secs]
Heap PSYoungGen total 76288K, used 13878K [0x000000076b400000, 0x0000000774900000, 0x00000007c0000000)
eden space 65536K, 19% used [0x000000076b400000,0x000000076c09fb68,0x000000076f400000)
from space 10752K, 8% used [0x000000076f400000,0x000000076f4ee030,0x000000076fe80000)
to space 10752K, 0% used [0x0000000773e80000,0x0000000773e80000,0x0000000774900000)
ParOldGen total 175104K, used 60008K [0x00000006c1c00000, 0x00000006cc700000, 0x000000076b400000) object space 175104K, 34% used [0x00000006c1c00000,0x00000006c569a010,0x00000006cc700000)
Metaspace used 3343K, capacity 4496K, committed 4864K, reserved 1056768K 25 class space used 361K, capacity 388K, committed 512K, reserved 1048576K
大对象直接进入老年代
何为大对象?
就是需要大量并且连续内存存储的就叫大对象,JVM 参数
-XX:PretenureSizeThreshold 通过这个参数可以设置如果对象的大小满足这个参数设定的值,则是大对象直接进入老年代。
为什么要这样做?
个人理解,是为了让出足够的空间给新生的小对象使用,如果大对象不直接进入老年代,而通过判断GC的次数是否满足,大对象又不是朝生夕死的对象,这样会触发动态年龄判断机制,这样会频繁的触发Full GC
长期存活的对象将进入老年代
长期存活的对象指的是经历过多次Minor GC 并且能存活,每次Minor GC GC 次数都会+1 ,这个次数可以JVM参数:-XX:MaxTenuringThreshold 来设定 (默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同)这个GC 次数存在对象头中
动态年龄判断机制
当Survivor区域存活的一批对象超过Survivor区50%(-XX:TargetSurvivorRatio可以指定) 那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,
例如:
Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会 把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年 龄判断机制一般是在minor gc之后触发的。
老年代空间担保机制
空间担保机制图:
minor gc 之前 首先会判断 老年代的剩余空间是否大于年轻代现有的全部对象 如果大于则会进行 minor gc 否则 判断是否配置-XX:-HandlePromotionFailure 参数,如果没有配置
-XX:-HandlePromotionFailure则直接执行full gc,如果配置了则判断 老年代剩余空间是否大于历次minor gc 进入老年代对象的平均大小 大于则执行 minor gc 否则执行 full gc, 执行完 full gc后也会执行minor gc,
对象内存回收
对象内存回收,即对象没有用处回收对应的内存,回收内存存在两种算法 引用计数法、可达性分析算法
引用计数法:
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0 的对象就是不可能再被使用的。 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决 对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,如下面代码所示:除了对象objA 和 objB 相互引用着对 方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算 法无法通知 GC 回收器回收他们。
可达性分析算法
将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的 对象都是垃圾对象
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
常见引用类型
java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用
强引用:普通的变量引用 1 public static User user = new User();
**软引用:**将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放 新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
public static SoftReference<User> user = new SoftReference<User>(new User());
软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从 缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
弱引用:
将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用
public static WeakReference<User> user = new WeakReference<User>(new User());
虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用
finalize()方法最终判定对象是否存活
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一 个对象死亡,至少要经历再次标记过程。 标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
- 第一次标记并进行一次筛选。 筛选的条件是此对象是否有必要执行finalize()方法。 当对象没有覆盖finalize方法,对象将直接被回收。
- 第二次标记 如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救 自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。 示例代码:
public class OOMTest {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
int i = 0;
int j = 0;
while (true) {
list.add(new User(i++, UUID.randomUUID().toString()));
new User(j‐‐, UUID.randomUUID().toString());
}
} }
如何判断一个类是无用的类
方法区主要回收的是无用的类,
那么如何判断一个类是无用的类的呢?
类需要同时满足下面3个条件才能算是 “无用的类” :
1.该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
2.加载该类的 ClassLoader 已经被回收。
3.该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。