JVM系列(三) - 对象创建过程以及内存分配机制

内容导读

  • 对象的创建过程
  • 内存的分配方法以及分配时面临的问题和解决方案
  • 什么是对象头
  • 对象栈上创建: 逃逸分析和标量替换
  • 对象内存回收

一. 对象的创建过程

对象的创建过程.png
  • 类是否加载
    检查Class文件是否已经被类加载子系统加载到内存.没有的话则走类的加载过程(load-link-init).

  • 分配内存
    类加载完后, 需要在堆内开辟一块内存区域.
    但是在分配内存时, 需要解决两个问题:

    1. 内存如何分配
      首先, 第一个问题内存如何分配, JVM给出的结局方案是指针碰撞空闲列表
      指针碰撞
      JVM默认使用指针碰撞, 如果Java是一块连续的内存,指针的一边是使用过的内存, 一边是未使用的内存, 而指针所在的位置就是两块内存的边界. 当创建新对象时, 指针会向未使用的内存移动新对象大小的内存距离.
      空闲列表
      对于不连续的内存, 无法使用指针碰撞的方式, JVM则需要维护一个列表, 记录未使用的内存区域的地址.当创建新对象时, 从列表中找出一块大小适合的内存存放该对象, 并更新列表

      内存分配方式.png

    2. 并发的情况下, 如何避免分配失败?
      CAS
      JVM采用CAS和失败重试的方式保证内存分配的原子性

      线程本地分配缓冲(Thread Local Allocation Buffer, TLAB)
      为每个线程预留一块内存, 每个线程在自己的内存空间上创建对象.如果TLAB依旧创建失败, 则会自动采用CAS的方式解决. 可以通过-XX:+UseTLAB开始TLAB, -XX:TLABSize指定TLAB的大小

  • 初始化(赋默认值)
    内存分配完毕后, JVM会为分配的内存设置默认值

  • 设置对象头
    对象在JVM中一共分为三块: 对象头, 实例数据, 对齐填充
    而对象头由分为: MarkWord, Klass Point(类型指针), 数组长度4个字节

    对象的构成.png

    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为好

二. 内存分配机制

对象内存分配的流程如图所示:


对象内存分配过程.png

栈上分配

通常对象在堆上分配, 当对象不在被引用后, 会被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

老年代空间担保机制.png

三. 对象内存回收

回收算法

  • 引用计数法
    增加一个对象引用计数器.每有一个地方引用对象, 引用计数器就加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反射

你可能感兴趣的:(JVM系列(三) - 对象创建过程以及内存分配机制)