三、对象内存分配

192 阅读10分钟

对象内存分配过程


1、类加载

在使用到new关键字以后,首先检查这个指令的参数是否可以在常量池中可以找到相应的符号引用,如果没有找到或者常量池中的符号引用对应的类没有进行加载,首先对类进行加载。

2、分配内存空间

在类加载完毕以后会对新生对象进行内存空间分配,实例的大小在类加载的完成便可以确定,为对象分配空间的相当于从内存划分出

分配方法:指针碰撞法、空闲列表;

指针碰撞(默认为指针碰撞):如果Java内存是规整的,所有用过的内存存放在一边,空闲内存存放在一边中间放着一个指针作为分界点的指示器,那么分配内存仅是将指针像空闲内存那边挪动一段与对象大小相同的距离。

空闲列表:如果Java内存并不是规整的,已经使用的内存与未使用内存相互交错,那么就无法进行指针碰撞,Java虚拟机维护了一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的内存空间分配给对象并更新内存列表上的记录。

内存分配的并发问题解决:

cas:虚拟机采用cas配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。

本地线程分配缓冲(TLAB):把内存分配动作按照线程划分在不同的空间之中进行,即每个Java线程在内存中分配一块内存空间。通过-XX:+/-UseTLAB参数来控制是否开启TLAB,-XX:TLABSize指定TLAB内存大小。

3、初始化

内存分配完以后需要对分配到内存空间的变量进行初始化(不对对象头进行设置),如果设置了TLAB初始化的过程会提前到TLAB进行。

4、设置对象头

初始化以后需要将对象头进行设置,包括这个对象是哪个类的实例、对象的年龄信息、如何找到元数据信息、对象hash码等信息。一个对象可以分为:对象头、实例数据、对齐填充。

HotSpot中对象头主要包含两部分:mark word(对象自身运行时需要的数据,包括hash码、分代年龄、锁状态标识、偏向线程ID等)、klassPointer(类型指针,指向方法区中类对应的元数据信息,通过类型指针确定这个对象是哪个类的实例信息)。


5、执行init方法

执行对象的构造方法,为对象设置指定的值。


对象栈上分配

对象在堆上分配,当对象没有被引用的时候需要GC对对象进行回收,如果对象较多的时候会给GC带来压力,也影响了性能。为了减少临时对象在对上分配,Java虚拟机引入了逃逸分析。JVM通过逃逸分析来判断这个对象是否会在该方法内发生逃逸,如果确定对象不会发生逃逸那么就会在栈上对对象进行内存分配,当方调用结束后随着栈帧出栈而销毁,减少了GC的压力。

逃逸分析:分析对象动态作用域,当一个对象在方法中定义后,可能被外部方法引用,例如作为参数传递到其他的方法中。

public User test1(){
   User user=new User();
   user.setId(1);
   user.setName("zhuge");
   //TODO 保存到数据库
   return user;
}

public void test2(){
    User user=new User();
    user.setId(1);
    user.setName("zhuge");
    //TODO 保存到数据库
}

test1()中的user对象被返回给调用方法,不确定user对象的作用范围,因此不会分配到栈上。在test2()中user对象的作用范围仅仅在方法中。当test2方法结束后user对象也会被销毁,因此可以将user对象分配到栈上。JVM通过-XX:+DoEscapeAnalysis开启逃逸分析参数,来优化对象内存分配,使其通过标量替换优先在栈上进行分配。(JDK1.7之后默认开启逃逸分析)

标量与聚合量:标量即不能被进一步分解的量,JAVA的基本数据类型就是标量,标量对应的就是聚合量,可以被进一步分解,JAVA中对象就是聚合量可以进行进一步分解。(开启标量替换-XX:+EliminateAllocations)

对象在栈上分配依赖于逃逸分析与标量替换。

对象在Eden区分配

大多数对象都在Eden中分配,当Eden中没有足够空间的时候将会触发一次Minor GC。

Minor GC/Young GC:指发生在新生代的垃圾回收动作,Minor GC发生的比较频繁,回收速度也比较快。

Major GC/Full GC:指发生在老年代的垃圾回收动作。Major GC一般会触发年轻代、老年代、方法区的垃圾回收。Major GC的垃圾回收速度一般比Minor GC的垃圾回收速度要慢。

Eden与Survivor默认比例为8:1:1(JVM默认开启-XX:+UseAdaptiveSizePolicy,会导致这个比例发生变化,如果不想这个比例发生变化可以使用-XX:-UseAdaptiveSizePolicy)

当对象创建的时候首先在eden区域进行分配,当eden区域放满之后将会触发一次minor gc,对eden进行垃圾回收,将存活的对象放到s0区域。下一次eden区域放满后又会触发一次minor gc,对eden与s0区域进行回收,将eden区与s0区存活的对象挪动到s1区域,然后清空eden与s0。eden区域再次对外提供内存,当eden区放满之后将会触发minor gc,对eden、s1进行垃圾回收将存活的对象复制到s0区域然后清空s1区域。当对象在s0与s1交换一定的次数后将会被放到老年代。

大对象直接进入老年代

大对象的存储需要连续的内存空间进行存储,JVM可以通过-XX:PretenureSizeThreshold可以设置大对象的大小,如果超过设定的值对象直接进入老年代,不会进入年轻代,这个参数只是在Serial与ParNew两个收集器下有效。大对象直接进入老年代避免了大对象在年轻代频繁复制而导致效率比较低。

长期存活对象进入老年代

虚拟机采用分代收集思想来管理内存,那么内存回收必须识别哪些对象放在新生代哪些对象放在老年代。为了做到这一点JVM为每个对象设置了一个Age来标识这个对象的年龄。

如果对象在eden出生并经过一次minor gc以后存活下来,并且survivor区域能够接纳改对象那么这个对象的年龄为1,以后每次经过从s0到s1区域的挪动年龄加1,当年龄超过15岁(CMS默认为6岁)对象被移入老年代。对象晋升到老年代的阀值通过-XX:MaxTenuringThreshold来指定。

动态年龄判断

当前放在Survivor区域里的对象,一批对象的总大小超过survivor大小的50%(-XX:TargetSurvivorRatio指定),那么大于等于这批对象年龄最大值的对象就可以直接进入老年代。例如Survivor区域中有一批对象,年龄1 + 年龄2 + 年龄3 + 年龄n超过了survivor区域大小的50%,那么年龄>=n的对象将被加入到老年代。动态年龄判断的目的就是为了将多次存活的对象提前放入老年代,冬天年龄判断一般在minor gc之后进行。

老年代空间分配担保机制

年轻代分配空间之前会计算一次老年代剩余空间的大小,如果老年代的剩余空间小于年轻代所有对象之和(包含垃圾对象),那么要判断是否设置了-XX:-HandlePromotionFailure,如果有这个参数,就会看老年代剩余空间的大小是否大于每次minor gc放入到老年代的对象的平均大小。如果没有设置这个参数或者上一步比较的值是小于,那么将会触发一次full gc,对老年和年轻代进行一次垃圾回收,如果垃圾回收还是没有足够的空间存放新的对象就会发生OOM,如果minro gc之后剩余存活的对象移入到老年代还是没有足够的空间去存储,那么也会触发full gc,full gc之后还是没有足够的空间存放minor gc存活的对象也会触发oom。


对象内存回收

对象在内存回收之前需要标记出哪些是不需要回收的对象,哪些是需要回收的对象。JVM主要通过引用器计数法和可达性分析法来实现。

引用计数器法:每个对象都有一个引用计数器,每当被引用的时候计数器+1,当引用失效的时候计数器-1,知道引用计数器为0则这个对象不可再使用。引用计数器的法的效率比较高,但是不能解决循环引用的问题。

如下所示两个对象除了obja 和 objb 对他们有引用之外,两个对象间相互引用,当obja 和 objb 不在对两个对象进行引用的时候,由于这两个对象间存在相互引用所以这两个对象不能被回收。

public class 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 Root对象作为起点向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未被标记的对象为垃圾对象。

可以作为GC Root的对象:线程栈的本地变量、静态变量、本地方法栈的本地变量。


常见的引用类型

强引用:使用关键字new出来的对象,即使发生oom也不会回收该对象。

软引用:将对象使用SoftReference软引用类型包裹,正常情况下这种对象不会被回收,只有当GC发生之后没有足够的空间,则需要对该类型对象进行回收。软引用可以用来实现缓存。

弱引用:将对象使用WeakReference弱引用类型报错,当发生GC的时候会被直接回收掉。

虚引用:虚引用也成为幽灵引用,是一种最弱的引用关系,一般情况不用。

如何判断一个类是无用的引用类

1、该类所对应的所有实例已经被回收,Java堆中不存在这个类的任何实例

2、加载该类的ClassLoader已经被回收

3、该类的java.lang.Class对象没有被引用,无法在任何地方通过反射获取该类的实例