本文共 10015 字,大约阅读时间需要 33 分钟。
JVM :英文名称(Java Virtual Machine),就是我们耳熟能详的 Java 虚拟机。它只认识 xxx.class 这种类型的文件,它能够将 class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作。所以说,jvm 是 Java 能够跨平台的核心,具体的下文会详细说明。
JRE :英文名称(Java Runtime Environment),我们叫它:Java 运行时环境。它主要包含两个部分,jvm 的标准实现和 Java 的一些基本类库。它相对于 jvm 来说,多出来的是一部分的 Java 类库。JDK :英文名称(Java Development Kit),Java 开发工具包。jdk 是整个 Java 开发的核心,它集成了jre 和一些好用的小工具。例如:javac.exe,java.exe,jar.exe 等。JavaCard:支持一些Java小程序(Applets)运行在小内存设备(如智能卡)上的平台。JavaME(MicroEdition):支持Java程序运行在移动终端(手机、PDA)上的平台,对JavaAPI有所精简,并加入了针对移动终端的支持,这个版本以前称为J2ME。JavaSE(StandardEdition):支持面向桌面级应用(如Windows下的应用程序)的Java平台,提供了完整的Java核心API,这个版本以前称为J2SE。JavaEE(EnterpriseEdition):支持使用多层架构的企业应用(如ERP、CRM应用)的Java平台,除了提供JavaSEAPI外,还对其做了大量的扩充并提供了相关的部署支持,这 个版本以前称为J2EE。虚拟机的发展
1.Sun Classic VM
2.Exact VM
3.HotSpot VM
HotSpot VM的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知JIT编译器以方法为单位进行编译。 如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准编译和OSR(栈上替换)编译动作。 通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间与最佳执行性能中取得平衡,而且无须等待本地代码输出才能执行程序,即时编译的时间压力也相对减小,这样有助于引入更多的代码优化技术,输出质量更高的本地代码。4.JRockit5.J9
6.KVM(Kilobyte)
KVM中的K是“Kilobyte”的意思,它强调简单、 轻量、 高度可移植,但是运行速度比较慢。 在Android、iOS等智能手机操作系统出现前曾经在手机平台上得到非常广泛的应用。 java未来的发展方向JVM在运行过程中会把它所管理的内存划分成若干不同的数据区域!
线程私有:程序计数器、虚拟机栈、本地方法栈 线程共享(公有):堆、方法区是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于Java虚拟机的 多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址; 如果正在执行的是Native方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。为什么需要程序计数器?
- Java是多线程的,意味着线程切换
- 确保多线程情况下的程序正常执行
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时 都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出 栈的过程。
经常有人把Java内存区分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,Java内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。其中所指的“堆”笔者在后面会专门讲述,而所指的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和 returnAddress类型(指向了一条字节码指令的地址)。 其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变 量表的大小。 在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。1、局部变量表
局部标量表是一组变量值的存储空间,用于存放方法参数和局部变量。在Class 文件的方法表的Code 属性的 max_locals 指定了该方法所需局部变量表的最大容量。
局部变量表的基本单位为变量槽(slot,下文有单独的介绍);局部变量表存放的是方法参数和局部变量;虚拟机是通过索引定位的方式使用局部变量表。 当调用方法是非static 方法时,局部变量表中第0位索引的 Slot 默认是用于传递方法所属对象实例的引用(reference),即 “this” 关键字指向的对象。分配完方法参数后,便会依次分配方法内部定义的局部变量。 为了节省栈帧空间,局部变量表中的 Slot 是可以重用的。因为即使是一个方法内,也是存在作用域的,当离开了某些变量的作用域之后,这些变量对应的 Slot 空间就可以交给其他变量使用。但是这种机制有时候会影响垃圾回收行为,原因很简单,当离开某个作用域时,如果没有新的变量值覆盖之前作用域内的变量(指reference)空间,那么当垃圾回收时,则该引用对应的java堆中的 内存则不允许被回收,因为局部变量表中还存在该引用。所以问题在于虚拟机并没有主动清理局部变量表中离开作用域的变量值,而是采用新盖旧的方法被动清理。 所以很明显,局部变量表的作用就是记录执行该方法时会使用到的变量值,它可以说这个方法的数据池,是我们方法中变量的化身,相当于把我们方法中所需要的变量整合成一个数组对象或集合对象,这个对象的名称就叫做局部变量表。2、操作数栈
操作数栈也常被称为操作栈,。在Class 文件的Code 属性的 max_stacks 指定了执行过程中最大的栈深度。Java 虚拟机的解释执行引擎称为“基于栈的执行引擎”,这里的栈就是指操作数栈。
操作数栈的每个位置上可以保存一个java虚拟机中定义的任意数据类型的值,包括long和double。 很明显,操作数栈是方法执行算术运算或者是调用其他的方法进行参数传递的时候时的媒介,这就是“基于栈的执行引擎”。操作数栈中的元素类型必须与字节码指令序列严格匹配,比如不能用iadd 指令去加两个long类型的数据。当然这些基本数据类型的校验在编译期中会校验,编译是无法通过两个long类型加为int类型的代码。当然指令还有很多种,不要像我一开始就产生困惑iadd这么简单的指令如何解释那么复杂的代码的。 当一个方法刚开始执行时,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈出栈的操作。 在概念模型中,两个栈帧是相互独立的。但是大多数虚拟机的实现都会进行优化,令两个栈帧出现一部分重叠。令下面的部分操作数栈与上面的局部变量表重叠在一块,这样在方法调用的时候可以共用一部分数据,无需进行额外的参数复制传递。虚拟机栈的两种典型错误:
StackOverflowError:若单个线程请求的栈深度大于虚拟机允许的深度,则会抛出StackOverflowError(栈溢出错误)。JVM会为每个线程的虚拟机栈分配一定的内存大小(-Xss参数),因此虚拟机栈能够容纳的栈帧数量是有限的,若栈帧不断进栈而不出栈,最终会导致当前线程虚拟机栈的内存空间耗尽,典型如一个无结束条件的递归函数调用。
OutOfMemoryError:不同于StackOverflowError,OutOfMemoryError指的是当整个虚拟机栈内存耗尽,并且无法再申请到新的内存时抛出的异常。JVM未提供设置整个虚拟机栈占用内存的配置参数。虚拟机栈的最大内存大致上等于“JVM进程能占用的最大内存(依赖于具体操作系统) - 最大堆内存 - 最大方法区内存 - 程序计数器内存(可以忽略不计) - JVM进程本身消耗内存”。当虚拟机栈能够使用的最大内存被耗尽后,便会抛出OutOfMemoryError,可以通过不断开启新的线程来模拟这种异常。
3、动态连接
每个栈帧都包含一个指向当前方法所在类型的运行时常量池的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。通俗点讲就是对堆中对象的引用路径。
4、方法返回地址
当一个方法开始执行以后,只有两种方法可以退出当前方法:
当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址。 当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定。 当方法返回时,可能进行3个操作:1、恢复上层方法的局部变量表和操作数栈。
2、把返回值压入调用者调用者栈帧的操作数栈。
3、调整 程序计数器的值以指向方法调用指令后面的一条指令。
本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的native 方法。和java虚拟机栈类似,本地方法栈也是一个后入先出(Last In First Out)栈。由于是线程私有的,生命周期随着线程,线程启动而产生,线程结束而消亡。本地方法栈也会抛出 StackOverflowError 和 OutOfM。
JNI开发流程主要分为以下6步:
1、编写声明了native方法的Java类。 2、将Java源代码编译成class字节码文件。 3、用javah -jni命令生成.h头文件(javah是jdk自带的一个命令,-jni参数表示将class中用native声明的函数生成jni规则的函数)。 4、用本地代码实现.h头文件中的函数。 5、将本地代码编译成动态库(windows:.dll,linux/unix:.so,mac os x:*.jnilib)。 6、拷贝动态库至 java.library.path 本地库搜索目录下,并运行Java程序。
对于大多数应用来说,Java堆(JavaHeap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(GarbageCollectedHeap,幸好国内没翻译成“垃圾堆”)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、FromSurvivor空间、ToSurvivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。 根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过Xmx和Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。方法区(Method Area)与 Java 堆一样,是所有线程共享的内存区域。JDK7 之前(永久代)用于存储已被虚拟机加载的类信息、常量、字符串常量、类静态变量、即时编译器编译后的代码等数据。方法区主要包括已加载的类信息和常量池等内容(随着jdk版本变化有更新)。
Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的是与 Java 堆区分开。
类加载信息主要有:类型信息、类型的常量池、类字段信息、方法信息、类变量、指向类加载器的引用、指向Class实例的引用、方法表等。
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本/字段/方法/接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将类在加载后进入方法区的运行时常量池中存放。
Java虚拟机对Class文件每一部分(自然也包括常量池)的格式都有严格规定,每一个字 节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行,但对于运行 时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自 己的需要来实现这个内存区域。不过,一般来说,除了保存Class文件中描述的符号引用外, 还会把翻译出来的直接引用也存储在运行
时常量池中。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方 法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申 请到内存时会抛出OutOfMemoryError异常。Java 中基本类型的包装类的大部分都实现了常量池技术,这些类是 Byte、Short、Integer、Long、Character、Boolean,另外 Float 和 Double 类型的包装类则没有实现。另外 Byte、Short、Integer、Long、Character 这5种整型的包装类也只是在对应值在-128到127之间时才可使用对象池。
永久代:存储包括类信息、常量、字符串常量、类静态变量、即时编译器编译后的代码等数据。可以通过 -XX:PermSize 和 -XX:MaxPermSize 来进行调节。当内存不足时,会导致OutOfMemoryError 异常。JDK8 彻底将永久代移除出 HotSpot JVM,将其原有的数据迁移至Java Heap 或 Native Heap(Metaspace),取代它的是另一个内存区域被称为元空间(Metaspace)。
元空间(Metaspace):元空间是方法区的在 HotSpot JVM 中的实现,方法区主要用于存储类信息、常量池、方法数据、方法代码、符号引用等。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。,理论上取决于32位/64位系统内存大小,可以通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 配置内存大小。随JDK版本变迁的方法区:
JDK6 class 元数据信息每个类的运行时常量池(字段、方法、类、接口等符号引用)、编译后的代码静态字段(无论是否有final)在instanceKlass 末尾(位于 PermGen 内)oop(Ordinary Object Pointer(普通对象指针)) 其实就是 Class 对象实例全局字符串常量池 StringTable,本质上就是个 Hashtable符号引用(类型指针是 SymbolKlass)。JDK7 class 元数据信息每个类的运行时常量池(字段、方法、类、接口等符号引用)、编译后的代码静态字段从 instanceKlass 末尾移动到了 java.lang.Class 对象(oop)的末尾(位于 Java Heap内)oop 与全局字符串常量池移到 Java Heap 上符号引用被移动到 Native Heap 中。JDK8 移除永久代,class 元数据信息每个类的运行时常量池、编译后的代码移到了另一块与堆不相连的本地内存 -- 元空间(Metaspace)。JVM各版本内存区域的变化:
JDK1.6 运行时常量池在方法区中。JDK1.7 运行时常量池在堆中。JDK1.8 去永久代,使用元空间(空间大小只受制于机器的内存)替代永久代,永久代参数 -XX:PermSize;-XX:MaxPermSize =100M 超过100M OOM()元空间参数 -XX:MetaspaceSize; -XX:MaxMetaspaceSize why? 永久代来存储类信息、常量、静态变量等数据不是个好主意, 很容易遇到内存溢出的问题。对永久代进行调优是很困难的,同时将元空间与堆的垃圾回收进行了隔离,避免永久代引发的Full GC和OOM等问题;
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储 在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著 提高性能,因为避免了在Java堆和Native堆中来回复制数据。显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。java试图定义一个Java内存模型(Java memory model JMM模型)来屏蔽掉各种硬件/操作系统的内存访问差异,以实现让java程序在各个平台下都能达到一致的内存访问效果。java内存模型主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
转载地址:http://vfuqb.baihongyu.com/