随笔 - 11,  文章 - 4,  评论 - 0,  阅读 - 13765

 

一.JVM是什么      

     JVM是Java Virtual Machine的缩写,即JAVA虚拟机。JVM 是运行Java 程序的跨系统平台。JVM针对不同操作系统,实现了不同版本JVM,为JAVA程序运行屏蔽了底层操作系统差异,使得JAVA程序具备跨平台特性。

     随着JVM 发展, 已经支持运行其它语言,例如 groovy 脚本语言, Scala函数式语言,Kotlin、JRuby 等。

 

二. JVM 架构模型

      JVM 是基于栈的指令集架构设计的, 实现跨平台特性。

      目前主流有两种架构指令集: 

              基于栈式架构:

                          1.设计实现简单,适用资源受限的系统。

                          2.避开寄存器分配难。使用零地址指令方式分配。

                          3.由于指令都是零地址,指令运行依赖于操作栈,指令集小,编译容易实现。

                          4.不需要硬件支持,可移植性好,更容易实现跨平台。

              基于寄存器架构:

                          1.指令集架构完全依赖硬件,可移植性差。

                          2.指令执行高效,性能强。

                          3.花费更少的指令执行操作。

                          4.大部分基于寄存器的指令,都是基于 地址 指令 设计。

                        典型的 X86二进制指令,以及 Android 的 Davlik 虚拟机指令。

二.JVM内存结构

      JVM运行时,内存结构由程序运行相关的 程序计数器, 虚拟机栈, 本地方法栈  和  方法区,堆 组成。

 

 

 

 

 

 

程序计数寄存器(Program Counter Register): 

          寄存器用来存储下一条指令的地址,让执行引擎读取下一条指令。每个线程栈对应一个寄存器。线程中断或恢复时,通过寄存器保存执行指令地址,可以快速恢复执行。

 

JAVA虚拟机栈(JVM Stacks):

         每个线程在创建时,都会对应创建一个虚拟机栈。虚拟机栈内部由一个个栈帧(Stack Frame)组成,一个栈帧对应一个方法的调用。栈帧由局部变量表,操作栈,动态链接,方法返回地址组成。

          1.当虚拟机栈设置为固定大小时,可能抛出 StackOverflowError 异常。(-Xss 设置虚拟机栈最大可用空间)

          2.当虚拟机栈设置为动态扩展时,可能抛出 OutofMemoryError 异常。

         JVM对虚拟机栈的栈帧进行压栈 和出栈操作。遵循 先进后出 / 后进先出 原则。在运行线程中,具体某个时间点,只有一个活动栈帧,即栈顶栈帧是有效的。这个栈帧称为当前栈帧(Current Frame),

 对应的方法称为当前方法(Current Method),定义当前方法的类,称为当前类(Current Class)。执行引擎只针对当前栈帧操作。

         JAVA 有两种退出当前方法的方式,一种是方法正常返回,使用 return 指令(void 返回值默认在方法最后面添加return 指令);另一种是抛出异常。这两种方式,最终会导致栈帧被弹出。

       

       局部变量表(Local Variables):是一个数字数组,主要用于存储方法的参数(形参)和在方法内定义的局部变量。数据类型包括 基本数据类型,对象引用 以及 returnAddress 类型。在方法内定义局部变量是线程私有,不存在数据安全问题。即不会出现被其它线程修改,导致出现并发问题。但由形参传递过来的引用类型则看具体使用情况。比如:String类型是final修饰的类,不可变类,虽然是引用类型,但也不存在并发问题。 局部变量表的大小在编译期间就确定了,保存在方法code属性 maximum local variables 的数据项中。运行期不会改变大小。方法调用结束后,随着方法栈帧一起销毁。

        局部变量表 的基本存储单元是变量槽(Slot),32bit(4字节)长度。数据类型中,用32bit(4字节)来表示的类型(int,char),占用一个变量槽(包括 returnAddress 类型),64bit(8字节)(long,double)的占用两个变量槽。

       byte,short, char 在存储前都会转换为 int类型。boolean 也会转换成int类型, 0-表示false,非0表示true。

        当前方法是实例方法时,该对象引用this将会保存在局部变量第一个变量槽。即index = 0。其它变量依次按参数定义顺序排列。

       局部变量表的槽位是可以重复使用的。

       局部变量表的变量(除this变量以外)必须手动赋值,否做无法使用。

       局部变量表的变量是垃圾回收的跟节点,只要被局部变量表直接或间接引用的变量,都不会被回收。

    

       操作数栈(Operand Stack)或表达式栈:在方法执行过程中,根据字节指令,往栈中写入数据或提取数据,即入栈(push)和出栈(pop)。

                操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

                操作数栈深度在编译期间确定好,具体值保存在 code属性中max_stack的值。

                操作数栈中可以保存任何数据类型。32bit(4字节)占一个栈单位深度,64bit(8字节)占两个栈单位深度。

                操作数栈通过标准入栈(push)或出栈(pop)操作来完成一次数据访问。非访问索引式访问。

                解释引擎是基于栈的执行引擎,而操作数栈就是栈的具体实现。

 

 

 

 

       栈顶缓存技术(Top-of-Stack): 操作数存储在内存中,频繁的读取会影响执行速度。HotSpot JVM的设计者提出栈顶缓存,将栈顶元素全部缓存在物理CPU寄存器上,降低对内存的读写次数,提高执行引擎的执行效率。

       动态链接(Dynamic Linking): 每个栈帧内部都有一个引用,记录该方法在运行时常量池中的位置。这个引用是为了支持当前方法的代码实现动态链接(Dynamic Linking)。invokeDynamic 指令。Java源文件编译到字节码文件中时,变量和方法引用都作为符合引用(Symbolic Reference)保存在class文件的常量池中。动态链接将这些符号引用转换成调用方法的直接引用。

 

       方法返回地址(Return Address):调用该方法的PC寄存器的值。

       

       附加信息: 与JVM实现相关的一些附加信息。对程序调试提供支持的信息。

 

      本地方法栈(Native Method Stacks): Java调用非Java代码的接口。本地方法,在Java中用native修饰的方法,在C/C++中通过JNI方式实现。通过本地方法,Java可以直接和操作系统交互。

 

堆区(Heap): 在JVM启动时,即创建堆。堆是JVM管理的最大一块内存空间。Java 所有线程共享堆空间,并且线程还可以划分线程私有的缓冲区(Thread Local Allocation Buffer, TLAB)。对象实例和数组默认都分配在堆上。方法结束后,堆中对象并不会马上被回收,而是在垃圾清理时进行。GC(Garbage Collection,垃圾收集器) 执行垃圾回收主要就是关注堆回收的效率。

      -Xms: 表示堆的起始内存大小。等价于 -XX:InitialHeapSize。默认情况,物理电脑内存 / 64。

      -Xmx:   表示堆的最大内存空间。等级于 -XX:MaxHeapSize。 默认情况,物理电脑内存 / 4 。

                   当堆内存超过 mx 配置大小,抛出 OutofMemoryError 异常。

                   生产环境,一般将 -Xms 和 -Xmx 设置为相同的值,避免垃圾回收重新分配堆大小,提高性能。

 

 

     堆中的对象分为两类:

         1.生命周期较短的瞬时对象。

         2.生命周期非常长的对象。

 配置堆中 新生代 和 老年代的结构占比:

     默认 -XX:NewRatio=2,表示新生代占1,老年代占2,即新生代占整个堆的1/3。

    Eden 和 两个Survivor 空间 默认配置 -XX:SurvivorRatio=8,即默认占比例  8:1:1。

 

对象的内存布局:

     在HotSpot虚拟机中,对象在内存中分成三个部分: 对象头(Header), 实例数据(Instance Data), 对齐填充(Padding)。

    32位中:   

                          MarkWord:4字节;  

                         class对象指针(Klass Pointer): 4字节。

                         实例数据(Instance Data):  实际数据大小。

                         对齐填充(Padding): 按8 字节对齐。

    64位中:   

                          MarkWord:8字节;  

                         class对象指针(Klass Pointer): 8字节。开启压缩指针则4字节。jdk7之后默认开启。

                         实例数据(Instance Data):  实际数据大小。

                         对齐填充(Padding): 按8 字节对齐。

  • 对象头(Header):
    • Mark Word(标记字段):关于锁的信息。对象的Mark Word部分占4个字节/8个字节,表示对象的锁状态(比如轻量级锁的标记位,偏向锁标记位),另外还可以用来配合GC分代年龄、存放该对象的hashCode等。
    • Klass Pointer(Class对象指针):Class对象指针的大小也是4个字节/8个字节,其指向的位置是对象类型对应的Class信息实例(其对应的元数据对象)的内存地址。
    • 数组长度:如果对象是数组类型,占用4个字节/8个字节,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
  • Instance Data(对象实例数据):这里面包括了对象的所有成员变量,其大小由各个成员变量的大小决定,比如:byte和boolean是1个字节,short和char是2个字节,int和float是4个字节,long和double是8个字节,reference是4个字节。
  • padding data(对齐):如果上面的数据所占用的空间不能被8整除,padding则占用空间凑齐使之能被8整除。被8整除在读取数据的时候会比较快。

      实例分析:  

      java.lang.String 类:

对象字节数对齐填充字节实际内存大小备注
Byte 13 3 16

 

 

Short 14 2 16  
Integer 16 0 16  Integer对象的字段其实只有value一个,其他都是类变量。

实际内存大小 = 对象头 + value属性 + 填充对齐字节 = 12 + 4 + 0 = 16字节

Long 20 4 24  
Float 16 0 16  
Double 20 4 24  
Boolean 13 3 16

 

Character 14 2 16  
String  20 4 24

 Mark Word(标记字段) 8 字节,  Klass Pointer(Class对象指针) 4字节(开启压缩指针), 实例字段 hash 是int 类型 4 字节,实例字段 value char数组,对象引用, 4字节,补齐 4字节。合计24字节。实际内存大小 = 对象头 + hash 字段 + value字段 + 填充对齐字节 = 12 + 4 + 4 + 4 = 24字节

Object 12 4 16

实际内存大小 = 对象头 + 填充对齐字节 = 12 + 4 = 16字节

ArrayList

24 0 24

实际内存大小 = 对象头 + modCount字段 + size字段 + elementData字段 + 填充对齐字节 = 12 + 4 + 4 + 4 + 0 = 24字节

HashMap

44 4 48

实际内存大小 = 对象头 + 字段 + 填充对齐字节 = 12 + 8 * 4 + 4 = 48字节

 

    对象分配过程概述:

      1.当程序创建一个对象时(大部分是通过new关键字),优先保存到伊甸园(Eden)。

      2.当伊甸园空间满时,这时还需要创建对象,则GC对伊甸园进行垃圾回收(Minor GC), 将伊甸园中不再被其它对象所引用的对象进行销毁。再将需要创建的对象放到伊甸园区。

      3.然后将伊甸园中剩余的对象移动到幸存者0区(Survivor 0)。

      4.再次GC后,将伊甸园和幸存者区0 没有回收的对象放到 复制到 幸存者1区(Survivor 1)。

      5.之后垃圾回收,重新 3 和 4 步骤。

      6.当经历了多次Minor GC垃圾回收,对象还没被回收,则将对象保存到老年代(Old)。 一般默认是15次,不同JDK版本会有差别。具体可以通过参数 -XX:MaxTenuringThreshold=<N> 设置。

      7.当老年代满时,触发 Major GC,进行老年代垃圾清理。

      8.若老年代清理后,依然无法保存对象,则抛出内存溢出异常。 java.lang.OutofMemoryError: Java  heap space

 

    对象分配过程 - TLAB:

       JVM在堆区为每个线程分配了一个私有缓存区域。每个线程私有,避免一些线程安全问题,同时提高内存分配吞吐量。因此也叫快速分配策略。

 

    逃逸分析: 

        1.栈上分配。将堆分配转换成栈分配。

        2.同步省略。一个对象被发现只有一个线程被访问到,则对这个对象操作不考虑同步。锁消除。

        3.分离对象 或标量替换。

   标量(Scalar)是指一个无法再分解成更小的数据的数据。Java 中原始数据类型就是标量。

   聚合量(Aggregate)是可以分解的数据。Java的对象就是一个聚合量,可以再分解成 聚合量 和 标量。

 

    方法区:

        方法区在JDK7之前方法区叫永久代(Permanent generation),JDK8之后使用元空间(Metaspace)取代方法区。元空间不在虚拟机设置的内存中,而是使用本地内存。

        方法区具体存储被JVM加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等。

 

 

 

 

 

 

 

 

    CodeCache(JIT编译优化):

 

 

         

 

posted on   流羽  阅读(49)  评论(0编辑  收藏  举报
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5
点击右上角即可分享
微信分享提示