对象的创建过程有如下步骤:
1.类加载检查:
虚拟机遇到一个new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有,那么必须先执行相应的类加载过程
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。
2.分配内存
对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。这个步骤有两个问题:
- 如何划分内存
- 在并发情况下,可能出现在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
划分内存的方法:
“指针碰撞”(默认)(Bump the Pointer):
如果Java堆中的内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是吧那个指针向空闲空间那边挪动一段与对象大小相等的距离。
“空闲类表”(Free List)
如果Java堆中的内存并不是规整的,已使用的内存和空闲内存相互交错,那就没有办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存快是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
解决并发问题的方法:
1.CAS(Compare and swap)
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来分配内存空间的动作进行同步处理。
2.本地线程分配缓冲(Thread Local Allocation Bufffer, TLAB)
把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存。通过-XX:+/-UseTLAB参数来设定迅即是否使用TLAB(JVM会默认开启-XX:+UseTLAB),- XX:TLABSize指定TLAB大小。
3.初始化
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4.设置对象头
对象除了成员变量之外,还有对象头,对象填充,对象在内存中存储的布局可以分为3块区域:对象头、实例数据和对齐填充
1.Mark Word标记字段(32位占4字节,64位占8字节)自身运行时数据哈希表,GC分代年龄,锁状态标志,线程执有锁,偏向线程ID,偏向时间戳。
2.Klass Pointer类型指针(开启压缩4字节,关闭压缩8字节)。剋的元数据指针(D。代码放在方法区,JVM使用Java头的类型指针拿到方法区的头。
3.数组长度(4字节,只有数组对象才有)
5.执行(init)方法
给对象真正的赋值以及执行构造方法。
对象大小与指针压缩
启用指针压缩-XX:+UseCompressedOops(默认开启),禁止-XX:-UseCompressedOops
压缩对象地址到32位
为什么要进行指针压缩?
- 在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大带宽,同时GC也会承受较大压力。
- 为了减少64位平台下内存的消耗,启用指针压缩功能。
- 在JVM中,32位地址最大支持4G内存,可以通过对象指针的压缩编码、解码方式进行优化,使得JVM只用32位地址就可以支持更大的内存配置(小于等于32G)
- 堆内存小于4G时,不需要启用指针压缩,JVM会直接去除高32位地址,即使用低虚拟地址空间
- 堆内存大于32G时,压缩指针会失效,会强制使用64位堆Java对象寻址,这样会现1的问题,所以堆内存不要大于32G为好。
-XX:+UseCompressedClassPointers默认开启的,只压缩对象头里的类型指针Klass Pointer对象内存分配。
对象逃逸分析:比如User对象在方法执行后返回,则这个对象逃逸了,减少GC压力。
-XX:DoEscopeAnalysis开启逃逸分析参数,JDK7后默认开启
标量替换:一个对象如果经过逃逸分析能够确定这个对象在栈上分配,这里会做一个优化,即便栈帧里没有一整块空间放对象,也能够通过其他方法存在栈帧上。
如果对象不逃逸,栈帧只放对象成员变量,把完整的对象打散放到栈帧上去,这几个变量都标识属于某一个对象。
标量与聚合量:标量即不可以被进一步分解的量,而Java的基本数据类型就是标量,标量的对立就是可以被进一步分解的量,而这种量称为聚合量。而Java对象就是可以被进一步分解的聚合量。
开启标量替换-XX:+ECiminateAllocations,JDK7后默认开启
-Xmx 15m -Xms 15m -XX:+DoEscapeAnslysis -XX:+printGC -XX:+ElininoteAllocations.大对象直接进入老年代
-XX:PretenurefSizeThreshold=1000000 -XX:+UseSerialGC(支持大对象ParNew)。定义大对象配置,这个参数只在Serial和ParNew两个收集器下有效
长期存活的对象将进入老年代。当它的年龄增加到一定程度,默认为15岁,CMS收集器默认为6岁,通过-XX:maxTennringThreshold设置对象动态年龄判断。
对象动态年龄判断:
如果S区超过50%,进入老年代,例如Survivor区域里现在有一批对象年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时会把年龄n(含)以上的对象都放入老年代,这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代,对象动态年龄判断机制一般是在minor gc之后触发。把年轻代设大点(-Xmn)
老年代空间分配担保机制
软引用、弱引用和虚引用在GC时没空间时会被回收。
finalize()方法最终判定对象是否存活。
如果你的对象实现了finalize,这个对象会做标记,不会马上回收,第二次标记,清理掉前执行该方法,finalize只会自救一次。
如何判断一个类是无用的类,方法区主要回收的是无用的类,那么如何判断一个类时无用类呢?
类在方法区,该区要被回收满足该类的实例都被回收。
加载类的Class Loader被回收了,自定义的类加载器是有可能需要回收,比如JSP类加载器。
该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。