对象创建与内存分配机制
一、对象创建
对象创建流程:
- 类加载检查(检查类是否加载)
- 分配内存
- 初始化
- 设置对象头
- 执行 <init> 函数
1. 类加载检查
在new对象的时候,首先会去检查该类是否已经加载过,如果没有加载过,则进行类加载。
2. 分配内存
如果类已经加载过,则从堆内存中划分对象给线程用来创建对象。
划分内存方法:
- 指针碰撞(默认使用):

堆内存默认使用紧凑的方式分配内存,默认从前往后分配内存,在内存分配的最后有一个临界指针。
-
空闲列表:
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟
机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,
并更新列表上的记录
JVM如何保证线程内存分配安全:
- CAS:
采用CAS使得线程争抢内存的分配 - 本地线程分配缓冲(TLAB):
JVM会预先分配给线程一块小空间内存,线程在分配内存的时候可以使用这块空间。
-XX: +/- UseTLAB: 是否启用(+/-)本地线程分配缓冲
-XX:+TLABSize: 如果启用本地线程分配缓冲,则设置本地线程分配缓冲的大小
3. 初始化
将对象的属性初始化为默认值(和类加载中的初步初始化一致)
4. 设置对象头
对象头中包含mark word、Klass point、数组长度(如果是数组对象)
mark word:
| 锁状态 | 25bit (23bit + 2bit) | 4bit | 1bit (是否偏向锁) | 2bit (锁标志位) |
|---|---|---|---|---|
| 无锁态 | 对象的hashCode | 分代年龄 | 0 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | --- | --- | 00 |
| 重量级锁 | 指向互斥量(重量级锁)的指针 | --- | --- | 10 |
| GC标记 | 空 | --- | --- | 11 |
| 偏向锁 | 线程ID (23bit) | Epoch (2bit) | 分代年龄 | 1 | 01 |
- MarkWord标记字段(32位 占4字节,64位占8字节)自身运行时数据:哈希值,GC分代年龄,锁状态标志, 线程持有锁,偏向线程ID, 偏向时间戳
- KlassPointer类型指针(开启压缩占4字节,关闭压缩占8字节):指向类的元数据的指针
- 数组长度(4字节,只有数组对象才有)
5. 执行 <init> 函数
显示赋值、普通代码块的指向、构造方法的执行等
二、对象大小与指针压缩
对象结构:
对象头(mark word(一般8B)、klass pointer(4B)+ 对象体 + 填充字段(保证最后对象占用大小为8的倍数B)
对象大小计算:
java
public static class A {
//8B mark word
//4B Klass Pointer 如果关闭压缩‐XX:‐UseCompressedClassPointers或‐XX:‐UseCompressedOops,则占用8B
int id; //4B
String name; //4B 如果关闭压缩‐XX:‐UseCompressedOops,则占用8B
byte b; //1B + 3B 填充
Object o; //4B 如果关闭压缩‐XX:‐UseCompressedOops,则占用8B
}
最后占用大小是28B+4B对齐 (32B)
1. 什么是压缩指针
- 压缩指针就是使用特定的压缩算法,使得64位指针压缩为32位指针,如何CPU在运行时将32位指针转换成64位指针
- JVM配置参数:-XX: +/- UseCompressedOops: 是否启用(+/-)压缩指针(默认开启)
如果不使用压缩指针,则指针占用8B,会占用更多的内存空间,同时GC压力也会增大
注意:
- 堆内存小于4G(32位地址)时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
- 堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,所以堆内存不是越大越好
三、JVM对象内存分配
分配流程图:

1. 栈上分配 & 对象逃逸 & 标量替换
栈上分配 :
线程在创建线程的时候,JVM优先将对象分配在栈上,这样就可以使得栈退出的时候直接销毁栈,减少GC压力
对象逃逸:
java
// 最后返回对象,对象逃逸,此时,user对象只能分配到堆内存,不能分配到栈上
public User test1() {
User user = new User();
user.setId(1);
user.setName("zhuge");
//TODO 保存到数据库
return user;
}
// 不返回对象,对象不逃逸,对象可以分配在栈上,随栈的退出而销毁,减少GC压力
public void test2() {
User user = new User();
user.setId(1);
user.setName("zhuge");
//TODO 保存到数据库
}
逃逸分析 :
-XX:+DoEscapeAnalysis(默认开启),开启之后,在编译时会进行对象逃逸分析,如果对象没有逃逸,则对象分配在栈上,否则分配在堆上。
标量替换 :
在开启逃逸分析之后,如果对象没有逃逸,则对象可以分配在栈上,但是栈上可能没有连续的空间分配对象,
此时使用标量替换,将对象属性分配在栈上(简单理解为将对象拆开,将属性存在不连续的空间中)。
2. 对象在Eden区分配
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC(Young GC)。
- Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
- Full GC:一般会回收老年代 ,年轻代,方法区的垃圾,速度慢。
Eden与Survivor区默认8:1:1
3. 大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置大
对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下
有效。(为了避免对象频繁在新生代挪动,导致GC压力增大,会直接进入老年代)
4. 长期存活的对象进入老年代
-XX:MaxTenuringThreshold 可以控制进入老年代的年龄
5. 对象动态年龄判断
当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的
50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,
例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄3的多个年龄对象总和超过了Survivor区域的50%,此时就会
把年龄3(含)以上的对象都放入老年代。
6. 老年代空间分配担保机制

本质就是,当老年代剩余空间小于新生代现有使用空间时,如果配置了担保机制,则看当前老年代剩余空间是否比之前回收的时候进入老年代的平均空间大小小,
如果小的话则先进行full gc,否则就进行minor gc。
(因为如果先minor gc,过程中再full gc, 比直接full gc然后再minor gc耗时更长)
四、对象内存回收机制
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;
}
}
可达性分析
将"GC Roots" 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的
对象都是垃圾对象
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
2. 如何判断一个类是无用类(类什么时候会被卸载)
- 类的所有对象都被回收,且没被引用
- 类的类加载器被回收(表明使用new等方式创建的类一般不会被卸载,只有自定义类加载器可能会被回收)
- 类的class对象被回收
五、日均百万级交易订单系统JVM参数设置实例
对于指令:
cmd
java -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M-jar microservice-eureka-server.jar
场景:
- 大促销期间,抢购发生在几分钟之内
- 每秒产生1000多订单
- 假设有3个服务器,平均每个服务器每秒的订单数是300单
- 每个订单对象1kB,对于其他对象例如库存、优惠卷、用户信息等等,都是对象,假设扩为20倍,也就是每秒创建的内存达到
300 * 20 * 1kB = 60M
JVM结构图:

内存分析:
大约13秒左右Eden区占满,此时出发Minor GC,将对象移动到Survivor区
此时13秒以及13秒之前的对象被清理,第14秒的对象由于业务没有完成,不会被回收,但是由于动态判断机制
60(为了简化计算,假设为60M) > 100 * 0.5 = 30M,对象进入老年代,此后,Minor GC不会回收这60M对象,
所以每13秒,就会有60M进入老年代,所以在2G / 13 * 60M 时间之后,会出发full GC,此时表现为full gc频繁(full gc确保小时为单位执行最佳)。
优化:
增加新生代大小
-Xmn2048M eden区大小
cmd
java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M-jar microservice-eureka-server.jar

在minor gc的时候,13秒钟的60M对象可以移动到Survivor区,因为Survivor区大小为2048M,
所以后续再次触发Minor GC,可以将之前的60M对象清除,减少full gc的次数。