JVM 对象创建与内存分配分析

JVM 对象创建与内存分配分析

对象创建流程图

在这里插入图片描述

1.类加载检查:

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过 。如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。

2.分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类 加载完成后便可完全确定,为
对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
①划分内存的方式:
指针碰撞(默认方式): 如果是 Java内存堆 是整齐的,所有使用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
在这里插入图片描述
⒉空闲列表:
如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
在这里插入图片描述

②在并发情况下划分内存的方式:
⒈CAS(compare and swap):
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
⒉本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过­XX:+/­-UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启­XX:+UseTLAB),­XX:TLABSize 指定TLAB大小。
在这里插入图片描述

3.初始化

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

4.设置对象头

初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对
象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

32位对象头

在这里插入图片描述

64位对象头

在这里插入图片描述

5.执行#init方法

执行#init方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值,和执行构造方法。

对象大小

引用依赖:

        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>

代码示例:

package com.example.demo.test4;

import org.openjdk.jol.info.ClassLayout;


public class JOLTestDemo {
    public static void main(String[] args) {
        ClassLayout layout = ClassLayout.parseInstance(new Object());
        System.out.println(layout.toPrintable());

        System.out.println();
        ClassLayout layout1 = ClassLayout.parseInstance(new int[]{});
        System.out.println(layout1.toPrintable());

        System.out.println();
        ClassLayout layout2 = ClassLayout.parseInstance(new A());
        System.out.println(layout2.toPrintable());
    }
}

class A{
    int id;
    String name;
    byte b;
    Object o;
}

// 输出结果:
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


[I object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     0    int [I.<elements>                             N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total


com.example.demo.test4.A object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           63 cc 00 f8 (01100011 11001100 00000000 11111000) (-134165405)
     12     4                int A.id                                      0
     16     1               byte A.b                                       0
     17     3                    (alignment/padding gap)                  
     20     4   java.lang.String A.name                                    null
     24     4   java.lang.Object A.o                                       null
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

指针压缩:

1.jdk1.6开始,在64位操作系统重,JVM支持指针压缩;
2.JVM配置参数为:UseCompressedOops。compressed(压缩),oop(对象指针);
3.启用指针压缩: -XX:+UseCompressedOops(默认开启),关闭指针压缩:-XX:-UseCompressedOops

指针压缩的作用:
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;

对象内存分配

对象内存分配流程图
在这里插入图片描述

对象栈上分配

我们通过JVM内存分配可以知道JAVA中的对象都是在堆上进行分配,当对象没有被引用的时候,需要依靠GC进行回收内存,如果对象数量较多的时候,会给GC带来较大压力,也间接影响了应用的性能。为了减少临时对象在堆内分配的数量,JVM通过逃逸分析确定该对象不会被外部访问。如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。
对象逃逸分析: 分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参
数传递到其他地方中。JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优先分配在栈上(栈上分配),JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)。

标量替换: 通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认开启。

标量和聚合量: 标量即不可被进一步分解的量,而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

大量的对象被分配在Eden区,Eden区满了后会触发Minor GC,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块Survivor区,下一次eden区满了后又会触发Minor GC,把Eden区和Survivor区垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空的Survivor区,因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例是很合适的,让Eden区尽量的大,Survivor区够用即可,JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy。

大对象在内存中的分配

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效。
如,设置JVM参数:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC ,再执行下上面的第一个程序会发现大对象直接进了老年代。
这样做 是避免 大对象分配内存时的复制操作 使得效率降低

长期存活的对象将进入老年代

虚拟机采用分代收集的方式来管理内存 。为了能够准确的将对象放在新生代、老年代,虚拟机给每个对象设置一个对象年龄计数器。
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor
空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度
(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代
的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

动态年龄判断机制

当前存放对象的Survivor内存区中,一批对象的占用内存的总大小 >Survivor区域的内存大小 的50% 时(由参数
-XX:TargetSurvivorRatio控制),≥这批对象年龄最大值的对象(在Survivor区中的所有对象),就可以直接进入老年代。
例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会
把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年
龄判断机制一般是在minor gc之后触发的。

老年代空间分配担保机制

年轻代每次进行minor gc之前JVM都会计算 老年代的剩余可用空间。当剩余可用空间<年轻代里所有的对象的大小之和(包含垃圾对象),那么就会触发full gc , 对年轻代和老年代进行垃圾回收,如果垃圾回收之后的空间仍然不足存放新的对象就会触发OOM。
如果minor gc 之后存活对象需要移动到老年代,此时老年代可用空间不足,也会触发full gc。老年代空间不足,依然会触发OOM。
老年代空间分配担保机制 由 参数 -XX:-HandlePromotionFailure 控制,jdk1.8默认开启。
在这里插入图片描述

对象内存回收

内存堆中,存放堆垃圾回收前的第一步需要判断对象是否为垃圾对象。
判断对象是否为垃圾对象的算法:
1.引用计数法;
2.可达性分析算法;

引用计数法

给对象添加引用计数器,当对象被引用时,计数器+1,当引用失效,则-1,如果对象的引用计数器为0 时,这个对象就是不被使用的。
优点:实现方法简单,效率高;
缺点:难以解决对象循环引用的问题;
因为存在循环引用(对象A引用对象B,对象B又引用对象A,就会导致垃圾回收器不再回收这两个对象)

可达性分析算法

GC Roots 对象作为起点,从这些节点开始往后搜索引用的对象,找到对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。
**GC Roots 根节点:**线程栈的本地变量、静态变量、本地方法栈的变量等。
在这里插入图片描述

常见引用类型

Java 引用类型分为 四种:强引用、软引用、弱引用、虚引用
强引用: 普通的变量引用
如: public static User user = new User();
软引用: 将对象用SoftReference 软引用类型的对象包裹,正常情况不会被回收,但是垃圾回收机在GC 之后如果释放内存空间依旧不足,会将软引用的对象进行回收。软引用主要是用于实现内存敏感的高速缓存。
如: public static SoftReference user= new SoftReference(new User());
软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从
缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建;
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出;
弱引用: 将对象用WeakReference 弱引用引用的对象包裹,弱引用跟没引用差不多,GC会直接回收,较少使用
public static WeakReference user = new WeakReference(new User());
虚引用: 虚引用也成为幽灵引用或者幻影引用,是最弱的引用关系,几乎不用。

finalize() 方法最终判定对象是否存活

即使 可达性分析算法 中不可达的对象,也并非马上就直接被垃圾回收,这时候这些对象还有一次拯救自己的机会。
标记的前提是对象在进行可达性分析后发现没有 被GC Roots 引用。
1.第一次标记并进行第一次筛选
筛选的条件是这个对象是否有必要执行finalize() ;
当对象没有覆盖finalize() 方法,对象将直接被回收。
2.第二次标记
如果这个对象重写了finalize()方法,那么finalize()方法是对象逃脱 被垃圾回收的最后机会,如果对象要在finalize()中是自己被引用。如,将自己赋值给某个类变量或者对象的成员变量,在第二次标记是将他从 即将回收 的集合。如果对象这时候还是没有被引用,那么就会直接被回收。
finalize()方法只会被执行一次,在调用finalize()方法的机会就一次;

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

方法区主要回收无用的类,判断一个类是无用的类的条件有:
1.该类所有实例对象都被回收,在Java堆内存中不存在该类的任何实例;
2.加载该类的ClassLoader已经被回收;
3.该类的对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值