Java-Jvm
简介
JVM(Java Virtual Machine) 是Java平台的核心组件之一,它是一个在计算机上执行Java的虚拟机。他允许Java程序在不同的操作系统上运行,提供了跨平台的特性。
JVM的主要功能是执行Java字节码。在Java开发中,源代码首先被编译成字节码文件(以.class为扩展名),这些字节码文件包含了与特定平台无关的指令集。然后JVM将字节码解释或编译成特定平台的机器码,以便在该平台上执行。
JDK JRE 与 JVM
JDK
Jdk(Java Development Kit) 是Java开发工具包,它是用于开发Java应用程序的完整工作集。Jdk包含Jre,所以它具备Jre的所有功能。此外,Jdk还包含编译器(javac)和其他开发工具,用于将Java源代码编译成Java字节码。开发人员可以用Jdk来编写、编译和调试Java应用程序。
JRE
Jre(Java Runtime Environment) 是Java运行时环境,他是运行Java应用程序所需的基本环境。Jre包含了Jvm以及执行Java程序所需的类库、资源文件和其他支持文件。当我们在计算机上安装Java时,实际上就是安装了Jre。如果只需要运行Java应用程序而不进行开发,安装Jre就足够了。
三者关系
-
JDK(Java Development Kit)是Java开发工具包,包含了JRE以及用于开发Java应用程序的工具。
-
JRE(Java Runtime Environment)是JDK的一个子集。JRE包含了JVM和执行Java应用程序所需的类库、资源文件等。
-
JVM(Java Virtual Machine)是Java虚拟机,是Java平台的核心组件之一。
简单来说,JDK包含了JRE,而JRE包含了JVM。JDK提供了开发Java应用程序所需的工具和环境,包括编译器和其他开发工具。JRE提供了运行Java应用程序所需的基本环境,其中包括JVM。JVM则负责执行Java字节码,将其转换为机器码并执行。
JVM所处的位置
JVM位于操作系统和Java应用程序之间,作为一个中间层。它提供了Java应用程序与底层操作系统之间的桥梁,实现了Java的跨平台特性。
当我们运行Java应用程序时,操作系统会调用JVM来执行程序。JVM会加载应用程序的字节码,并将其解释或编译成特定平台上的机器码,然后在操作系统上执行。JVM负责处理内存管理、线程管理、异常处理等任务,为Java应用程序提供一个独立于操作系统的执行环境。
JVM的体系结构
JVM的执行流程
-
编写Java程序:首先,开发人员使用Java编程语言编写源代码文件(.java文件),其中包含了类、方法和其他程序元素的定义。
-
编译Java程序:使用Java编译器(javac),将源代码文件编译为字节码文件(.class文件)。字节码是一种中间代码,它是一种与特定平台无关的二进制格式,可以在JVM上执行。
-
类加载:JVM的类加载器(ClassLoader)负责加载字节码文件。类加载器按需加载类文件,并将其转换为JVM内部的数据结构。
-
字节码验证:在加载类之后,JVM会对字节码进行验证,以确保其符合Java语言规范和安全要求。字节码验证检查类的结构、类型安全、访问权限等,以防止恶意代码或错误的字节码对系统造成损害。
-
解释与编译:JVM有两种执行字节码的方式:解释执行和即时编译(Just-in-Time Compilation,JIT)。在解释执行中,JVM逐条解释字节码并执行相应的操作。在即时编译中,JVM将热点代码(频繁执行的代码)编译为本地机器码,以提高执行效率。
-
运行时内存管理:JVM负责管理程序的运行时内存。它将内存划分为不同的区域,包括堆(Heap)、栈(Stack)、方法区(Method Area)等。JVM动态分配和回收内存,并执行垃圾回收(Garbage Collection)以释放不再使用的对象。
-
异常处理:JVM提供了异常处理机制,用于捕获和处理程序中的异常情况。当异常发生时,JVM会查找适当的异常处理程序,并执行相应的操作,如抛出异常、捕获异常或执行异常处理代码块。
-
安全管理:JVM的安全管理器(Security Manager)控制Java程序的权限。它根据安全策略文件定义的规则,限制代码对系统资源的访问和操作,以提供安全的执行环境。
-
程序退出:当Java程序执行完成或遇到特定的退出条件时,JVM会终止程序的执行并释放相关资源。
类加载器
负责将Java字节码加载到JVM中。它将字节码文件解析成JVM内部使用的数据结构,并存储在方法区中。类加载器根据需要动态加载和链接类,实现了Java的动态性和灵活性。
-
启动类加载器(Bootstrap Class Loader):也称为根加载器,它是JVM的一部分,负责加载Java的核心类库,包括
java.lang
、java.util
等。启动类加载器是用本地代码实现的,不是由Java类实现的。 -
扩展类加载器(Extension Class Loader):它是Java平台的扩展机制的一部分,负责加载Java平台的扩展库,位于
jre/lib/ext
目录下的JAR文件。它是由sun.misc.Launcher$ExtClassLoader
类实现的。 -
应用程序类加载器(Application Class Loader):也称为系统类加载器,它负责加载应用程序的类,包括开发人员自己编写的类以及第三方库。应用程序类加载器是由sun.misc.Launcher$AppClassLoader类实现的。
类加载器的执行流程
-
加载(Loading):加载是类加载器的第一个阶段。当程序需要使用某个类时,类加载器会根据类的名称查找并读取类的字节码数据。字节码可以来自于本地文件系统、网络或其他来源。
-
验证(Verification):验证是类加载器的下一个阶段。在这个阶段,类加载器会对已加载的字节码进行验证,确保其符合Java虚拟机规范和安全要求。验证过程包括文件格式验证、语义验证、字节码验证和符号引用验证等。
-
准备(Preparation):准备阶段是为类的静态变量分配内存并设置初始值的过程。在准备阶段,静态变量被赋予默认值,例如数值类型为0,引用类型为null。
-
解析(Resolution):解析是将符号引用转换为直接引用的过程。在解析阶段,类加载器会解析类中的符号引用,将其替换为直接引用,以便在后续的执行过程中能够正确地定位到相关的类、方法和字段。
-
初始化(Initialization):初始化是类加载器的最后一个阶段。在这个阶段,类的静态变量会被赋予程序中指定的初值,静态代码块会被执行,以及执行其他必要的初始化操作。类的初始化是在首次使用该类的时候进行的,且只会进行一次。
-
使用(Usage):在类加载器完成上述阶段后,类就可以被程序使用了。程序可以通过类加载器创建类的实例、调用类的方法和访问类的字段等。
双亲委托机制
双亲委托机制(Parent Delegation Model)是Java类加载器的一种工作机制,用于保证类的唯一性和一致性,并提供类加载器的层次结构。
根据双亲委托机制,当一个类加载器需要加载类时,它首先将加载请求委托给其父类加载器。父类加载器会依次向上委托,直到达到最顶层的启动类加载器。只有当父类加载器无法加载该类时,当前类加载器才会尝试加载该类。
具体来说,当类加载器接收到加载请求时,依照双亲委托机制,它会按照以下步骤执行:
-
检查该类是否已经被加载过,如果已经加载过,则直接返回该类的引用。
-
如果该类尚未加载,则将加载请求委托给父类加载器。
-
父类加载器会按照相同的步骤继续检查和委托加载请求,直到达到最顶层的启动类加载器。
-
如果最顶层的启动类加载器无法加载该类,则会依次向下回溯,尝试使用自己的加载方式加载该类。
-
如果所有的父类加载器都无法加载该类,则会抛出类找不到的异常(ClassNotFoundException)。
本地方法栈
Native关键字
在Java中,关键字"native"用于修饰方法,表示该方法是一个本地方法(Native Method)。本地方法是使用本地语言(如C、C++)编写的方法,它们与Java虚拟机(JVM)的执行环境有直接的交互。
具有"native"关键字修饰的方法声明在Java代码中只有方法的声明部分,方法的具体实现是在本地代码中完成的。这意味着该方法的实现不是使用Java语言编写的,而是使用本地语言编写的,通常是通过Java Native Interface(JNI)实现的。
凡是带了native 关键宁的,说java的作用范用达不到了,会去调用底层C语言的库!会进入本地方法栈调用本地方法本地接口 。JNI(Java Native Interface)。
-
JNI作用: 扩展Java的使用,融合不同的编程语言为Java所用。
-
最初: C、C++。Java诞生的时候 C、C++ 横行,想要立足,必须些有调用C、C++ 的程序。
-
所以Java在内存区城中专门开牌了一块标记区域: Native Method Stack,用于登记 native 方法。
-
在最终执行的时候,加载本地方法库中的方法通过JNI
程序计数器
JVM程序计数器(Program Counter Register)是Java虚拟机中的一块内存区域,它用于指示当前线程执行的字节码指令的地址或索引。想象你在一本书中阅读,并且你想跟踪你读到哪一页。你可以使用一个小标记,比如一根笔或者书签来标记当前阅读的页码。这个标记就是类似于计算机中的程序计数器。就像你在阅读时移动书签来跟踪你的进度一样,处理器会在执行完一条指令后,将程序计数器递增到下一条指令的位置,以便继续执行。这样,它可以按照正确的顺序执行指令,让程序正常运行。
每个线程在JVM中都有一个独立的程序计数器,它是线程私有的。在任何给定的时刻,一个线程只能执行一个方法,而程序计数器用于跟踪线程当前正在执行的方法的位置。
程序计数器在Java虚拟机中起到以下几个重要的作用:
-
字节码解释器:程序计数器指示了字节码解释器下一条要执行的指令地址。字节码解释器通过不断地从程序计数器中读取指令,解码并执行它们,从而实现了Java程序的执行。
-
方法间调用:程序计数器记录了方法调用的返回地址。当一个方法调用另一个方法时,程序计数器保存了当前方法被调用的位置,以便在被调用方法执行完毕后,能够返回到正确的位置继续执行。
-
异常处理:程序计数器也在异常处理中起到重要作用。当发生异常时,程序计数器指示了异常处理器应该从哪个位置开始执行异常处理代码。
需要注意的是,程序计数器是JVM中唯一一个不会出现OutOfMemoryError的内存区域。它的分配空间是固定且较小的,主要用于线程间的切换和字节码的解释执行,并且不会进行垃圾回收。
方法区
JVM方法区(Method Area)是Java虚拟机中的一块内存区域,用于存储类的结构信息、静态变量、常量、方法字节码等数据。
方法区是所有线程共享的内存区域,它在JVM启动时被创建,并且与Java堆(Heap)是分开的。方法区的大小可以通过命令行参数进行配置。
方法区主要用于存储以下内容:
-
类的结构信息:包括类的完整结构、字段描述符、方法描述符、访问标志等。
-
静态变量:存储类的静态变量,这些变量在类加载时被初始化,并且在整个生命周期中保持不变。
-
常量池(Constant Pool):存储类的常量,包括字符串字面值、类和接口的符号引用、字段和方法的符号引用等。
-
方法字节码:存储类的方法字节码,即编译后的Java字节码指令。
-
符号引用:存储类、字段和方法的符号引用,包括类名、字段名、方法名以及对应的描述符。
栈
JVM的栈(Stack)是Java虚拟机中的一块内存区域,用于存储方法的调用和执行信息以及八大数据类型。每个线程在JVM中都有一个私有的栈,用于跟踪线程的方法调用和执行状态。
JVM栈存储了方法的栈帧(Stack Frame),每个方法在执行时都会创建一个对应的栈帧。
栈的主要作用是支持方法调用和方法执行的过程。当一个方法调用另一个方法时,JVM会在栈中创建一个新的栈帧,将参数和局部变量存储在该栈帧中,并将控制转移到被调用方法的栈帧中。当被调用方法执行完毕后,对应的栈帧会被弹出,控制权回到调用方法的栈帧中。
思考: 为什么Java程序会先执行main方法?
当JVM启动时,它会创建一个主线程,并为该线程分配一个对应的虚拟机栈。然后,JVM会在主线程的栈中创建一个栈帧(Stack Frame),栈帧中包含了main方法的信息,例如局部变量表、操作数栈、动态链接等。接下来,JVM开始执行这个栈帧中的指令,即执行main方法的代码。
栈内存溢出
原因:
栈内存溢出(Stack Overflow)是指当程序在执行过程中使用的栈空间超出了其分配的限制,导致栈溢出的错误。
当栈空间不足以容纳新的栈帧时,就会发生栈溢出。这通常是由递归方法的无限递归调用导致的,每次递归调用都会创建一个新的栈帧,当递归层级过深时,栈的空间会被消耗殆尽,导致栈溢出错误。
栈内存溢出可能会导致程序中止并抛出异常。在Java中,常见的栈溢出异常是StackOverflowError。
解决:
-
优化递归算法:检查递归算法是否可以进行优化,避免无限递归调用。
-
减少局部变量的使用:减少方法中使用的大量局部变量,或者使用更合适的数据结构来管理数据。
-
增加栈空间限制:
-Xss<size>:该参数用于指定每个线程的栈大小。可以使用不同的单位,如k(千字节)或m(兆字节)。例如,-Xss1m 表示每个线程的栈大小为1兆字节。
-XX:ThreadStackSize=<size>:该参数也用于指定每个线程的栈大小,可以使用不同的单位。例如-XX:ThreadStackSize=1024 表示每个线程的栈大小为1024字节。
-
使用循环代替递归:在可能的情况下,将递归算法转换为迭代算法,使用循环进行实现。