内容导读
- 对象的创建过程
- 内存的分配方法以及分配时面临的问题和解决方案
- 什么是对象头
- 对象栈上创建: 逃逸分析和标量替换
- 对象内存回收
一. 对象的创建过程
类是否加载
检查Class文件是否已经被类加载子系统加载到内存.没有的话则走类的加载过程(load-link-init).-
分配内存
类加载完后, 需要在堆内开辟一块内存区域.
但是在分配内存时, 需要解决两个问题:-
内存如何分配
首先, 第一个问题内存如何分配, JVM给出的结局方案是指针碰撞 和空闲列表
指针碰撞
JVM默认使用指针碰撞, 如果Java是一块连续的内存,指针的一边是使用过的内存, 一边是未使用的内存, 而指针所在的位置就是两块内存的边界. 当创建新对象时, 指针会向未使用的内存移动新对象大小的内存距离.
空闲列表
对于不连续的内存, 无法使用指针碰撞的方式, JVM则需要维护一个列表, 记录未使用的内存区域的地址.当创建新对象时, 从列表中找出一块大小适合的内存存放该对象, 并更新列表
-
并发的情况下, 如何避免分配失败?
CAS
JVM采用CAS和失败重试的方式保证内存分配的原子性线程本地分配缓冲(Thread Local Allocation Buffer, TLAB)
为每个线程预留一块内存, 每个线程在自己的内存空间上创建对象.如果TLAB依旧创建失败, 则会自动采用CAS的方式解决. 可以通过-XX:+UseTLAB开始TLAB, -XX:TLABSize指定TLAB的大小
-
初始化(赋默认值)
内存分配完毕后, JVM会为分配的内存设置默认值-
设置对象头
对象在JVM中一共分为三块: 对象头, 实例数据, 对齐填充
而对象头由分为: MarkWord, Klass Point(类型指针), 数组长度4个字节
MarkWord
保存对象的hashCode, 锁标记, gc年龄, 偏向ID等信息.32位系统占4个字节, 64位系统占8个字节
Klass Point
类型指针: 指向方法区中类元信息.开启压缩占4个字节, 关闭压缩占8个字节
数组长度
对象时数组类型的才有, 所以图上以虚线表示, 占4个字节对象最终的大小始终是8的倍数
初始化
执行方法: 为属性赋值和执行构造方法
指针压缩
JDK1.6以后支持指针压缩, 可以通过-XX:+CompressedOops开启
为什么要进行指针压缩?
1.在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力
2.为了减少64位平台下内存的消耗,启用指针压缩功能
3.在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得jvm
只用32位地址就可以支持更大的内存配置(小于等于32G)
4.堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
5.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内
存不要大于32G为好
二. 内存分配机制
对象内存分配的流程如图所示:
栈上分配
通常对象在堆上分配, 当对象不在被引用后, 会被GC回收.如果堆上的对象非常多, GC的时间会很长, 影响性能. 这个时候JVM会通过逃逸分析和标量替换决定一些对象可以直接在栈上分配. 栈上分配的对象会随栈帧的出栈而销毁, 不用通过GC回收, 节省内存空间.
- 逃逸分析
某个方法内创建的对象, 在方法外部不存在引用关系, 并且可以进一步分解. JVM对于这种对象, 不会在堆上创建, 而是在栈上分配.
-XX:+DoEscapeAnalysis 开启逃逸分析, JDK1.7默认开启
- 标量替换
首先得清楚什么是标量?
标量和聚合量
标量: 即不能进一步分解的量. Java的基本类型就是不可分解的标量.
聚合量: 可以进一步分解的量, 就是聚合量, 比如对象
通过逃逸分析确定对象不会被外部访问, 且可以进一步分解时, JVM不会创建对象, 而是将对象的成员变量分成若干个方法的变量, 而这个被替换的成员变量可以直接在栈帧或者寄存器里分配, 从而避免对象不够分配的情况.
-XX:+EliminateAllocations 开启标量替换
案例:
package com.learn.jvm;
/**
* Description:逃逸分析
* -XX:+DoEscapeAnalysis -XX:+EliminateAllocations -XX:+PrintGC -Xmx15m -Xms15m
*
* 对象栈上分配, 必须得开启逃逸分析和标量替换, JDK1.7以后默认开启
* -XX:+DoEscapeAnalysis -XX:+EliminateAllocations 有效 只有一次GC
* -XX:+DoEscapeAnalysis -XX:-EliminateAllocations 无效 大量GC
* -XX:-DoEscapeAnalysis -XX:+EliminateAllocations 无效 大量GC
* -XX:-DoEscapeAnalysis -XX:-EliminateAllocations 无效 大量GC
*
*
*/
public class EscapeAnalysisTest {
public static void main(String[] args) {
final long l = System.currentTimeMillis();
for (int i = 0; i < 100_000_000; i++) {
allocation();
}
final long end = System.currentTimeMillis();
System.out.println(end - l);
}
private static void allocation() {
User user = new User();
user.setAge(1);
user.setName("test");
}
}
对象在Eden分配
新创建的对象会分配在Eden区,当发生Minor GC时, 没被回收的对象, 会进入Survivor区, 同时会记录对象的分代年龄. 每发生一次Minor GC, 分代年龄就会加1, 到达一定阈值后, 会进入老年代.
影响新对象的参数有:
-Xms : 堆大小, 堆越小, Eden和Survivor区就越小, MinorGC就越频繁, 导致分代年龄增长快
–XX:NewRatio : 设置年轻代和老年代的比例, 也影响年轻代的大小
-XX:SurvivorRatio=8 : 设置Eden和Survivor的比例, 影响Eden的大小
-XX:+UseAdaptiveSizePolicy : 默认开启, 自动调整Eden和Survivor的比例, 开启后不是绝对的8:1:1
-XX:MaxTenuringThreshold : 设置对象年龄, 超过该阈值的对象, 进入老年代
测试对象在Eden分配
package com.learn.jvm.allocation;
/**
* 对象分配测试 -Xms15m -Xmx15M -XX:+PrintGCDetails Eden: 4.5M Survivor: 1M Old:11M
*/
public class ObjectAllocationTest {
public static void main(String[] args) {
byte[] bytes = new byte[1024 * 1024 * 2];
byte[] bytes1 = new byte[1024 * 1024 * 2];
byte[] bytes2 = new byte[1024 * 520];
}
}
// 只创建 byte[] bytes = new byte[1024 * 1024 * 2];
Heap
PSYoungGen total 4608K, used 2689K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
eden space 4096K, 65% used [0x00000000ffb00000,0x00000000ffda0488,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 11264K, used 2048K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000)
object space 11264K, 18% used [0x00000000ff000000,0x00000000ff200010,0x00000000ffb00000)
Metaspace used 3185K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 348K, capacity 388K, committed 512K, reserved 1048576K
可以看到Eden一共4M, 使用了65%
// 当创建byte[] bytes = new byte[1024 * 1024 * 2]; byte[] bytes1 = new byte[1024 * 1024 * 2];时
Heap
PSYoungGen total 4608K, used 2773K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
eden space 4096K, 67% used [0x00000000ffb00000,0x00000000ffdb5420,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 11264K, used 4096K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000)
object space 11264K, 36% used [0x00000000ff000000,0x00000000ff400020,0x00000000ffb00000)
Metaspace used 3194K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 350K, capacity 388K, committed 512K, reserved 1048576K
Eden还是用67%, 但是PerOldGen则用了36%, 说明有一个对象进入了老年代
// 当三个对象都创建时
Heap
PSYoungGen total 4608K, used 3214K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
eden space 4096K, 78% used [0x00000000ffb00000,0x00000000ffe23a68,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 11264K, used 4096K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000)
object space 11264K, 36% used [0x00000000ff000000,0x00000000ff400020,0x00000000ffb00000)
Metaspace used 3248K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 352K, capacity 388K, committed 512K, reserved 1048576K
Eden使用了78%, ParOldGen使用了36%, 说明Eden分配了两个对象, 有一个对象进入了老年代
大对象直接进入老年代
对于Eden和Survivor区都放不下的对象会直接进入老年代., -XX:PretenureSizeThreshold=100000(单位字节), 设置大对象的阈值, 超过该大小的对象直接进入老年代
测试
package com.learn.jvm.allocation;
/**
* Description: -Xms15m -Xmx15M -XX:+PrintGCDetails 大概内存分配 Eden: 4.5M Survivor: 1M Old:11M
*/
public class LargeObjectTest {
public static void main(String[] args) {
byte[] obj = new byte[1024 * 1024 * 5];
}
}
Heap
PSYoungGen total 4608K, used 2773K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
eden space 4096K, 67% used [0x00000000ffb00000,0x00000000ffdb55e0,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 11264K, used 5120K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000)
object space 11264K, 45% used [0x00000000ff000000,0x00000000ff500010,0x00000000ffb00000)
Metaspace used 3231K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 350K, capacity 388K, committed 512K, reserved 1048576K
Eden: 大约4M, Survivor: 512K, 放不下5M的对象,
ParOldGen: 使用了45% , 说明对象直接进入了老年代
对象动态年龄判断
-XX:TargetSurvivorRatio=n : 指定年龄
当survivor区有一批对象的总大小大于survivor区的50%, 那么这批年龄大于n的对象, 直接进入老年代.
对象动态年龄判断一般发生在Minor GC之后
老年代空间担保机制
年轻代每次MinorGC之前, 都会计算下老年代的剩余可用空间, 如果剩余可用空间小于年轻代里对象大小的总和, 就会检查是否设置了 -XX:-HandlePromotionFailure该参数, 如果配置了, 则会检查老年代剩余空间是否小于历史年轻代MinorGC后进入老年代的对象的平均值
如果小于的话, 则进行Full GC, 如果FullGC之后, 还不够, 则会发生OOM.
如果大于的话, 则只进行Minor GC. 但是MinorGC后, 需要进入老年代的对象依旧大于老年代的剩余可用空间, 则需要进行FullGC, 如果FullGC之后, 还不够, 则会发生OOM.
目的:可以减少一次Full GC
三. 对象内存回收
回收算法
- 引用计数法
增加一个对象引用计数器.每有一个地方引用对象, 引用计数器就加1; 引用失效了就减1. 当计数器为0时, 可以被回收
弊端: 无法解决循环依引用的问题., 比如A对象有个成员变量b, B对象有个成员变量a, 但是对象A和对象B都没有任何地方在引用它们, 但是引用计数算法会认为A对象引用了B对象.
- 可达性分析算法
在说可达性分析之前, 先了解下什么是GC Root
GC Root: 主要指线程栈的本地变量, 静态变量, 本地方法栈的变量等.
以GC Root作为起点, 向下搜索对象. 所有找到的对象都是非垃圾对象, 不会被回收.
常见的引用类型
- 强引用
存在引用关系, 就不会被回收.不管是否会OOM.
Object object = new Object()
- 软引用
将对象用SoftRefernce类型包装, 正常情况下不会被回收. 如果GC做完后发现释放不出什么空间, 那么会在下次GC时回收.可以用于内存敏感的高速缓存.
public static SoftReference reference = new SoftReference<>(new User());
- 弱引用
使用WeakReference包装的对象, 当发生GC时, 会被回收. - 虚引用
一般是垃圾回收器会用到.
无论如何都会被回收的对象.
如何判断一个类"无用"
方法区也会发生OOM, 因此方法区也要回收, 一般是回收无用的类.但是无用的类的条件很苛刻.
- 该类的所有实例对象都已被回收.
- 加载该类的类加载器也已被回收
- 该类对应的java.lang.Class对象没有任何引用, 无法在任何地方通过反射获取该类的实例
创建类实例的方法
- new的方法
- Class.forName()
- 通过java.lang.Class反射