Java虚拟机入门,内存结构详解

提示:该文章仅供初学者个人学习参考

目录

前言

一、什么是JVM?

二、JRE和JDK

三、JVM的位置

四、JVM的内存结构

1、方法区 (Method Area)

2、堆 (Heap)

3、虚拟机栈 (JVM Stack)

4、本地方法栈 (Native Method Stack)

5、程序计数器 (Program Counter Register)

总结


前言

        有很多学习Java的人,可能到现在才听过JVM这个东西,也好奇的随随便便浏览了一下,发现这里面并没有编程的东西,几乎都是文字和图文的描述,感觉就可以不用学习JVM了。但是不管怎样,学习JVM可以帮我们更好的了解Java到底是如何实现编译运行的过程,仿佛接触到了一个不一样且神奇的领域,并不是简简单单的写好了代码点击main运行就没事了。深入了解后,你才会发现Java的美妙之处。

        同时JVM也是Java程序员走向社会面试的一个必考的一个问题(别杠,杠就是你对!)。所以呢,JVM是作为Java开发人员必学的一个知识。


一、什么是JVM?

        JVM (Java Virtual Machine) 又称 Java 虚拟机,是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java 虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java 虚拟机屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 Java 虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

二、JRE和JDK

        JRE(Java Runtime Enviroment) 是 Java 的运行环境。面向 Java 程序的使用者,而不是开发者。如果你仅下载并安装了 JRE,那么你的系统只能运行 Java 程序。JRE 是运行 Java 程序所必须环境的集合,包含 JVM 标准实现及 Java 核心类库。它包括 Java 虚拟机、Java 平台核心类和支持文件。它不包含开发工具(编译器、调试器等)。

        JDK(Java Development Kit) 又称 J2SDK(Java2 Software Development Kit),是 Java 开发工具包,它提供了 Java 的开发环境(提供了编译器 javac 等工具,用于将 java 文件编译为 class 文件)和运行环境(提供了 JVM 和 Runtime 辅助包,用于解析 class 文件使其得到运行)。如果你下载并安装了 JDK,那么你不仅可以开发 Java 程序,也同时拥有了运行 Java 程序的平台。JDK 是整个 Java 的核心,包括了 Java 运行环境(JRE),一堆 Java 工具 tools.jar 和 Java 标准类库 (rt.jar)。

三、JVM的位置

        JVM 上承开发语言,下接操作系统,它的中间接口就是字节码。        正是因为如此,Java 在任意平台上编写相同的代码进行编译后,通过下载相对应的系统运行 JRE 包能够在不同的系统中运行。从而实现了“一次编译,到处运行”。        “一次编译,到处运行”,说的就是 Java 语言跨平台的特性。Java 的跨平台特性与 Java 虚拟机的存在密不可分。

四、JVM的内存结构

        JVM 的内存结构主要分为五大块,从线程的角度来讲,分为线程私有和线程共享两大块。其中属于线程共享的有方法区 (Method Area)堆 (Heap),而属于线程私有的有虚拟机栈 (JVM Stack)本地方法栈 (Native Method Stack)程序计数器 (Program Counter Register),其中虚拟机栈又称为方法栈。

1、方法区 (Method Area)

        方法区 (Method Area),线程共享。主要负责存储类的信息静态变量常量,即编译器编译后的代码。简单来说,所有定义方法的信息都保存在该区域。

  • 类的信息,主要包含该类的完整有效名称(全名=包名.类名)、直接父类的完整有效名称(java.lang.Object 除外)、类的访问修饰符、直接接口的有序列表。
  • 静态变量,又称为类变量,主要包含 static 修饰的成员变量。静态变量之所以被称为类变量,是因为静态变量和类关联在一起,随着类的加载而存在于方法区中,而不是堆中。八大基本数据类型的静态变量会在方法区开辟空间,并将对应的值存储在方法区中。
  • 常量,主要包含 static final 修饰的成员变量。

        运行时常量池在方法区中(注意:运行时常量池不等价于常量池!),但是实例变量存储在堆内存中与方法区无关。在这里给大家粗略解释下常量池和运行时常量池的差别。

  • 常量池,可以比喻为 Class 文件里的资源仓库,它是 Class 文件结构中与其他项目关联最多的数据,通常也是占用 Class 文件空间最大的数据项目之一。另外,它还是在 Class 文件中第一个出现的表类型数据项目。常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近 Java 语言层面的常量概念,如文本字符串、被声明为 final 的常量值等。而符号引用则属于编译原理方面的概念,主要包含下面几类常量:
    • 被模块导出或者开放的包(Package)
    • 类和接口的全限定名(Fully Qualified Name)
    • 字段的名称和描述符(Descriptor)
    • 方法的名称和描述符
    • 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
    • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
  • 运行时常量池,方法区的一部分。当类加载到内存中后,JVM 就会将 class常量池中的内容存放到运行时常量池中。由此可见,运行时常量池是每个类中都会含有的一个。运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段的引用。此时不再是常量池中的符号地址,而是真实地址。
  • 运行时常量池相对于常量池,最重要的特征之一是:具备动态性

2、堆 (Heap)

        堆 (Heap),线程共享。所有的对象实例以及数组都要在堆上进行分配,同时也是垃圾回收器主要的管理对象(内存调优指定也就是堆内存调优)。从堆的结构上来说,可分为年轻代老年代(注意:永久代在 Java 8 以后包含 Java 8 就取消了永久代),其堆空间默认比例为 1 : 2。年轻代可分为 Eden SpaceFrom Survivor (S0)To Survivor (S1),其新生代中空间默认比例为 8 : 1 : 1。

  • 年轻代 (Young Generation),又称为新生代。一般情况下新创建的对象都会被分配到 Eden 区(对于特别大的对象,则直接分配到老年代),这些对象经过第一次 Minor GC(是指发生在整个新生代的GC,较为频繁,回收速度快)后,如果仍然存活,那么将会被移动到 Survivor 区。对象在 Survivor 区每经历过一次 GC 并且存活,则年龄会增加 1 岁,当达到一定年龄时(默认15岁),则会移动到老年代中。
  • 老年代 (Old Generation)。当空间占用值达到了某个值之后就会触发 Major GC(又称 Full GC,是指发生在老年代的GC,出现了 Major GC 通常会伴随至少一次 Minor GC ,Major GC 的速度通常会比 Minor GC 慢 10 倍以上),一般使用编辑整理的执行算法。
  • 永久代 (Permanent Generation),在 Java 8 以后 包含 Java 8 就被取消。在 Java7 及以前的版本的 Hotspot 中方法区位于永久代中(仅因为 Hotspot 将 GC 分代扩展至方法区)。同时,永久代和堆在逻辑上是相互隔离的,但是他们使用的物理内存又是连续的,当时永久代的垃圾收集和老年代是捆绑在一起进行的。在 Java 7 中永久代中存储的部分数据就已经开始转移到 Heap 或 Native Memory 中,例如字符串常量池、类的静态变量转移到了堆中,符号引用转移到了本地内存中。
  • 元空间 (Metaspace),在取消了永久代后应用而生。在 Java 8 中,方法区存在于元空间中,此时元空间不再与堆连续,而是存在于本地内存中。
  • 幸存区(Survivor),又分为 From Survivor (s0) 和 To Survivor (S1)。在 GC 最开始的时候,对象只存在于 Eden 区 和 From Survivor 区,此时 To Survivor 区是空的。进行 GC 后,Eden 区所有存活的对象都会被复制到 To Survivor ,From Survivor 幸存下来的对象同样会复制到 To Survivor ,然后 Eden 和 From Survivor 就会被清空。这个时候 To Survivor 和 From Survivor 就会互换身份,从而至始至终保证名为 To Survivor 的区是空的。新生代的对象也只会从 To Survivor 进入到老年代。

3、虚拟机栈 (JVM Stack)

        虚拟机栈 (JVM Stack),又称方法栈,线程私有。主要存储局部变量表操作栈动态链接返回地址对象指针。从结构上来讲,虚拟机栈是一个先入后出的栈。栈帧是保存在虚拟机栈中,栈帧是用来存储数据和存储部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、方法返回值和异常分派 (Dispatch Exception)。线程运行过程中,只有一个栈帧处于活跃状态,称为“当前活跃栈帧”,当前活跃栈帧始终是虚拟机栈的栈顶元素。

  • 局部变量表,是一组局部变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在 Java 文件编译为 Class 文件时,就在方法表的 Code 属性的 max_locals 数据项中确定了该方法需要分配的最大局部变量表的容量。
  • 操作数栈,又称操作栈,是一个先入后出的栈。VM底层字节码指令集是基于栈类型的,所有的操作码都是对操作数栈上的数据进行操作,对于每一个方法的调用,JVM会建立一个操作数栈,以供计算使用。当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。另外,在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的,但是大多数虚拟机的实现里都会作一些优化处理,令两个栈帧出现一部分重叠。让下栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用返回时就可以共用一部分数据,而无须进行额外的参数复制传递了。
  • 动态链接,是指每个栈帧中都包含一个指向运行时常量池中该栈帧所拥有属性方法的引用。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接
  • 返回地址。当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法方式称为正常完成出口(Normal Method Invocation Completion)。另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的调用都产生任何返回值的。     无论采用何种方式退出,在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。 方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。

4、本地方法栈 (Native Method Stack)

        本地方法栈 (Native Method Stack),线程私有。为虚拟机使用到的Native方法服务,例如在 Java 使用 C 或者 C++ 编写的接口服务时,则代码在此区运行。凡是带 native 关键字的,说明 java 的作用范围达不到,需要调用底层 C 语言的库;会进入本地方法栈调用本地库接口,从而使用本地方法库;JNI 的作用扩展了 java 的使用,融合不同的编程语言为 java 所用;native 在内存区中专门开辟了一块标记区域:Native Method Stack,用来登记 native 方法;最终执行的时候,加载本地方法库中的方法(通过JNI);例如Java程序驱动打印机,管理系统,在企业级应用中较为少见。

        当某个线程调用一个本地方法时,它就进入了一个全新的并且不受 JVM 限制的世界,同时它和 JVM 具有相同的权限。本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区,甚至可以直接使用本地处理器中的寄存器,也可以直接从本地内的堆中分配任意数量的内存。

5、程序计数器 (Program Counter Register)

        程序计数器 (Program Counter Register),线程私有。每个线程都有一个程序计数器,是线程私有的,就是一个指针指向方法区中的方法字节码(用来存储指向一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令时加一,是一个非常小的内存空间,几乎可以忽略不计。


总结

        初学 JVM 的时候总是会对某些概念模糊不清,因此在后续阶段决定编写一篇文章同时作为自己日后复习的笔记。总结了网上现有的文章弥补了自身对于 JVM 理解存在的问题,同时也添加了自身的理解。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值