java虚拟机有哪几部分组成,Java虚拟机基本结构

Java虚拟机是JVM类语言的根基,其中动态内存管理和垃圾收集技术是JVM中最重要的特性。本节主要讲述其中的内存管理相关概念。

一 Java虚拟机的基本结构

fa894feca39a

Java虚拟机结构.jpg

如图所示为Java虚拟机的基本结构,每个模块介绍如下:

类加载子系统

类加载子系统负责从文件或网络中加载Class字节码信息,然后存放于方法区。

方法区

方法区是各个线程共享的内存区域,用于存储虚拟机加载的类变量、常量、静态变量以及即时编译后的代码等数据。

Java堆

在虚拟机启动的时候建立,是Java程序最主要的内存工作区域,几乎所有的对象实例和数组都在Java堆上分配。和方法区一样,是各个线程共享的内存区域。可通过-Xmx和Xms虚拟机参数控制Java堆大小。

垃圾回收系统

垃圾回收是Java虚拟机的重要组成部分,其中的垃圾回收器可以对Java堆、方法区和直接内存进行回收。同时Java堆是回收器的工作重点。

直接内存

在Java的NIO库中,允许Java程序使用直接内存,它是Java堆外直接向系统申请的内存区域。通常情况下该区域的内存访问速度优于Java堆。

Java栈

Java栈是线程私有的,它的生命周期和线程相同。它在线程创建的时候被创建。Java栈中保存帧信息,每个方法创建的时候都会创建一个栈帧,用于存储局部变量、方法参数、操作数栈、方法出口灯信息,和方法的调用返回密切相关。

本地方法栈

和Java栈类似,但其中最大的不同是Java栈用于方法调用,而本地方法栈用于本地方法调用。

PC寄存器

该区域也是每个线层私有的空间。Java虚拟会为每个Java线程创建PC寄存器。当当前执行的方法不是本地方法时,PC寄存器就会指向当前正在被执行的指令。若当前执行的方式是本地方法,则PC寄存器的值为undefined。

执行引擎

执行引擎是虚拟机最核心的组件之一,它负责执行虚拟机的字节码。

二 Java堆

Java堆是和Java应用程序关系最为密切的内存空间。Java堆内存通过垃圾回收机制,垃圾对象会被自动清理,而不需要显示的释放。Java堆分为新生代和老年代。其中新生代存放新生对象或年龄不大的对象,而老年代则存放老年对象。新生代和老年代结构如下图所示:

fa894feca39a

Java堆结构.png

在大多数情况下,对象首先在Eden区分配,在一次新生代回收后,若对象还存活着则进入S0或S1,在这之后,每经一次新生代回收,若对象还存活着则它的年龄会加1,达到一定年龄后该对象就被认为是老年对象,从而进入老年代。当然这里只是其中一种方式进入老年代,后续文章会有详细叙述。

三 Java栈

Java栈是一块线程私有内存空间。Java栈用于传递每次函数调用的数据。它是一块后进先出的数据结构,在其中保存的主要内容是栈帧。每一次函数调用都会有对应的栈帧压如Java栈,同时函数结束时栈帧被弹出。

fa894feca39a

Java栈帧.jpg

上图可用下面的代码表示:

public void function1() {

public void function2();

}

public void function2() {

public void function3();

}

public void function3() {

....

}

...

在一个栈帧中至少包含局部变量表、操作数栈和帧数据区几个部分。

但是Java栈空间也不能无限使用下去,它受-Xss参数限制,该参数也决定了函数调用的最大深度。

示例:

public class TestStackDeep {

private static int count = 0;

public static void recursion() {

count++;

recursion();

}

public static void main(String[] args) {

try {

recursion();

} catch (Throwable e) {

System.out.println("deep of calling = " + count);

e.printStackTrace();

}

}

}

上面的代码计算最大栈深度,设置虚拟机参数-Xss256K,其结果为:

deep of calling = 2374

当设置-Xss512K时,结果为:

deep of calling = 9245

栈溢出则会抛出java.lang.StackOverflowError异常。

1)局部变量表

局部变量表用于保存函数的参数以及局部变量,它同样也随函数的调用而生灭,函数变量表可通过jclasslib工具查看,在Idea中,jclasslib可作为插件方式安装。

示例代码:

public class TestStack {

public void test1() {

int m, n, i, j, k;

System.out.println("hello world");

}

public void test2(int param1, int param2) {

long m, n, i, j, k;

System.out.println("hello world");

}

public static void main(String[] args) {

TestStack testStack = new TestStack();

}

}

通过查看jclasslib可看到以下内容:

fa894feca39a

testStack

在jclasslib中,可看到当前类中的静态池,接口字段以及方法等信息。查看方法的Code部分可看到局部变量表统计信息:

fa894feca39a

局部变量表信息

从上图看出,test2()最大局部变量表占用大小为13字,因为test2()参数为两个int,加上this字段以及5个long变量正好是13字(int占用一个字,this占用一个字,long占用两个字)。查看局部变量表信息可通过LocalVariableTable:

fa894feca39a

局部变量表

上图中分别对应了局部变量的作用域范围,所在槽位(index),变量名(name)以及数据类型(Descriptor)

栈帧中局部变量表的槽位是可以复用的,如果一个局部变量过了其作用域,那么在其后申明的新局部变量就有可能复用过期局部变量的槽位,从而达到节省资源的目的。

2)操作数栈

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

3)帧数据区

4)栈上分配

栈上分配是Java虚拟机提供的一项优化技术,基本思想是对于线程私有对象(即不会被其它线程访问到的对象实例),可以将它们分配在栈上,而不必从堆中分配,这样的好处是该对象在函数调用完毕后可以自行销毁而不必接入垃圾回收器,从而提高系统的整体性能。栈上分配的一个基础技术是逃逸分析,逃逸分析的目的是判断对象作用域是否逃逸出函数体。

在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法——分析在程序的哪些地方可以访问到指针。它涉及到指针分析和形状分析。

当一个变量(或对象)在子程序中被分配时,一个指向变量的指针可能逃逸到其它执行线程中,或是返回到调用者子程序。如果使用尾递归优化(通常在函数编程语言中是需要的),对象也可以看作逃逸到被调用的子程序中。如果一种语言支持第一类型的延续性在Scheme和Standard ML of New Jersey中同样如此),部分调用栈也可能发生逃逸。

编译器可以使用逃逸分析的结果作为优化的基础:

将堆分配转化为栈分配。如果某个对象在子程序中被分配,并且指向该对象的指针永远不会逃逸,该对象就可以在分配在栈上,而不是在堆上。在有垃圾收集的语言中,这种优化可以降低垃圾收集器运行的频率。

同步消除。如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。

分离对象或标量替换。如果某个对象的访问方式不要求该对象是一个连续的内存结构,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

例如下面的代码便是一个逃逸对象:

private static Bean bean;

public static void alloc() {

bean = new Bean();

bean.setParam(23);

....

}

其中bean字段可能被其它线程访问到,故属于逃逸对象。

下面的代码显示了非逃逸对象:

public static void alloc() {

bean = new Bean();

bean.setParam(23);

....

}

启用逃逸分析需要设置-server执行程序,JVM参数如下:

-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations

-Xmx256m -Mms256m 分别制定了堆最大空间和堆最小空间为10M;

-XX:+DoEscapeAnalysis 启用逃逸分析;

-XX:+PrintGC 打印GC信息;

-XX:-UseTLAB 关闭TLAB;

-XX:+EliminateAllocations 开启标量替换,允许将对象打散分配到栈上。

以上参数都是默认启用的。

示例:

public class OnStackTest {

public static class User {

public int id = 0;

public String name = "";

}

public static void alloc() {

User user = new User();

user.id = 10;

user.name = "vincent";

}

public static void main(String[] args) throws InterruptedException {

long start = System.currentTimeMillis();

for (int i = 0; i < 1000000000; i++) {

alloc();

}

System.out.println(System.currentTimeMillis() - start);

}

}

上面的代码进行了1000000000次调用,但是产生的GC日志很少:

[GC (Allocation Failure) 2047K->536K(9728K), 0.0008531 secs]

7

但如果关闭了逃逸分析,则会产生大量的GC日志。例如将-XX:+DoEscapeAnalysis 替换成-XX:-DoEscapeAnalysis

四 方法区

方法区也是所有线程共享的内存区域,用于保存系统类信息,例如字段,方法常量池等。该区域大小决定了系统可以保存多少类。但若定义了太多类同样也会导致方法区溢出。

在JDK6和JDK7中,方法区可链接为永久区,通过参数-XX:PermSize和-XX:MaxPermSize指定。但在JDK8中,永久区已经被移除,替代为元数据区,可使用-XX:MaxMetaspaceSize参数指定,若不指定该参数,默认情况下虚拟机会耗尽所有可用系统内存,在VisualVM中可观察永久区:

fa894feca39a

永久区

元数据区溢出虚拟机会抛出java.lang.OutOfMemoryError: Metaspace异常。

参考

《实战Java虚拟机: JVM故障与性能优化》

《深入理解Java虚拟机:JVM高级特性与最佳实践》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Java虚拟机结构主要由类加载器、虚拟机栈、本地方法栈、堆、方法区组成。类加载器的作用是加载Java类;虚拟机栈是用于存储局部变量、操作数栈等信息;本地方法栈是用于存储本地方法的栈;堆是Java虚拟机使用的最大内存空间,用于存储对象实例;而方法区则存放类信息、常量、静态变量等数据。 ### 回答2: Java虚拟机Java Virtual Machine,JVM)是Java程序的运行环境,它在物理机器上创建一个虚拟的计算机平台,在这个平台上执行Java字节码。JVM的结构包含以下几个关键组成部分: 1. 类加载器(ClassLoader):负责加载字节码文件(.class文件)并将其转换为Java类的内存表示。类加载器可以根据需要动态加载和卸载类。 2. 执行引擎(Execution Engine):负责执行Java字节码。执行引擎将字节码解释为机器指令序列或将其编译为本地代码执行,以达到提高性能的目的。 3. 运行时数据区(Runtime Data Areas):包括多个不同类型的数据区域,用于存储程序运行所需的数据。 - 方法区(Method Area):存储被加载的类信息、常量、静态变量、即时编译器编译后的代码等。 - 堆(Heap):存储Java对象实例,堆是在JVM启动时创建的,用于存放动态分配的对象。 - 栈(Stack):存储方法执行时的局部变量、操作数栈、调用信息等。每个线程都有自己的栈,用于方法的调用和返回。 - 程序计数器(Program Counter Register):记录当前线程执行的指令地址或指令索引。 4. 本地方法接口(Native Method Interface,JNI):允许Java应用程序调用使用其他语言编写的本地库中的方法。 5. 安全性引擎(Security Engine):提供安全管理和访问控制功能,确保Java程序在执行时具有必要的权限。 这些组成部分共同构成了Java虚拟机结构Java程序在JVM上运行时,通过类加载器将程序转化为内存表示,在运行时数据区执行代码,执行引擎解释和执行字节码指令,最终完成Java程序的运行。JVM的结构和功能的设计有效地将Java程序的开发与底层的操作系统解耦,提供了跨平台的能力。 ### 回答3: Java虚拟机(JVM)是Java程序的运行环境,它是一个软件程序,能够解释和执行Java字节码。JVM的结构可以分为以下几个部分: 1. 类加载器(Class Loader):类加载器负责加载Java类文件,将其加载到内存中,并生成对应的类对象。JVM中有三个主要的类加载器:启动类加载器(Bootstrap Class Loader)、扩展类加载器(Extension Class Loader)和应用程序类加载器(Application Class Loader)。 2. 运行时数据区(Runtime Data Area):运行时数据区是JVM用于存储程序运行时所需数据的区域。主要包括方法区、堆、虚拟机栈、本地方法栈和程序计数器。 - 方法区(Method Area):用于存储类的结构信息、静态变量、常量等数据。 - 堆(Heap):用于存储Java对象。所有的对象实例都分配在堆中,并可以通过引用在方法区或栈中访问。 - 虚拟机栈(VM Stack):每个线程在运行时都会创建一个对应的虚拟机栈,用于存储局部变量、方法参数、返回值等。 - 本地方法栈(Native Method Stack):与虚拟机栈类似,但用于执行本地方法。 - 程序计数器(Program Counter):用于记录当前线程执行的字节码指令地址。 3. 执行引擎(Execution Engine):执行引擎负责解释和执行字节码指令,将其转换为对应的机器指令。常见的执行引擎有两种:解释器(Interpreter)和即时编译器(Just-In-Time Compiler,JIT)。 4. 本地方法接口(Native Interface):本地方法接口提供了Java代码调用本地方法的能力。本地方法接口定义了一组规范,使得Java代码可以与C、C++等底层语言进行交互。 5. 垃圾回收系统(Garbage Collection System):垃圾回收系统负责自动管理堆内存的分配和释放,回收不再使用的对象。垃圾回收系统通过标记-清除、复制算法等方式来回收内存。 通过以上的结构Java虚拟机能够提供一种平台无关的执行环境,使得Java程序在不同的操作系统和硬件平台上都能够运行。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值