java虚拟机

简介: java虚拟机会将管理的内存划分为若干个不同的数据区域,如下图其中虚拟机栈、本地方法栈、程序计数器是线程隔离的,即每个线程都拥有自己的虚拟机栈、本地方法栈、程序计数器。方法区、堆这两块内存是所有线程间共享的 程序计数器 当前线程所执行的字节码的行号指示器(指示器工作时就说通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理等)。
  • 1、java虚拟机的内存分区是如何的划分?
  • 2、划分的分区的作用是什么?解决什么问题?
  • 3、java对象的创建的过程?
  • 4、OOM内存溢出异常是如何产生的?

java运行时数据区

java虚拟机会将管理的内存划分为若干个不同的数据区域(有程序计数器、虚拟机栈、本地方法栈、方法区、堆),如下图
image

  • 程序计数器、虚拟机栈、本地方法栈是线程隔离的,即每个线程都拥有自己的虚拟机栈、本地方法栈、程序计数器
  • 方法区、堆这两块内存是所有线程间共享的

2、程序计数器

2.1)什么是程序计数器

程序计数器是一个记录着当前线程所执行的字节码的行号指示器。
  
JAVA代码编译后的字节码在未经过JIT(实时编译器)编译前,其执行方式是通过“字节码解释器”进行解释执行。简单的工作原理为解释器读取装载入内存的字节码,按照顺序读取字节码指令。读取一个指令后,将该指令“翻译”成固定的操作,并根据这些操作进行分支、循环、跳转等流程。
  
从上面的描述中,可能会产生程序计数器是否是多余的疑问。因为沿着指令的顺序执行下去,即使是分支跳转这样的流程,跳转到指定的指令处按顺序继续执行是完全能够保证程序的执行顺序的。假设程序永远只有一个线程,这个疑问没有任何问题,也就是说并不需要程序计数器。但实际上程序是通过多个线程协同合作执行的。
  
首先我们要搞清楚JVM的多线程实现方式。JVM的多线程是通过CPU时间片轮转(即线程轮流切换并分配处理器执行时间)算法来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,在JVM中,通过程序计数器来记录某个线程的字节码执行位置。因此,程序计数器是具备线程隔离的特性,也就是说,每个线程工作时都有属于自己的独立计数器。

2.2)程序计数器的特点

  • 1.线程隔离性,每个线程工作时都有属于自己的独立计数器。
  • 2.执行java方法时,程序计数器是有值的,且记录的是正在执行的字节码指令的地址(参考上一小节的描述)。
  • 3.执行native本地方法时,程序计数器的值为空(Undefined)。因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。如下图image
  • 4.程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。
  • 5.程序计数器,是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。

3、Java虚拟机栈

3.1、什么是虚拟机栈

  • 虚拟机栈是用于描述java方法执行的内存模型。
  • 方法调用时,创建栈帧,并压入虚拟机栈;方法执行完毕,栈帧出栈并被销毁,如下图所示:image

3.2、栈帧

  • 每个java方法在执行时,会创建一个“栈帧(stack frame)”,栈帧的结构分为“局部变量表、操作数栈、动态链接、方法出口”几个部分(具体的作用会在字节码执行引擎章节中讲到,这里只需要了解栈帧是一个方法执行时所需要数据的结构)。
  • 我们常说的“堆内存、栈内存”中的“栈内存”指的便是虚拟机栈,确切地说,指的是虚拟机栈的栈帧中的局部变量表,因为这里存放了一个方法的所有局部变量。

3.2.1、局部变量表

  • 用于存储方法参数和方法内部定义的局部变量,在Java程序编译成Class文件的时候,就在Code属性中的max_locals数据项中确定了该方法所要分配的局部变量表的最大容量。
  • 局部变量区被组织为以一个slot(变量槽)为单位、从0开始计数的数组。虚拟机规范中并没有明确规定slot的大小,只是说明每个slot都应该能存放一个boolean、byte、char、short、int、float、reference、或returnAddress类型的数据。
  • 类型为short、byte和char的值在存入数组前要被转换成int值,而long和 double在数组中占据连续的两项,在访问局部变量中的long或double时,只需取出连续两项的第一项的索引值即可,如某个long值在局部变量区中占据的索引是3、4项,取值时,指令只需取索引为3的long值即可。

举例说明

public static int runClassMethod(int i,long l,float f,double d,Object o,byte b {
    return 0;
}

public int runInstanceMethod(char c,double d,short s,boolean b) {
    return 0;
}

image
首先,可以看到虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的。并且runInstanceMethod(实例方法)的局部变量区第一项是个reference(引用),它指定的就是对象本身的引用,也就是我们常用的this,但是在RunClassMethod方法中,没这个引用,那是因为runClassMethod是个静态方法。

关于reference类型,代表的是一个对象实例的引用,这个引用应当可以做到两点:

1.从此引用中直接或间接地查找到对象在Java堆中的数据存放的起始地址索引。

2.从此引用中直接或间接的查找到对象所属类型在方法区中存储的类型信息。

  • slot的复用:为了节省栈帧空间,局部变量表中的 Slot 是可以重用的。当离开了某些变量的作用域之后,这些变量对应的 Slot 就可以交给其他变量使用。这种机制有时候会影响垃圾回收行为。
    举例说明

代码一

public static void main(String[] args){
        {
            byte[] placeholder = new byte[64*1024*1024];
        }
        System.gc();
    }

运行结果:

[GC 602K->378K(15872K), 0.0603803 secs]
[Full GC 378K->378K(15872K), 0.0323107 secs]
[Full GC 66093K->65914K(81476K), 0.0074124 secs]

代码二

public static void main(String[] args){
        {
            byte[] placeholder = new byte[64*1024*1024];
        }
        int a = 0;
        System.gc();
    }

运行结果

[GC 602K->378K(15872K), 0.0018270 secs]
[Full GC 378K->378K(15872K), 0.0057871 secs]
[Full GC 66093K->378K(81476K), 0.0054067 secs]

分析:通过结果可以知道,代码一和代码二内的 placeholder 变量在 System.gc() 执行后理应被回收了,可是结果却是只有代码二被回收了,这是为什么呢?

这是因为代码一中 placeholder 虽然离开了作用域,但之后没有任何局部变量对其进行读写,也就是说其占用的 Slot 没有被复用,也就是说 placeholder 占用的内存仍然有引用指向它,因而它没有被回收。而代码二中的变量a由于复用了 placeholder 的 Slot ,导致 placeholder 引用被删除,因此占用的内存空间被回收。

《Practical Java》一书中把”不使用的对象应手动赋值为 null “作为一条推荐的编码规则,这并不是一个完全没有意义的操作。但是不应该对 赋 null 值有过多的依赖,主要有两点原因:

  • 从编码的角度来讲,用恰当的变量作用域来控制变量的回收才是最优雅的解决方法。
  • 从执行角度将,使用赋值 null 的操作优化内存回收是建立在对字节码执行引擎概念模型基础上的,但是概念模型与实际执行模型可能完全不同。在使用解释器执行时,通常离概念模型还比较接近,但是一旦经过JIT 编译为本地代码才是虚拟机执行代码的主要方式,赋 null 值在JIT编译优化之后会被完全消除,这时候赋 null 值是完全没有意义的。(其实,上面代码一在 JIT 编译为本地代码之后,gc() 之后内存也会被自动回收)

3.2.2、操作数栈

  • 与局部变量表一样,均以字长为单位的数组。不过局部变量表用的是索引,操作数栈是弹栈/压栈来访问。操作数栈可理解为java虚拟机栈中的一个用于计算的临时数据存储区。
  • 存储的数据与局部变量表一致含int、long、float、double、reference、returnType,操作数栈中byte、short、char压栈前(bipush)会被转为int。
  • 数据运算的地方,大多数指令都在操作数栈弹栈运算,然后结果压栈。
  • java虚拟机栈是方法调用和执行的空间,每个方法会封装成一个栈帧压入占中。其中里面的操作数栈用于进行运算,当前线程只有当前执行的方法才会在操作数栈中调用指令(可见java虚拟机栈的指令主要取于操作数栈)。
  • int类型在-1~5、-128~127、-32768~32767、-2147483648~2147483647范围分别对应的指令是iconst、bipush、sipush、ldc(这个就直接存在常量池了)

举例说明:

public class Hello {
    public static void main(String[] args) {
        Hello hello = new Hello();
        int testVal = hello.test();
    }
 
    public int test() {
        //int类型
        int a = 5 + 10;      //验证直接相加在编译阶段已合并完结果
        int b = a + 3;        //探究变量与常量的相加过程
        b = b + 6;             //验证int不在-1~5,在-128~127范围的指令是bipush
        b = b + 128;         //验证int不在-128~127,在-32768~32767范围的指令是sipush
        b = b + 32768;     //验证int不在-32768~32767,在-2147483648~2147483647范围的指令是ldc(ldc:从常量池取并压栈,所以这个范围的int是存在常量池)
        //short                  //验证byte、short、char在操作数栈压栈前均会转为int
        short a_s = 5 + 10;
        short b_s = (short)(a_s + 3);
        //float类型            //以下验证float、double、String均是从常量池中取出(均使用了ldc、ldc_w、ldc2_w其中一个)
        float a_f = 5.00F + 10.00F;
        float b_f = a_f + 3.00F;
        //double类型
        double a_d = 5.00D + 10.00D;
        double b_d = a_d + 3.00D;
        //String类型
        String a_str = "a" + "b";
        String b_str = a_str + "c";
        return b;
    }
}

javac Hello.java编译,然后javap -verbose Hello.class反编译分析test()方法如下:

Code:
      stack=4, locals=13, args_size=1
         0: bipush        15 //1 15压入操作数的栈顶(编译过程中5+10合并成15,并且由于15在-128-127范围,即用bipush)  压栈
         2: istore_1           //2  从栈顶弹出并压入局部变量表访问索引为1的Slot                                                                    弹栈入局部变量表
         3: iload_1            //3  将局部变量表中访问索引为1的Slot重新压入栈顶                                                                    局部变量表入栈
         4: iconst_3          //4  数值3压入操作数的栈顶(范围-1~5,即用指令iconst)                                                              压栈
         5: iadd                //5  将栈顶的前两个弹出并进行加法运算后将结果重新压入栈顶                                                     前两弹栈相加
         6: istore_2          //6   从栈顶弹出并压入局部变量表访问索引为2的Slot                                                                    弹栈入局部变量表
         7: iload_2            //7  将局部变量表中访问索引为2的Slot重新压入栈顶                                                                    局部变量表入栈
         8: bipush        6  //8  6压入操作数的栈顶(在-128-127范围,用bipush指令) 
        10: iadd
        11: istore_2
        12: iload_2
        13: sipush     128//9  128压入操作数的栈顶(在-32768~32767范围,用sipush指令) 
        16: iadd
        17: istore_2
        18: iload_2
        19: ldc           #5  // int 32768  //10  128压入操作数的栈顶(在-2147483648~2147483647范围,用ldc指令) 
        21: iadd
        22: istore_2
        23: bipush        15 //11  验证了short、byte、char压栈前都会转为int
        25: istore_3
        26: iload_3
        27: iconst_3
        28: iadd
        29: i2s
        30: istore        4
        32: ldc           #6   // float 15.0f //12  以下验证float、double、String均是从常量池中取出(均使用了ldc、ldc_w、ldc2_w其中一个)
        34: fstore        5
        36: fload         5
        38: ldc           #7                  // float 3.0f
        40: fadd
        41: fstore        6
        43: ldc2_w        #8                  // double 15.0d
        46: dstore        7
        48: dload         7
        50: ldc2_w        #10                 // double 3.0d
        53: dadd
        54: dstore        9
        56: ldc           #12                 // String ab
        58: astore        11
        60: new           #13                 // class java/lang/StringBuilder
        63: dup
        64: invokespecial #14                 // Method java/lang/StringBuilder."<init>":()V
        67: aload         11
        69: invokevirtual #15                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        72: ldc           #16                 // String c
        74: invokevirtual #15                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        77: invokevirtual #17                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        80: astore        12
        82: iload_2
        83: ireturn                        //13  返回结果

本地方法栈

与java虚拟机栈类似,只是描述的是native方法

java堆

java虚拟机管理的最大的一块内存,被所有线程共享,几乎所有的对象实例都在这里分配内存。

方法区

用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

直接内存

NIO支持引入,通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,避免了在java堆和native堆中来回复制数据,可以显著提供性能。
可以通过-XX:MaxDirectMomorySize 指定直接内存大小,如果不指定,则默认与-Xmx指定值一样。

对象

对象的创建

1、遇到new指令,首先将去检查常量池中类的符号引用,并检测类是否已被加载、解析和初始化过。
2、分配内存(对象的内存分配在类加载完后便可以完全确定)。分配内存有两种方案:一种是对分配内存空间的动作进行同步处理-实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另外一种是把内存分配的动作按照线程划分不同的空间中进行,即每个线程分配一小块内存,称为TLAB,只有在TLAB用完并分配新的TLAB时,才需要同步锁定。通过-XX:+/-UseTLAB参数设定
3、将将要分配的内存都初始化为0值
4、设置对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息
5、执行init方法进行初始化

对象的内存布局

相关文章
|
2月前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
5月前
|
存储 算法 安全
Java面试题:Java内存模型及相关知识点深度解析,Java虚拟机的内存结构及各部分作用,详解Java的垃圾回收机制,谈谈你对Java内存溢出(OutOfMemoryError)的理解?
Java面试题:Java内存模型及相关知识点深度解析,Java虚拟机的内存结构及各部分作用,详解Java的垃圾回收机制,谈谈你对Java内存溢出(OutOfMemoryError)的理解?
74 0
|
3月前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
122 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
4月前
|
Java
Java常见JVM虚拟机指令(47个)
Java常见JVM虚拟机指令(47个)
71 3
Java常见JVM虚拟机指令(47个)
|
5月前
|
监控 Oracle Java
(一)JVM成神路之初识虚拟机 - 探寻Java虚拟机的前世今生之秘
JVM(Java Virtual Machine)Java虚拟机的概念大家都不陌生,Java之所以可以做到“一次编译,到处运行”的跨平台性,其根本原因就在于JVM。JVM是建立在操作系统(OS)之上的,Java虚拟机屏蔽了开发人员与操作系统的直接接触,我们在通过Java编写程序时,只需要负责编写Java代码即可,关于具体的执行则会由JVM加载字节码后翻译成机械指令交给OS执行。
|
4月前
|
Java 数据安全/隐私保护 Windows
【Azure Developer】使用Java代码启动Azure VM(虚拟机)
【Azure Developer】使用Java代码启动Azure VM(虚拟机)
|
4月前
|
存储 Java API
【Azure Developer】通过Azure提供的Azue Java JDK 查询虚拟机的CPU使用率和内存使用率
【Azure Developer】通过Azure提供的Azue Java JDK 查询虚拟机的CPU使用率和内存使用率
|
5月前
|
存储 Ubuntu Java
【Linux】已解决:Ubuntu虚拟机安装Java/JDK
【Linux】已解决:Ubuntu虚拟机安装Java/JDK
178 1
|
5月前
|
监控 算法 Java
深入理解Java虚拟机:内存管理与性能优化
在Java的世界里,虚拟机(JVM)是幕后的守护者,它默默地支撑着每一个字节码的运行。本文将揭开JVM的神秘面纱,探讨其内存管理机制及如何通过调优提升应用性能。从堆内存的分配到垃圾回收的策略,再到实践中的性能监控与调优技巧,我们将一同走进JVM的内部世界,学习如何让我们的Java程序跑得更快、更稳。
|
5月前
|
Java
Java演进问题之单个虚拟机的最大线程数量一般会设置到200至400条如何解决
Java演进问题之单个虚拟机的最大线程数量一般会设置到200至400条如何解决