您的位置  > 互联网

JavaClass编译后的代码就能够运行起来,到处运行

中小型工厂内部晋升渠道很多,有正规的,也有外包的。 欢迎大家来找我内部宣传JVM的基本概念。

JVM的中文名称是Java虚拟机。 它是通过软件技术模拟计算机操作的虚拟计算机。

JVM还扮演着翻译者的角色。 我们编写的Java程序不能直接被操作系统识别。 这时候JVM的作用就体现出来了。 它负责将我们的程序翻译到系统中进行“监听”。 ,告诉它我们的程序需要执行哪些操作。

我们都知道Java代码需要经过编译器生成.Class文件,JVM才能识别并运行它。 JVM为每个操作系统都开发了其对应的解释器,所以只要其操作系统有对应版本的JVM,那么这个Java编译出来的代码就可以运行。 这就是为什么Java可以一次编译,到处运行。

目前市场上使用最广泛的虚拟机是Sun/或中国的默认虚拟机。 这是官方开发并推广的虚拟机。 我们应该在网上或者我们买的书上解释一下这个虚拟机。

市场上仍然有很多优秀的Java虚拟机。 它们都遵循Java虚拟机规范,但在具体实现上又各有特点。 它们最重要的卖点是在某个领域突出的性能优势。 例如IBM的J9、Azul的Azul VM等。

本文仅使用虚拟机来详细解释其基本原理。

JVM 生命周期

JVM在Java程序开始运行时运行,在程序结束时停止。

Java程序将启动一个JVM进程。 如果一台机器上运行3个Java程序,那么就会有3个正在运行的JVM进程。

JVM中有两种线程:守护线程和普通线程

守护线程是JVM本身使用的线程。 例如,垃圾收集(GC)就是一个守护线程。

普通线程一般是Java程序的线程。 只要JVM中有普通线程在执行,JVM就不会停止。

结束生命周期

在以下情况下,Java虚拟机将结束其生命周期

1.执行.exit()方法

2. 程序正常结束。

3. 程序在执行过程中遇到异常或错误,终止进程。

4. Java虚拟机进程因操作系统错误而终止。

JDK和JRE的区别

JDK是面向开发人员的SDK。 它提供了Java开发环境和运行环境。 JDK 包括 JRE。

JRE 是 Java 运行时环境,面向所有 Java 程序用户,包括开发人员。

JVM 包含在 JRE 中。

JVM架构

类文件

类文件由Java编译器生成。 我们创建的.Java文件经过编译器后,会成为.Class文件,以便JVM可以识别并运行。

Class文件的核心设计思想是平台无关性。 它存储的不是操作系统可以直接识别的二进制本地机器代码,而是根据Java虚拟机规范定制的指令集、符号表等一些信息,因此只要在任何情况下开发出相应的Java虚拟机一个操作系统,可以运行开发者的Java程序。

类加载子系统(类加载器)

类加载子系统也可以称为类加载器。 JVM默认提供三个类加载器:

1.:称为启动类加载器,它是顶层类加载器,负责加载JDK中的核心类库,如rt.jar、.jar、.jar等。

2.:称为扩展类加载器,它负责加载Java扩展类库。 默认情况下会加载$中jre/lib/*.jar或-Djava.ext.dirs指定的目录下的jar包。

3、App:称为系统类加载器,负责加载应用程序目录下的所有jar和class文件。

除了Java默认提供的三个加载器之外,我们还可以根据自己的需求进行定制。 自定义的类加载器必须继承自java.lang。 班级。

除了两个默认加载器之外,它们都是继承自java.lang。 它不是一个普通的 Java 类。 它的底层是用C++编写的,并且已经嵌入到JVM的内核中。 当JVM启动时,它也会启动并负责加载核心类库后,并构造App类加载器。

类加载器子系统不仅负责定位和加载类文件,它还严格按照以下步骤做很多事情:

1、加载:寻找并导入Class文件的二进制信息
2、连接:进行验证、准备和解析
     1)验证:确保导入类型的正确性
     2)准备:为类型分配内存并初始化为默认值
     3)解析:将字符引用解析为直接引用
3、初始化:调用Java代码,初始化类变量为指定初始值

详细内容可以参考另一篇文章:Java类加载机制-知乎专栏

方法区(Area)

方法区用于存储JVM加载后由即时编译器编译的类型信息、常量、静态变量以及代码缓存。 方法区和Java堆区一样,是线程共享的内存区域。

JDK8之前,采用永久代的方式来实现方法区。 JDK8之后,永久代的概念被抛弃,方法区被JDK、J9等在本地内存中实现的Meta Space所取代。 好处 元空间将在运行时根据需要动态调整。 只要不超过当前进程可用的内存上限(32位和64位系统不同),就不会出现溢出问题。

方法区也可以被垃圾回收,但是条件很严格,必须是没有任何对该类的引用。 详细内容可以参考另一篇文章:Java性能优化JVM GC(垃圾回收机制)-知乎专栏

当扩展空间不足时,就会出现异常。

类型信息包括什么?

1、类型的全名(The fully qualified name of the type)
2、类型的父类型全名(除非没有父类型,或者父类型是java.lang.Object)(The fully qualified name of the typeís direct superclass)
3、该类型是一个类还是接口(class or an interface)(Whether or not the type is a class )
4、类型的修饰符(public,private,protected,static,final,volatile,transient等)(The typeís modifiers)
5、所有父接口全名的列表(An ordered list of the fully qualified names of any direct superinterfaces)
6、类型的字段信息(Field information)
7、类型的方法信息(Method information)
8、所有静态类变量(非常量)信息(All class (static) variables declared in the type, except constants)
9、一个指向类加载器的引用(A reference to class ClassLoader)
10、一个指向Class类的引用(A reference to class Class)
11、常量池(The constant pool for the type)

Java堆(JVM堆、Java堆)

堆区负责存储对象实例。 当Java创建类的实例对象或数组时,会在堆中为新对象分配内存。

虚拟机中只有一个堆,程序中的所有线程共享它。

通常,堆占用的内存空间最多。

堆的访问方式是管道类型,先进先出。

程序运行时,可以动态分配堆的内存大小。

堆的内存资源回收由JVM GC 管理。 详细内容请参考:Java性能优化之JVM GC(垃圾收集机制)-知乎专栏

当扩展空间不足时,就会出现异常。

虚拟机堆栈(JVM堆栈、VM Stack)

Java栈中只保存基本数据类型(参考:Java基本数据类型-四类八-知乎专栏)和对象引用。 请注意,它只是对对象的引用,而不是对象本身。 该对象存储在堆区域中。 。

拓展知识: Byte、Short、Long等封装类型都存储在堆中。

堆栈的访问类型类似于水杯,先进后出。

在栈中创建的基本类型数据超出其范围后会自动释放,并且不受JVM GC管理。 在堆栈上创建的引用类型实例仍然由 JVM GC 管理。

当一个线程创建并运行时,就会创建相应的堆栈。 每个堆栈中的数据都是私有的,不能被其他线程访问。

每个线程都会创建一个堆栈,每个堆栈包含多个堆栈帧。 每个堆栈帧对应于每个方法的每次调用。 栈帧包含三部分:

局部变量区(方法内基本类型变量和对象实例的引用)

操作数栈区(存储方法执行过程中产生的中间结果)

运行环境区(动态连接、返回相关信息的正确方法、异常捕获)

当虚拟机栈深度溢出或扩展失败时,会分别抛出 和 异常。

本地方法栈(Stack)

本地方法栈的功能与JVM栈非常相似。 不同的是,虚拟机栈执行Java方法,而本地方法栈执行local()方法服务。 它还存储本地方法的局部变量表和本地方法的操作数栈。 和其他信息。

堆栈的访问类型类似于水杯,先进后出。

栈中的数据超出其范围后会自动释放,不受JVM GC管理。

每个线程都包含一个堆栈区域,每个堆栈中的数据都是私有的,不能被其他堆栈访问。

当程序调用或者JVM调用本地方法接口()时,本地方法栈被启用。

本地方法不是用 Java 语言编写的。 它们可以用 C 或其他语言编写。 本地方法不是由JVM运行的,因此本地方法的运行不受JVM管理。

VM合并了本地方法栈和JVM栈。

当深度溢出或者扩展失败时,原生方法栈也会抛出异常。

程序计数器

在JVM的概念模型中,字节码解释器通过改变这个计数器的值来选择下一条需要执行的字节码指令。 分支、循环、跳转、异常处理、线程恢复等基本功能都是依靠这个计数器来完成的。

JVM的多线程是通过依次切换线程并分配处理器执行时间来实现的。 为了在线程切换后将计数器恢复到正确的执行位置,每个线程都会有一个独立的程序计数器。 。

程序计数器只占用少量的内存空间。

当线程执行Java方法时,程序计数器记录正在执行的JVM字节码指令的地址。 如果一个(方法)正在执行,那么这个计数器的值为空()。

程序计数器不会抛出(内存不足错误)。

JVM执行引擎

Java虚拟机相当于虚拟的“物理机”。 两台机器都具有代码执行能力。 主要区别在于物理机的执行引擎直接构建在处理器、硬件、指令集和操作系统层面。 JVM的执行引擎是自己实现的,因此程序员可以开发自己的指令集和执行引擎结构。

执行引擎的主要职责就是将这些自定义的指令集翻译成硬件支持的指令集格式然后执行。

虚拟机字节码执行引擎的概念模型是在 JVM 规范中制定的。 这种模型称为JVM执行引擎的统一外观。 每个Java虚拟机发布者都需要按照这个规范来实现。

在不同的虚拟机实现中,可能有两种执行方式:解释执行(通过解释器执行)和编译执行(通过即时编译器生成本机代码)。 虚拟机可以根据自身的需要实现执行引擎的一种或多种组合。 但无论内部如何实现,都必须遵循JVM规范要求,即输入是字节码文件,处理过程是等效的字节码解析过程,输出是执行结果。

本机方法接口 (JNI)

JNI是Java的缩写。 它提供了几个API来实现Java和其他语言(主要是C和C++)之间的通信。

JNI的适用场景

当我们有一些用C语言编写的旧库时,将它们移植到Java中是非常耗时的。 不过JNI可以支持Java程序与C语言编写的库进行交互,所以不需要移植。 或者可以使用JNI与硬件和操作系统交互,提高程序性能等。需要注意的一点是,需要确保本机代码可以在任何Java虚拟机环境中工作。

JNI 的副作用

一旦使用了JNI,Java程序就会失去Java平台的两个优点:

1.程序不再跨平台。 要实现跨平台,程序必须在不同的系统环境下用本地语言部分进行编译和配置。

2.程序不再绝对安全。 本地代码使用不当可能会导致整个程序崩溃。 一个通用的规则是,调用本地方法应该集中在少数几个类中,从而减少Java与其他语言之间的耦合。

JVM GC(垃圾收集机制)

具体可以参考我的另一篇文章:Java性能优化之JVM GC(垃圾收集机制)-知乎专栏

常量池

要了解常量池,首先要知道常量池分为三种类型:

1.Class文件内容中的常量池

2.运行时常量池(Pool)

3、各个包装类型中实现的常量池,如类中的字符串常量池(Pool)

类常量池

Java代码通过编译器后,会生成一个Class文件。 这个常量池是Class文件中的一大段内容(通常是最大的一段内容)。 它主要存储文字、符号引用等信息,存储在JVM中。 Class文件加载后,Class常量池中的数据将被存储到运行时常量池中。

运行时常量池(Pool)

运行时常量池是方法区(Area)的一部分。 运行时常量池中存储的是基本类型的数据和对象引用。 请注意,它是对对象的引用,而不是对象实例本身。

当Java虚拟机加载Class文件时,Class文件内容中常量池中的数据会被放入运行时常量池中。 每个加载的Class对象都会有一个运行时常量池。

字符串常量池(Pool)&其他封装类型实现的常量池

字符串由 char[] 组成。 当我们的Java程序中频繁出现相同的字面量代码时,重复创建和销毁对象是一种资源的浪费,因此Java实现了字符串常量池。

JDK7之后,字符串常量池从方法区迁移到堆区,其底层实现可以理解为一个。 Java虚拟机中只会有一个字符串常量池。 在字符串常量池中,存储的数据可以是引用,也可以是对象实例本身。

字符串常量池还具有运行时常量池的动态特性。 它支持在运行时将新常量放入池中。 开发人员通常通过 .() 方法使用此功能。

基本类型包装类和常量池

Byte、Short、Long,这七个包装类各自实现了自己的常量池。

Float 和这两种浮点类型没有实现常量池。

//例子:
Integer i1 = 20;
Integer i2 = 20;
System.out.println(i1==i2);//输出TRUE

Byte、Short、Long,这五个包装类都默认创建值为[-128, 127]的缓存数据。 当这五类数据不在这个范围内时,就会创建新的对象,并且这些新的对象不会放入常量池中。

//IntegerCache.low = -128
//IntegerCache.high = 127
public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
//例子
Integer i1 = 200;
Integer i2 = 200;
System.out.println(i1==i2);//返回FALSE

字符串常量池(pool)的实例

String str1 = "aaa";

当上面的代码运行时,JVM会去字符串常量池查找文字对象“aaa”是否存在:

:返回对象对变量str1的引用。

不存在:创建一个对象并返回对变量str1的引用。 (JDK8以后,对象实例直接存储在字符串常量池中)

String str1 = "aaa";
String str2 = "aaa";
System.out.println(str1 == str2);//返回TRUE

因为变量 str1 和 str2 都指向同一个对象,所以返回 true。

String str3 = new String("aaa");
System.out.println(str1 == str3);//返回FALSE

当我们使用new构造字符串对象时,无论字符串常量池中是否存在相同内容的对象的引用,都会创建一个新的字符串对象。 由于两者指向不同的对象,因此返回FALSE。

。()方法

对于使用new创建的字符串对象,如果想将这个对象添加到字符串常量池中,可以使用()方法。

String str1 = "aaa";
String str2 = "aaa";
String str3 = new String("aaa");
String interns = str3.intern();
System.out.println(interns == str1);//返回TRUE

()方法会检查字符串常量池中是否有匹配的对象,并执行以下操作:

存在:直接返回变量的对象引用。

不存在:将此对象引用添加到常量池中,然后将该对象引用返回给变量。

下面创建了多少个对象?

String str4 = "abc"+"efg";
String str5 = "abcefg";
System.out.println(str4 == str5);//返回TRUE

答案是三。 第一个:“abc”,第一个:“efg”,第三个:“abc”+“efg”(“”)

str5 =“”; 这段代码并没有创建对象,而是从常量池中查找“”的引用,因此str4 == str5返回TRUE,因为它们都指向同一个对象。

什么情况下字符串对象引用会自动添加到字符串常量池中?

//只有在这两种情况下会将对象引用自动加入到常量池:
String str1 = "aaa";
String str2 = "aa"+"a";
//以下都不会将对象引用自动加入到常量池:
String str3 = new String("aaa");
String str4 = New StringBuilder("aa").append("a").toString();
StringBuilder sb = New StringBuilder();
sb.append("aa");
sb.append("a");
String str5 = sb.toString();

双等号(==)的含义

基本数据类型之间使用双等号,并对值进行比较。

引用类型(Class类)之间使用双等号来比较对象的引用地址是否相等。

好了,JVM的基本原理就写到这里了。 以后有更深入的了解后我会补充。