JVM对象创建和内存分配机制

JVM对象创建

创建流程:

1. 类加载

JVM遇到new指令,先检查这个指令参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始换,如果没有,就先执行相应的类加载过程。

new指令对应到java语言层面,对应new关键字、对象克隆、对象序列化等。

2. 分配内存

完成类加载后,JVM为新生对象分配内存。 类加载的时候内存大小就已确定。

内存划分方式:

  1. "指针碰撞":是默认方式,JVM堆中的内存是规整的,使用和未使用的中间有个分界点指示器,分配就是将指示器往未分配区域挪一部分。
  2. "空闲列表":JVM堆中的内存不规整,JVM维护了一个列表记录哪些内存可用,从列表中找出一块足够大的空间划分给对象实例。

内存分配的并发问题:

  1. CAS:CAS+失败重试
  2. 本地线程分配缓冲(Thread Local Allocation Buffer, TLAB):每个线程在Java堆中预先分配一小块内存。通过­XX:+/-UseTLAB 参数来设定虚拟机是否使用TLAB(JVM会默认开启­XX:+ UseTLAB),-XX:TLABSize指定TLAB大小。

3.初始化

分配的内存空间设置为初始的零值(不包含对象头)

4. 设置对象头

初始化后,JVM需要对 对象做必要的设置。对象在内存中的布局分为3部分:对象头、实例数据、对齐填充。

对象头包括两部分信息:

  • 对象的哈希码、对象的GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
  • 类型指针,指向它的类元数据的指针

底层的C源码定义的对象头:

5. 执行<init>方法

程序员的赋值,即java语言层面为属性赋值、执行构造方法。这和初始化步骤中的设置初始的零值不一样。

JVM内存分配

对象在栈上分配

JVM通过逃逸分析确定该对象不会被外部访问,如果不会逃逸可以将该对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。

java 复制代码
   // test1()中的user可能会被外部引用,对应的作用域不确定
   public User test1() {
        User user = new User();
        user.setId(1);
        user.setName("zhuge");
        //TODO 保存到数据库
        return user;
    }
    
    // test2()中的user只在本方法内部使用,这样的对象可以分配在栈内
    public void test2() {
        User user = new User();
        user.setId(1);
        user.setName("zhuge");
        //TODO 保存到数据库
    }

// 这两个开关需要同时打开,才有可能减少gc

-XX:+DoEscapeAnalysis : 开启逃逸分析参数,JDK7之后默认开启

-XX:+EliminateAllocations : 开启标量替换参数,JDK7之后默认开启

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

标量聚合量:标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。

对象在Eden分配

  1. Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
  2. Major GC/Full GC:一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢10倍以上。

Eden与Survivor区默认8:1:1, JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化。

对象进入老年代

1. Minor GC 大对象进入老年代

为新的对象在eden分配空间时,如果空间不够,触发Minor GC, Minor GC后,Surviovor没有足够空间存储存活对象, 会将存活对象移动到老年代。

2. 大对象直接进入老年代

-XX:PretenureSizeThreshold 设置大对象的大小。

如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效。可以避免为大对象分配内存时的复制操作而降低效率。

2. 长期存活对象进入老年代

对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。默认年龄=15会进入到老年代。

3. 对象动态年龄判断

当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了。

对象动态年龄判断机制一般是在minor gc之后触发的。

4. 老年代空间分配担保机制

  • 判断:老年代剩余可用空间 < 年轻代里现有的所有对象(包括垃圾对象)
  • -XX:-HandlePromotionFailure"(jdk1.8默认就设置了)
  • Full GC后还没有足够空间访新对象就会发生OOM
  • Minor GC后需要移动到老年代的对象大小 > 老年代可用空间,触发Full GC.

对象内存回收

1、对象存活判断

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;
       }
 }

当objA合objB 都是null时, 但是这两个对象都还分别有个引用,是相互引用。

2. 可达性分析法

将"GC Roots" 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象

GC Roots根节点:

  1. 线程栈的本地变量表引用的对象
  2. 方法区的静态变量(静态属性引用的对象)
  3. 方法区的常量引用对象
  4. 本地方法栈中JNI(Java Native Interface)引用的对象
  5. 被同步锁(synchronized)持有的对象
  6. JVM内部的引用,比如基本数据类型的Class对象、系统类加载器、常驻的异常对象(OutOfMemoryError)

3. finalize()方法

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

可达性分析方法标记出一个对象不可达,到宣告这个对象正式死亡,需要经过两次标记阶段:

1. 第一次标记 并进行一次筛选

筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法,对象将直接被回收。

2. 第二次标记

如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出"即将回收"的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。

注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。等下次gc时就不会执行finalize()方法,直接被回收。

2、类的回收

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?

类需要同时满足下面3个条件才能算是 "无用的类" :

  1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  2. 加载该类的 ClassLoader 已经被回收。(系统类加载器很难被回收,主要是自定义类加载器)
  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

3、常见引用类型

java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用

  • 强引用:正常定义的对象。
  • 软引用:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收。但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
java 复制代码
public static SoftReference<User> user = new SoftReference<User>(new User());
  • 弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用。 ThreadLocal就是使用了弱引用。
java 复制代码
public static WeakReference<User> user = new WeakReference<User>(new User());
  • 虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用

JVM参数

// compressed­­压缩、oop(ordinary object pointer)­­对象指针

‐XX:+UseCompressedOops : 默认开启的压缩所有指针

‐XX:+UseCompressedClassPointers : 默认开启的压缩对象头里的类型指针Klass Pointer

‐XX:+PrintGC :

‐XX:+PrintGCDetails :

JVM中的概念

压缩指针

jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩注意,压缩的是指针占用的内存大小。

  • 启用指针压缩: -XX:+UseCompressedOops(默认开启),
  • 禁止指针压缩: -XX:-UseCompressedOops
  • ‐XX:+UseCompressedClassPointers : 默认开启的压缩对象头里的类型指针Klass Pointer

为什么要进行指针压缩?

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

相关推荐
##学无止境##9 小时前
深入浅出JVM:Java虚拟机的探秘之旅
java·开发语言·jvm
阿熊不凶10 小时前
c语言中堆和栈的区别
java·c语言·jvm
集成显卡11 小时前
在JVM跑JavaScript脚本 | 简单 FaaS 架构设计与实现
开发语言·javascript·jvm·设计模式·kotlin·软件开发·faas
Warren981 天前
Java Record 类 — 简化不可变对象的写法
java·开发语言·jvm·分布式·算法·mybatis·dubbo
Co0kie_1 天前
SpringAI报错:com.github.victools.jsonschema.generator.AnnotationHelper
jvm·spring boot·ai·ai编程
蚰蜒螟2 天前
JVM安全点轮询汇编函数解析
汇编·jvm·安全
蓝澈11212 天前
jvm学习笔记之jvm的生命周期和发展历程
jvm·笔记·学习
optimistic_chen2 天前
【Java EE初阶 --- 网络原理】JVM
java·jvm·笔记·网络协议·java-ee
猪蹄手2 天前
C/C++基础详解(三)
开发语言·jvm·c++