JVM对象创建、内存分配以及回收机制深度刨析
1、对象的创建(new)
由上图我们可以看出,对象的创建分为以下几个步骤:
1、类加载检查:当虚拟机遇到一条new命令时,首先会检查这个符号是否能引用到具体的堆中的对象,或者说这个符号所代表的对象是否有被加载、初始化过,如果没有,则执行类的加载过程,new,对象克隆、对象序列化。
2、分配内存:在类加载完成之后,一个类的大小就已经被确定出来了,这时候就需要从堆里面划分一块内存给对象,划分内存有以下两个方法。
2.1、指针碰撞:在内存的已分配和未分配空间的中间有一个指针,指针的一边是都已经存放完整,另一边全是空闲内存,当我们需要给一个对象分配堆内存时,仅仅是将指针向后挪一个对象大小的空间而已,详情如下图所示:
2.2、空闲列表:这种的堆内存空闲空间是不规整的,所有的空闲空间会有一个空闲列表所记录,那一块有空闲,空闲多大,当需要为一个对象分配堆内存时,会根据空闲列表找到一块合适的区域,为新对象分配内存区域,并更新空闲列表,详情如下图所示:
3、初始化:给程序的静态变量赋初始值。
4、设置对象头:java对象由三大部分组成,对象头、实例数据,对齐填充字节,而对象头又分为Mark Word(一个标记)、Klass Pointer(类型指针)、数组长度(只有数组对象才有),图解关系如下图:
4.1、对象头:下图是一副对象头的详细数据,这里我们着重说一下无锁态的对象头。
4.1.1、Mark Word:在32位操作系统下占32bit,在64位操作系统占64bit,这里面存储的就是下图中的对象运行中的hashcode值,分代年龄、线程ID、偏向锁标志位等等。
4.1.2、Klass Pointer:我们在运行程序的时候,栈中存放的变量指针会指向堆,而堆中的这个对象就会有我们对象的这三大组成部分,它的这个Klass Pointer(类型指针)将会指向我们元空间里面存放的我们的代码二进制文件的类元信息,详情请看下图
//代码示例
Object1 object1 = new Object1();
Class<? extends Object1> object1Class = object1.getClass();
4.1.3、数组长度:只有当对象是数组时,才会有这个标志位。
4.2、实例数据:我们类中存放的一些静态变量所占的空间。
4.3、对齐填充字节:我们的操作系统目前应用最多的应该就是32bit和64bit了吧,32bit是4字节,64bit是8字节,所以,当一个东西是4字节或者8字节的整数倍的时候,计算机的寻址效率是最高的,所以在每一个对象在生成自己的所有信息之后,如果不是4或者8的倍数的时候,我们就会产生一个对齐填充字节,将不足的字节部分补上。(我这里说的4或者8要根据计算机的操作位数来看)
5、执行init:设置完对象头之后,JVM底层会调用init方法,这个方法有两大作用,细节如下:
5.1、赋值:根据程序中写的初始值给对象的属性赋上真正的初始值。
5.2、执行构造方法:执行对象的构造方法,生成对象的实例。
2、对象的内存分配
对象在内存分配的时候,首先会做一个操作,叫做对象逃逸分析(前提是对象逃逸分析参数开启,JDK7之后默认开启逃逸分析),如果对象没有逃逸,将会进行一系列的判断,是否分配在栈内存,如果逃逸或者逃逸分析参数位开启,对象将直接分配到堆内存。
对象逃逸分析:可根据下面的代码分析
public Person function1() {
Person person = new person();
person.setId(1);
person.setName("XiaoLeLe");
return person;
}
public void function2() {
Person person = new person();
person.setId(2);
person.setName("XiaoLeLe");
}
根据上面这段代码,在function1中有一个Person的实例化对象,它被返回出了方法外,所以说明方法外还有变量引用着它,所以这个对象就逃逸出了方法,而在function2中实例化的Person的实例只在方法内部有效,方法结束后,person将销毁掉,所以,对于这种未逃逸出方法的对象,Java在为其分配内存的时候,会有一系列的判断,(开启逃逸分析参数-XX:-DoEscapeAnalysis,JDK7之后默认开启)如下图所示:
TLAB和CAS在本文辅助知识里面有,在这里我们解释两个名词:
1、标量替换:在对象逃逸分析之后,发现我们这个对象可以分配在栈帧上面,但是有一个问题,栈帧所剩余的空间都是碎片化的,总共加起来可以放的下这个对象,但是现在没有一块连续的空间将对象存在里面,这个时候就可以使用标量替换的方法将对象拆分成一个个成员变量碎片化的存在这个栈帧中,当然它肯定会有一个标志位标明这是哪个对象的成员变量。也有一个参数开启标量替换的参数(-XX:+EliminateAllocations,JDK7之后默认开启)。
2、标量与聚合量:标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。
3、判断进入老年代的方法
3.1、发生gc回收
3.2、大对象直接进入老年代
何为大对象:这里有一个参数可以设置(XX:PretenureSizeThreshold=1000000 (单位是字节)),当大于这个参数所指定的size时,则为大对象。
注:这个参数只能在Serial 和ParNew两个收集器下有效,比如说指定Serial收集器参数(-XX:+UseSerialGC)。
为什么要这样设置:防止一些大的对象明知道是不可能被gc清除掉,且一直在发生minor gc时在年轻代复制来复制去,占用资源,影响程序性能。
3.3、长期存活的对象进入老年代
这就是那个当对象的分代年龄达到15时,进入老年代,这里有一个参数可以设置分代年龄的值(-XX:MaxTenuringThreshold)
详情请查看这篇博客的堆模型详解
为什么要这样设置:当大致能推测到系统中大致85%以上要被销毁的对象分代年龄都不会超过8(这个数字根据实际情况来),那么我就可以设置分代年龄最大值为8,当达到8时就直接进入老年代,以节约年轻代的空间。
3.4、动态年龄判断机制
当发生minor gc时,Eden存活下来的所有对象的总和空间(年龄1+年龄2+…+年龄n)如果大于s0或者s1的50%的空间的时候,此时就会把年龄n(含)以上的对象都挪入老年代。
3.5、老年代空间分配担保机制
在每一次触发minor gc之前,会先判断以下老年代的剩余空间是否>=年轻代里面的所有对象容量总和,如果成立,直接执行minor gc(原因是害怕执行完一次minor gc之后,所有的对象都存活了下来,老年代不够放,又得做full gc,这样就是先执行了minor gc,然后才执行了full gc,记住这个顺序),如果不成立,说明老年代的空间已经不够了,这会儿会先检查有没有配置一个参数叫老年代担保参数(-XX:-HandlePromotionFailure,jdk1.8就已经默认设置了),如果没有配置,则直接执行full gc,再执行minor gc,如果有,则证明已经开启了老年代担保机制,将会去判断老年代的剩余空间是否>=每一次minor gc之后剩余存活对象的平均容量大小,如果成立,则执行minor gc(说明担保机制担保了可以放下),如果不成立,说明有极大可能放不下,就先执行full gc,在执行minor gc。将文字转换为图的方式表达如下:
4、对象的回收机制
4.1、判断是否是垃圾对象的算法
4.1.1、引用计数法
给对象添加一个引用计数器,如果有一个地方引用它,它的值就会加1,当引用失效,值就减1,当值为0的时候就会被gc所回收掉。
缺点:万一两个变量互相引用,那么这两个变量就永远不会被gc掉。
4.1.2、可达性分析算法
将“gc root”对象作为起点,从这些节点向下搜索,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。
常见的“gc root”:线程栈的本地变量、静态变量、本地方法栈的变量等等。
4.2、常见的引用类型
Java的引用类型一般分为4种:强引用、软引用、弱引用、虚引用
强引用:普通的变量引用,不会被gc回收。
软引用:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
软引用举例:比如在浏览一个网页1时,然后在同页面又打开了网页2,这时候点击浏览器左上角的回退箭头会回到上一个网页,而上一个网页的内容就使用的是这种软引用在内存中缓存着,这种引用对程序不会有什么影响,所以,当gc不出空间,就会销毁这些软引用,程序举例如下:
//软引用举例
public static SoftReference<User> user = new SoftReference<User>(new User());
弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用,代码举例如下:
public static WeakReference<User> user = new WeakReference<User>(new User());
虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用。
4.3、finalize()的自救
如果一个对象要被回收,gc会去看这个对象中有没有重写finalize()方法,如果有,则执行一下,如果finalize()执行完之后,这时候有变量引用了这个对象,则这个对象将不会被gc回收,如果还是没有引用,则被回收。
注意:finalize()方法只会执行一次,如果此类(class)的实例化对象再次被回收,则不会执行finalize()方法自救,会被直接回收。
5、辅助知识
5.1、对象分配内存时并发问题的解决
1、CAS(Compare and Swap)
这里简单说一下CAS,后续会有详细的会将博客链接贴上,就是很多线程去争抢,抢到了就去分配,没有抢到的会接着重试争抢。
2、TLAB(Thread Local Allocation Buffer本地线程分配缓存)
每一个线程都会有自己专属的一块堆内存,在为新对象分配内存时只会在自己的专属内存去给对象分配,解决了并发冲突问题,JVM默认开启这种解决方案(XX:+UseTLAB),可以使用这个XX:TLABSize参数指定TLAB大小。
5.2、使用jol‐core查看对象头信息
1、首先导入一个名称为jol-core-0.9.jar的jar包。
2、代码示例
package JVM;
import org.openjdk.jol.info.ClassLayout;
public class ObjectHead {
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 ObjectHead.SimpleObject());
System.out.println(layout2.toPrintable());
}
// -XX:+UseCompressedOops 默认开启的压缩所有指针
// -XX:+UseCompressedClassPointers 默认开启的压缩对象头里的类型指针Klass Pointer
// Oops : Ordinary Object Pointers
public static class SimpleObject {
//8B mark word
//4B Klass Pointer 如果关闭压缩-XX:-UseCompressedClassPointers或-XX:-UseCompressedOops,则占用8B
int id; //4B
String name; //4B 如果关闭压缩-XX:-UseCompressedOops,则占用8B
byte b; //1B
Object o; //4B 如果关闭压缩-XX:-UseCompressedOops,则占用8B
public int help(){
int a = 0;
int b = 1;
return a+b;
}
}
}
3、运行结果
JVM.ObjectHead
java.lang.Object object internals:
//OFFSET 偏移量
//SIZE 大小
//TYPE DESCRIPTION 类型描述
//object header 对象头
OFFSET SIZE TYPE DESCRIPTION VALUE
//对象头的mark word
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
//对象头的mark word
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
//对象头的Klass Pointer,本来是8字节,这里经过了指针压缩
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
//看到下面这一行了吗,由于前面的对象头只占了12字节,我的操作系统是64bit的,必须是8个字节的整数倍,所以对齐填充字节增加了四个字节
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
//对象头的mark word
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
//对象头的mark word
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
//对象头的Klass Pointer,本来是8字节,这里经过了指针压缩
8 4 (object header) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
//数组对象专有的数组长度
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 0 int [I.<elements> N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
JVM.ObjectHead$SimpleObject object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
//对象头的mark word
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
//对象头的mark word
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
//对象头的Klass Pointer,本来是8字节,这里经过了指针压缩
8 4 (object header) 63 cc 00 f8 (01100011 11001100 00000000 11111000) (-134165405)
//int类型,占4个字节
12 4 int SimpleObject.id 0
//byte类型,占1个字节
16 1 byte SimpleObject.b 0
//byte类型的专用对齐,对齐填充了3个字节
17 3 (alignment/padding gap)
//String类型,占四个字节,本来占8个,经过了指针压缩
20 4 java.lang.String SimpleObject.name null
//Object类型,占四个字节,本来占8个,经过了指针压缩
24 4 java.lang.Object SimpleObject.o null
//对齐填充字节,填充了4个字节,为了凑成8的整数倍
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total
Process finished with exit code 0
5.3、为什么要有指针压缩
指针压缩参数:
启用指针压缩:XX:+UseCompressedOops(jdk1.6之后默认开启),禁止指针压缩:XX:UseCompressedOops
答:在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,这样,将对象从堆中(电脑内存中)和主存(CPU)之间移动的指针也就相对较大,会占用较大宽带,同时GC也会承受较大压力,所以很影响性能。
采取措施:在jvm中,32位地址最大支持4G内存(2的32次方),我们现在所使用的是64bit的计算机,如果一个电脑的物理内存为16G,那个他需要(2的34次方),也就是34bit的机器就可以,在这里,JVM底层通过一定的算法,将他们压缩成32bit存放在堆中,要拿到CPU寄存器运算时,再解压回原来的34bit,这样既节省了堆的空间,减少了gc,也在传输的过程中更加高效。
建议:
堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间,堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,所以堆内存不要大于32G为好。
5.4、如何判断一个类是无用的类
这里面主要回收的区域是元空间的类元信息。
1、该类所有的对象实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
2、加载该类的 ClassLoader 已经被回收(对于一般的类加载器加载的方法,几乎不可能回收类加载器,就比如说ExtClassLoader和AppClassLoader都是JVM所创建,基本不可能被回收,但是对于自定义类加载器加载的类,比如说jsp文件,就可以被回收,这也就是热加载可以实现的原因,jsp热加载可以看这篇文章的为什么改变Jsp文件不需要重启系统)。
3、该类对应的 java.lang.Class 对象没有在任何地方被引用。