前言
JVM包含两个子系统和和两个组件,两个子系统为为类装载(Class Loader)和执行引擎(Execution engine);两个组件为运行时数据区(Runtime data area),本地接口(Native Interface)。
类装载(Class Loader):根据给定的全限定名类名(如:java.lang.Object)来装载class 文件到 运行时数据区(Runtime data area )中的方法区(method area)
执行引擎(Execution engine):执行classes 中的指令。
本地接口(Native Interface) :与native libraries 交互,是其他编程语言交互的接口。
运行时数区域(Runtime data arae):这就是我们常说的JVM 内存
在我们将代码编译成.class文件后,我们的类加载器就会将字节码文件加载到内存(方法区)中,再交给我们的执行引擎区解析成我们CPU可以执行的命令,再交由CPU执行
一、JVM内存结构
二、New对象后在JVM内部会经过哪些流程
1.类加载
new一个对象后,首先看这个类是否已经被类加载器加载过(一般只有当类用到时才加载,懒加载),如果没加载就根据双亲委派模型加载,加载过就下一步
双亲委派机制:
2.分配内存
类加载后开始分配内存,分配内存分三种情况,
一、通过逃逸分析去分析你这个对象是否被外部引用,比如一个void方法里面啥也不干,new一个对象,那么这个对象就属于不会被外部引用(废代码),此时,JVM为了减少GC压力会将此对象内存分配在栈中,随着方法的调用完毕,此对象随着栈空间一起被回收。甚至因为jvm内部机制标量替换这个对象都可能不会被New出来
逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。
通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优先分配在栈上(栈上分配),
标量替换:通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配
标量与聚合量:标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。
二、分配在堆中的Eden区,此时需要谈到分配地址的方式
2.1.指针碰撞:如果分配内存时,eden里面的内存是规整的,就是用过的内存都在一边,没用过的都在另一边,此时分配内存就只需要将指针指示器向空闲的内存移动一段与内存大小相同的距离即可
2.2.空闲列表:如果eden里面内存不是规整的,那么jvm会维护一个列表记录哪些内存时可用的并且有多大,会从空闲列表里面找出足够容纳对象的内存分配给对象,在更新空闲列表
2.JVM里面分配对象是非常频繁的事情,所以在分配内存时会有并发的问题,即两个线程的对象争抢同一块内存地址,此时JVM解决此问题的方式也有两种
2.1:对分配内存的操作采用同步处理,采用CAS锁+失败重试机制来保障分配内存的原子性
2.2:采用TLAB(Thread Local Allocation Buffer),相当于每个线程在堆上已经分配好了一定 的空间,哪个线程要创建对象分配内存就在哪个线程的TLAB上分配,这样就不存在并 发的问题了。
如果JVM开启了TLAB,一般在TLAB满了之后才会采用CAS锁+重试的分配机制
三、大对象直接进入老年代,在jvm中可以配置大对象的大小,当产生的对象内存超过这个值就将直接分配到堆中的老年代里面,默认配置的大对象大小为
3.对象的初始化
1.初始化零值:将分配到到的内存空间给对象都赋上零值,比如int类型的初始值就是0;
2.设置对象头:初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
3.初始化:执行init方法,一般也就是我们的构造方法为对象赋值
上述如有错误欢迎在评论区探讨
总结
这篇文章主要是简单阐述了new 对象后JVM做了哪些事情,可以从一个简单的关键字将java内存模型,类加载,内存分配规则,已经JVM内部各种优化机制等知识点串联起来,当然,这里只是描述了对象的诞生,下篇文章将一起探讨对象是如何死亡的(GC)。如果觉得有帮助,记得点赞收藏。