一、创建对象过程
1、类加载检测
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new指令对应语言层面讲是,new关键字、对象克隆、对象序列化等
2、分配内存
确保类加载完成之后,接下来jvm就需要为新生对象分配内存。对象所需内存的大小其实在类加载完成之后就可以确定了,然后就是从堆空间中划分出一块确定大小的内存给该对象使用
对象内存分配涉及到两个问题:如何划分内存和如何控制并发分配内存
划分内存的方法:
- 指针碰撞(Bump the Pointer)(默认用指针碰撞):在堆中分配内存中,有一个指针作为对象已分配和为分配内存分界点,当对象分配进来,指针往未分配内存区域挪动该对象相同大小的距离,这个过程叫指针碰撞
- 空闲列表:(Free List):在堆中分配对象中,如果堆内存的对象已分配区和未分配区特别凌乱,不是那么规整,这时候只能在jvm内部维护一个列表去记录哪些是内存区域是可用的,当有对象分配进来就更新这个列表
解决并发问题的方法:
- CAS(Compare And Swap):jvm内部采用CAS自旋来保证对象一定会被分配到内存
- 本地线程分配缓冲(Thread Local Allocation Buffer, TLAB):每个线程在堆中预先分配一小块内存。通过-XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM默认开启-XX:+UseTLAB),-XX:TLABSize指定TLAB大小
3、初始化
内存分配完成之后,虚拟机需要将分配到内存空间的对象成员变量都初始化为零值(不包括对象头),如果是TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值
4、设置对象头
初始化零值后,jvm要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找类的元数据信息、对象的哈希码、对象的gc分带年龄等信息。这些信息放在对象的对象头Object Header之中
在HotSpot虚拟机中,对象在内存中存储的布局可分为三个区域:对象头(Header)、示例数据(Instance Data)和对齐填充
- 对象头:比如hash码,对象所属分带年龄、对象锁、锁状态标志、偏向锁(线程)ID、偏向时间,数组长度(数组对象才有)等
- 实例数据:存放类的属性数据信息,包括父类的属性信息
- 对其填充:由于Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头已被精心设计成正好是8的整数倍,因此,如果是对象实例数据没有对齐的话,就需要通过对齐填充来补全
找了一张对象头的图片,
5、执行<init>方法
执行<init>方法,即对象按照程序的意愿进行初始化,就是为属性赋程序员指定值,然后执行构造方法。
6、对象的指针压缩
引入查看对象大小的包:
<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.9</version>
</dependency>
/*** 查看对象大小*/
public class ObjectSizeTest {public static void main(String[] args) {// object对象16B// 对象头// mark word 8B// Klass Pointer 4B// 不是数组 数组长度没有// 实例数据 没有成员变量// 对其填充 要求整个对象大小是8的整数倍 补上4BClassLayout layout = ClassLayout.parseInstance(new Object());System.out.println(layout.toPrintable());System.out.println();// object对象16B// 对象头// mark word 8B// Klass Pointer 4B// 数组长度占4B// 实例数据 没有成员变量// 对其填充 要求整个对象大小是8的整数倍 不需要填充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());}// -XX:+UseCompressedOops 默认开启的压缩所有指针// -XX:+UseCompressedClassPointers 默认开启的压缩对象头里的类型指针Klass Pointer// Oops : Ordinary Object Pointerspublic static class A {// 对象头// mark word 8B// Klass Pointer 4B 如果关闭压缩-XX:-UseCompressedClassPointers或-XX:-UseCompressedOops,则占用8Bint id; // 4BString name; // 对象在堆中的指针4B 如果关闭指针压缩-XX:-UseCompressedOops,则占用8Bbyte b; // 1B 这个有个内部的填充3BObject o; // 对象在堆中的指针4B 如果关闭指针压缩-XX:-UseCompressedOops,则占用8B// 需要对齐填充4B// A对象大小为32B}
}
打印结果:
java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)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 VALUE0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)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 totalcom.gaorufeng.jvm.ObjectSizeTest$A object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)8 4 (object header) 61 cc 00 f8 (01100001 11001100 00000000 11111000) (-134165407)12 4 int A.id 016 1 byte A.b 017 3 (alignment/padding gap) 20 4 java.lang.String A.name null24 4 java.lang.Object A.o null28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total
什么是java对象的指针压缩?
- jdk1.6 update14开始,在64bit操作系统中,jvm支持指针压缩
- jvm配置参数:UseCompressedOops,compressed--压缩、oop(ordinary object pointer)--对象指针
- 启动指针压缩:-XX:+UseCompressedOops(默认开启),禁止指针压缩:-XX:UseCompressedOops
为什么要进行指针压缩?
节省内存空间,减少总线寻址性能上的消耗,堆内存小于4G时,不需要启用指针压缩;堆内存小于4G时,不需要启用指针压缩
二、对象的内存分配
1、栈上分配
栈上分配是为了减少gc的次数,提高jvm性能,每次栈帧入栈时创建的对象,出栈就可以释放内存,对象自然就被清理了
需要开启两个参数来确定对象是否能在栈上分配
- 对象逃逸分析:当一个对象在栈帧方法中,分析出没有被外部对象引用到时,确定有可能在栈上分配,有引用到那就不能在栈上分配,逃逸分析参数(-XX:+DoEscapeAnalysis),jdk7之后默认开启,关闭逃逸分析参数(-XX:-DoEscapeAnalysis)
举个例子:
public User test1() {User user = new User();return user;}public void test2() {User user = new User();}
test1()方法的对象的作用范围是不确定的,test2()方法可以确定user对象作用范围只在当前栈帧方法,不会逃逸出当前方法,可以在栈上进行分配
- 标量替换:栈帧内存区域的可用内存不一定能够存放一整个对象且随机分布,开启标量替换参数(-XX:+EliminateAllocations),jdk7之后默认开启
- 标量与聚合量:标量即不可被进一步分解的量,而Java的基本数据类型就是标量(如int、long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量,而在Java中对象就是可以被进一步分解的聚合量
栈上分配依赖于逃逸分析和标量替换
栈上分配示例:
/*** 把堆空间设小一点 如果发生gc那就是分配在堆中,如果没有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 allocation() {User user = new User();}public static void main(String[] args) {long start = System.currentTimeMillis();for (int i = 0; i < 100000000; i++) {allocation();}long end = System.currentTimeMillis();System.out.println(end - start);}
}
2、堆中Eden区分配
大多数情况下,对象在新生代中Eden区分配。当Eden区没有足够内存空间,虚拟机发起一次minor gc
- minor gc/young gc:新生代垃圾对象的占用内存回收
- major gc/full gc:回收整个堆和方法区的垃圾对象占用的内存,比minor gc慢10倍以上
新生代和老年代比例1:2,eden区和s0、s1区比例8:1:1
大部分对象都分配eden区,eden区放满了之后触发minor gc,存活的对象放到survivor区中空的区域,下次触发minor gc,继续eden区和非空的survivor区的非垃圾对象往空的survivor区里面放
jvm默认有个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy
示例:
/*** eden区大概默认65M* 添加运行JVM参数: -XX:+PrintGCDetails*/
public class GCTest {public static void main(String[] args) {byte[] allocation1, allocation2/*, allocation3, allocation4, allocation5, allocation6*/;allocation1 = new byte[62000*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];*/}
}
运行结果:
HeapPSYoungGen total 75776K, used 65024K [0x000000076bc00000, 0x0000000771080000, 0x00000007c0000000)eden space 65024K, 100% used [0x000000076bc00000,0x000000076fb80000,0x000000076fb80000)from space 10752K, 0% used [0x0000000770600000,0x0000000770600000,0x0000000771080000)to space 10752K, 0% used [0x000000076fb80000,0x000000076fb80000,0x0000000770600000)ParOldGen total 173568K, used 0K [0x00000006c3400000, 0x00000006cdd80000, 0x000000076bc00000)object space 173568K, 0% used [0x00000006c3400000,0x00000006c3400000,0x00000006cdd80000)Metaspace used 2644K, capacity 4486K, committed 4864K, reserved 1056768Kclass space used 280K, capacity 386K, committed 512K, reserved 1048576K
可以看到eden区被全部占用,继续分配触发minor gc,由于被引用是非垃圾对象,survivor区放不下,放到老年代
/*** eden区大概默认65M* 添加运行JVM参数: -XX:+PrintGCDetails*/
public class GCTest {public static void main(String[] args) {byte[] allocation1, allocation2/*, allocation3, allocation4, allocation5, allocation6*/;allocation1 = new byte[62000*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 (Allocation Failure) [PSYoungGen: 64601K->632K(75776K)] 64601K->62640K(249344K), 0.0270385 secs] [Times: user=0.09 sys=0.02, real=0.03 secs]
HeapPSYoungGen total 75776K, used 9282K [0x000000076bc00000, 0x0000000775000000, 0x00000007c0000000)eden space 65024K, 13% used [0x000000076bc00000,0x000000076c472a78,0x000000076fb80000)from space 10752K, 5% used [0x000000076fb80000,0x000000076fc1e030,0x0000000770600000)to space 10752K, 0% used [0x0000000774580000,0x0000000774580000,0x0000000775000000)ParOldGen total 173568K, used 62008K [0x00000006c3400000, 0x00000006cdd80000, 0x000000076bc00000)object space 173568K, 35% used [0x00000006c3400000,0x00000006c708e010,0x00000006cdd80000)Metaspace used 2644K, capacity 4486K, committed 4864K, reserved 1056768Kclass space used 280K, capacity 386K, committed 512K, reserved 1048576K
后面有对象来会继续分配在eden区
/*** eden区大概默认65M* 添加运行JVM参数: -XX:+PrintGCDetails*/
public class GCTest {public static void main(String[] args) {byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6;allocation1 = new byte[62000*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 (Allocation Failure) [PSYoungGen: 64601K->696K(75776K)] 64601K->62704K(249344K), 0.0411987 secs] [Times: user=0.06 sys=0.03, real=0.04 secs]
HeapPSYoungGen total 75776K, used 13621K [0x000000076bc00000, 0x0000000775000000, 0x00000007c0000000)eden space 65024K, 19% used [0x000000076bc00000,0x000000076c89f3d8,0x000000076fb80000)from space 10752K, 6% used [0x000000076fb80000,0x000000076fc2e030,0x0000000770600000)to space 10752K, 0% used [0x0000000774580000,0x0000000774580000,0x0000000775000000)ParOldGen total 173568K, used 62008K [0x00000006c3400000, 0x00000006cdd80000, 0x000000076bc00000)object space 173568K, 35% used [0x00000006c3400000,0x00000006c708e010,0x00000006cdd80000)Metaspace used 2644K, capacity 4486K, committed 4864K, reserved 1056768Kclass space used 280K, capacity 386K, committed 512K, reserved 1048576K
3、大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如数组、字符串)。jvm参数-XX:PretenureSizeThreshold可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在Serial和ParNew两个收集器下有效
比如设置jvm参数:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC,再执行下上面的第一个程序就会发现大对象直接入老年代
/*** eden区大概默认65M* 添加运行JVM参数: -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000000 -XX:+UseSerialGC*/
public class GCTest {public static void main(String[] args) {// 6M的对象 设置1M就是大对象-XX:PretenureSizeThreshold=1000000byte[] allocation1 = allocation1 = new byte[6000*1024];}
}
打印结果(eden区默认会占用一定空间):
Heapdef new generation total 78016K, used 4162K [0x00000006c3400000, 0x00000006c88a0000, 0x0000000717800000)eden space 69376K, 6% used [0x00000006c3400000, 0x00000006c3810bd0, 0x00000006c77c0000)from space 8640K, 0% used [0x00000006c77c0000, 0x00000006c77c0000, 0x00000006c8030000)to space 8640K, 0% used [0x00000006c8030000, 0x00000006c8030000, 0x00000006c88a0000)tenured generation total 173440K, used 6000K [0x0000000717800000, 0x0000000722160000, 0x00000007c0000000)the space 173440K, 3% used [0x0000000717800000, 0x0000000717ddc010, 0x0000000717ddc200, 0x0000000722160000)Metaspace used 2644K, capacity 4486K, committed 4864K, reserved 1056768Kclass space used 280K, capacity 386K, committed 512K, reserved 1048576K
为什么要这样?
年轻代的minor gc是比较频繁的,复制耗费性能
4、长期存活的对象进入老年代
对象的对象头分带年龄占4个字节(1111),换算成十进制就是15,所以分带年龄不可能超过15.如果对象在eden区分配并且已经经历过一次minor gc后仍然能够存活,并且能被survivor区所容纳,该对象将被移动到survivor区,并将对象的年龄设为1。对象在survivor区每经历一次minor gc,年龄就会加1,当年龄增加到一定程度(默认15,CMS收集器默认6,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中,对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置
5、对象动态年龄判断机制
当前对象的survivor区域里(其中一块区域,放对象的那快s区),一批对象的总大小大于这块survivor区域的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,例如survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过survivor区域的50%,此时就会把n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发
6、老年带空间分配担保机制
年轻代每次minor gc之前jvm都会计算下老年代剩余可用空间,如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象),就会看一个-XX:-HandlePromotionFailure(jdk1.8默认就设置了)的参数是否设置了,如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次full gc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生OOM,如果minor gc之后剩余存活的需要挪到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full gc完之后如果还没有空间放minor gc之后存活的对象,也会发生OOM
三、内存回收
1、引用计数法
此算法就是当一个对象被引用一次计数器就会加1,引用为0,则认为是可回收垃圾;唯一的问题就是如果是循环引用,那就永远不可能为0,这样会导致内存泄露
2、可达性分析算法
从gc root(线程栈的本地变量、静态变量、本地方法栈的变量等)开始往下找,找被gc root引用的对象,这些对象都是非垃圾对象
3、常见引用类型
- 强引用:普通变量的引用
public static User user = new User();
- 软引用:一般情况下不会被回收,当发生gc释放不出来空间了就会被回收;比如对象频繁的创建但又不是特别重要的对象,可用用与大屏展示,可有可无的对象
public static SoftReference<User> user = new SoftReference<User>(new User());
- 弱引用:弱引用跟没引用一样,会直接被回收,很少用
public static WeakReference user = new WeakReference(new User());
- 虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用
4、finalize()方法自救
每个对象都会执行一次finalize()方法,在回收之前都会进行标记
- 第一次标记:查看是否有重写finalize()方法,如果没有就直接被回收掉了
- 第二次标记:执行finalize()方法,这个时候如果为了不让gc回收,可以让对象与gc root关联(这样每次创建这个对象都会被自救,内存泄漏,迟早会出现OOM)
5、方法区判断无用的类
- 该类在堆中实例对象已被回收
- 加载该类的ClassLoader已被回收
- 该类的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
6、内存泄漏(会出现OOM)
程序内部采用了静态map缓存数据,由于这个对象是静态的,所以一直在old区,随着数据不断增加,map占用内存更大,会触发full gc,但是清理之后map还是被引用,清理不掉,频繁full耗费cpu和内存(可以采用Ehcache对象淘汰结构、LRU)
7、内存溢出(会出现OOM)
新建的对象过大,导致内存被占满了