JVM对象创建
创建流程:
1. 类加载
JVM遇到new指令,先检查这个指令参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始换,如果没有,就先执行相应的类加载过程。
new指令对应到java语言层面,对应new关键字、对象克隆、对象序列化等。
2. 分配内存
完成类加载后,JVM为新生对象分配内存。 类加载的时候内存大小就已确定。
内存划分方式:
- "指针碰撞":是默认方式,JVM堆中的内存是规整的,使用和未使用的中间有个分界点指示器,分配就是将指示器往未分配区域挪一部分。
- "空闲列表":JVM堆中的内存不规整,JVM维护了一个列表记录哪些内存可用,从列表中找出一块足够大的空间划分给对象实例。
内存分配的并发问题:
- CAS:CAS+失败重试
- 本地线程分配缓冲(Thread Local Allocation Buffer, TLAB):每个线程在Java堆中预先分配一小块内存。通过XX:+/-UseTLAB 参数来设定虚拟机是否使用TLAB(JVM会默认开启XX:+ UseTLAB),-XX:TLABSize指定TLAB大小。
3.初始化
分配的内存空间设置为初始的零值(不包含对象头)
4. 设置对象头
初始化后,JVM需要对 对象做必要的设置。对象在内存中的布局分为3部分:对象头、实例数据、对齐填充。

对象头包括两部分信息:
- 对象的哈希码、对象的GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
- 类型指针,指向它的类元数据的指针

底层的C源码定义的对象头:

5. 执行<init>方法
程序员的赋值,即java语言层面为属性赋值、执行构造方法。这和初始化步骤中的设置初始的零值不一样。
JVM内存分配

对象在栈上分配
JVM通过逃逸分析确定该对象不会被外部访问,如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。
java
// test1()中的user可能会被外部引用,对应的作用域不确定
public User test1() {
User user = new User();
user.setId(1);
user.setName("zhuge");
//TODO 保存到数据库
return user;
}
// test2()中的user只在本方法内部使用,这样的对象可以分配在栈内
public void test2() {
User user = new User();
user.setId(1);
user.setName("zhuge");
//TODO 保存到数据库
}
// 这两个开关需要同时打开,才有可能减少gc
-XX:+DoEscapeAnalysis : 开启逃逸分析参数,JDK7之后默认开启
-XX:+EliminateAllocations : 开启标量替换参数,JDK7之后默认开启
标量替换:通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量 分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。
标量 与聚合量:标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。
对象在Eden分配
- Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
- Major GC/Full GC:一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上。

Eden与Survivor区默认8:1:1, JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化。
对象进入老年代
1. Minor GC 大对象进入老年代
为新的对象在eden分配空间时,如果空间不够,触发Minor GC, Minor GC后,Surviovor没有足够空间存储存活对象, 会将存活对象移动到老年代。
2. 大对象直接进入老年代
-XX:PretenureSizeThreshold 设置大对象的大小。
如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效。可以避免为大对象分配内存时的复制操作而降低效率。
2. 长期存活对象进入老年代
对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。默认年龄=15会进入到老年代。
3. 对象动态年龄判断
当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了。
对象动态年龄判断机制一般是在minor gc之后触发的。
4. 老年代空间分配担保机制

- 判断:老年代剩余可用空间 < 年轻代里现有的所有对象(包括垃圾对象)
- -XX:-HandlePromotionFailure"(jdk1.8默认就设置了)
- Full GC后还没有足够空间访新对象就会发生OOM
- Minor GC后需要移动到老年代的对象大小 > 老年代可用空间,触发Full GC.
对象内存回收
1、对象存活判断
1. 引用计数器法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
java
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}

当objA合objB 都是null时, 但是这两个对象都还分别有个引用,是相互引用。
2. 可达性分析法
将"GC Roots" 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。
GC Roots根节点:
- 线程栈的本地变量表引用的对象
- 方法区的静态变量(静态属性引用的对象)
- 方法区的常量引用对象
- 本地方法栈中JNI(Java Native Interface)引用的对象
- 被同步锁(synchronized)持有的对象
- JVM内部的引用,比如基本数据类型的Class对象、系统类加载器、常驻的异常对象(OutOfMemoryError)

3. finalize()方法
finalize()方法最终判定对象是否存活。
可达性分析方法标记出一个对象不可达,到宣告这个对象正式死亡,需要经过两次标记阶段:
1. 第一次标记 并进行一次筛选
筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法,对象将直接被回收。
2. 第二次标记
如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出"即将回收"的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。等下次gc时就不会执行finalize()方法,直接被回收。
2、类的回收
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
类需要同时满足下面3个条件才能算是 "无用的类" :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。(系统类加载器很难被回收,主要是自定义类加载器)
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
3、常见引用类型
java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用
- 强引用:正常定义的对象。
- 软引用:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收。但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
java
public static SoftReference<User> user = new SoftReference<User>(new User());
- 弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用。 ThreadLocal就是使用了弱引用。
java
public static WeakReference<User> user = new WeakReference<User>(new User());
- 虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用
JVM参数
// compressed压缩、oop(ordinary object pointer)对象指针
‐XX:+UseCompressedOops : 默认开启的压缩所有指针
‐XX:+UseCompressedClassPointers : 默认开启的压缩对象头里的类型指针Klass Pointer
‐XX:+PrintGC :
‐XX:+PrintGCDetails :
JVM中的概念
压缩指针
jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩 。 注意,压缩的是指针占用的内存大小。
- 启用指针压缩: -XX:+UseCompressedOops(默认开启),
- 禁止指针压缩: -XX:-UseCompressedOops
- ‐XX:+UseCompressedClassPointers : 默认开启的压缩对象头里的类型指针Klass Pointer
为什么要进行指针压缩?
-
在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力
-
为了减少64位平台下内存的消耗,启用指针压缩功能
-
在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对 对象指针的压缩编码、解码方式进行优化,使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G)
-
堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
-
堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内存不要大于32G为好