你了解JVM吗,快来看看这篇文章
本文基于Jdk1.7版本,VM为Hotspot前言:在讲JVM之前,首先引入一个概念叫“跨平台”,学JAVA的人都知道,Java就是一门跨平台的语言,其实就是因为Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。(面试点)概念:JVM 即Java虚拟机,...
本文基于Jdk1.7版本,VM为Hotspot
前言:
在讲JVM之前,首先引入一个概念叫“跨平台”,学JAVA的人都知道,Java就是一门跨平台的语言,其实就是因为Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。(面试点)
概念:
JVM 即Java虚拟机, 由类加载子系统、运行时数据区、执行引擎以及本地方法接口组成。本文着重介绍类加载子系统 和 运行时数据区。
一、类加载子系统:
Java的类加载功能是通过类加载子系统来完成的。当JVM需要使用一个类的时候,类加载子系统能够从class文件载入、链接和初始化这个类。整个类加载过程如下图所示(面试点):
1、加载
加载是类加载过程中的第一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对 象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既 可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理), 也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。
2、校验
这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并 且不会危害虚拟机自身的安全。
3、准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
例:
public static int vPort= 80; (静态变量)
实际上变量 vPort在准备阶段过后的初始值为 0 而不是 80,将 vPort赋值为 80 是在初始化阶段;public static final int vPort= 80; (常量)
在编译阶段会为 vPort生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 vPort赋值为 80。
4、解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。
符号引用:
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。
各种虚拟 机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
直接引用:
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。例:
User user = new User();
在未加载时候,user仅仅是一个字符(符号引用)它指向了一个实例,但是这个实例还不存在;
准备过程之后,实例分配了内存那么就会有内存地址或者物理地址(直接引用)
5、初始化
初始化阶段是类加载最后一个阶段,到了初始阶段,才开始真正执行类中定义的 Java 程序代码。
在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源(调用构造器)。
二、类的双亲委派机制:
类加载器:
启动类加载器(BootstrapClassLoader):负责加载 JAVA_HOME\jre\lib目录中的类库,或通过-Xbootclasspath 参数指定路径中的,且被 虚拟机认可(按文件名识别,如 rt.jar)的类库。
扩展类加载器(ExtensionClassLoader):负责加载JAVA_HOME\jre\lib\ext目录中的类库,或通过 java.ext.dirs 系统变量指定路径中的类库。
应用程序加载器(ApplicationClassLoader):负责加载用户路径(classpath)上的类库。
自定义类加载器:负责加载用户指定目录下的类库,可以通过继承 java.lang.ClassLoader 实现自定义的类加载器。
过程:
当类加载器接收到一个类的加载请求时,大致做了如下步骤:(面试点)
1)检测自己是否有加载过目标类,如果有直接进入第6步,否则进入第2步。
2)委托给父类加载器去加载目标类,父类加载器同样策略执行第1步,一直到启动类加载器。
3)当所有的父类加载器都无法加载的时候,当前类加载器执行第4步
4)加载目标类,如果找不到类,执行第5步,否则执行第6步,并将其放入自己的缓存中
5)抛出ClassNotFountException异常。
6)返回对应的java.lang.Class对象。
好处:
采用双亲委派的一个好处是:比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载 器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载 器最终得到的都是同样一个 Object 对象。
三、运行时数据区:
组成部分:程序计数器(线程私有)、虚拟机栈(线程私有)、本地方法栈(线程私有)、方法区/永久代(线程共享)、堆(线程共享)
程序计数器:
一块较小的内存空间,是当前线程所执行的字节码的行号指示器,记录着当前程序运行到哪了字节码指令。分支,循环,跳转,异常处理,线程回复等都需要依赖这个计数器来完成 。
由于Java的多线程是通过线程轮流切换完成的,一个线程没有执行完时就需要一个东西记录它执行到哪了,下次抢占到了CPU资源时再从这开始,这个东西就是程序计数器,正是因为这样,所以它也是“线程私有”的内存。
如果线程正在执行的是Java 方法,则这个计数器记录的是正在执行的虚拟机字节码指令地址;
如果正在执行的是Native 方法,则这个计数器值为空(null);
这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域;
虚拟机栈:
是描述java方法执行的内存模型,先进后出原则,每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成 的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
可能抛出的异常:
StackOverflowError:如果线程内请求的栈深度超过虚拟机允许的深度,会抛出该异常。(通常由递归不当造成)
OutOfMemoryError:如果虚拟机允许动态扩展栈的大小,但是在扩展时无法申请到足够的内存空间,会抛出该异常
栈帧( Frame):
用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接 (Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异 常)都算作方法结束。
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈就都已经确定了。因此一个栈帧需要分配多大的内存不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。先进后出原则:
如果方法methodOne方法调用了methodTwo,那么methodOne就会先入栈创建一个栈桢,接着methodTwo再入栈成为栈顶(假设没有其他的方法执行),methodTwo执行完先出栈,接着methodOne执行完出栈。
局部变量表:
存放了编译期可知的各种基本数据类型,对象引用(仅限局部变量的,不包含成员变量的)。其中每个局部变量空间(Slot)有32位,所以long和double类型的数据会占用两个局部变量空间,其他类型包括对象引用占用一个。对象引用调用的是存在堆中的对象,这个引用可以是对象的起始地址或者是指向对象的句柄。局部变量表所需的内存在编译期就已经确定了也就是进入这个方法时就已经确定了,运行期间不会更改。
操作数栈:
存储方法内一些进行了运算操作后的结果。
当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。例如,在做算术运算的时候就是通过操作数栈来进行的,又或者调用其它方法的时候是通过操作数栈来行参数传递的。动态链接:
在方法内调用接口,通过字面量链接到具体的实现类,实现Java的动态特性;
每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态链接。方法出口:
当一个方法被执行后,有两种方式退出这个方法。
执行引擎遇到方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者。这种退出方法方式称为正常完成出口。
在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理(没有catch),就会导致方法退出。这种退出方式称为异常完成出口。一个方法使用异常完成出口的方式退出不会产生任何返回值。
本地方法栈:
本地方法栈与虚拟机栈非常类似,他们之间的区别只是虚拟机栈为虚拟机执行Java服务,本地方法栈为虚拟机执行Native方法服务。
可能抛出的异常:StackOverflowError和OutOfMemoryError异常
方法区/永久代:
方法区也是一个线程共享的区域,存储已被虚拟机加载的类信息,常量(final),静态变量(static),JIT(即时编译器)编译后的代码等数据。(常量也可以在运行时产生,如String的intern方法)
虚拟机对方法区规范非常宽松,除了和Java的堆一样不需要连续的内存和可以选择固定大小意外,还可以选择不实现垃圾回收。垃圾回收行为在这个区域比较少见但还是有必要的,主要是针对常量池回收和类型的卸载
1.对于每一个加载的类型,会在方法区中保存以下信息:
类及其父类的全限定名(java.lang.Object没有父类)、类的类型(Class or Interface)、访问修饰符(public, abstract, final)、实现的接口的全限定名的列表、常量池、字段信息、方法信息、除常量外的静态变量、ClassLoader引用、Class引用;2.对于每一个字段,会在方法区中保存以下信息(字段声明顺序也会保存):
字段名、字段的类型、字段的修饰符(public, private , protected, static, final, volatile, transient);3.对于每一个方法,会在方法区中保存以下信息(方法声明顺序也会保存):
方法名、方法返回类型(或void)、参数信息、方法修饰符(public, private, protected , static, final, synchronized, native, abstract);如果方法不是抽象方法并不是本地方法(Native Method),还会保存以下信息: 方法的字节码、本地变量表及操作数栈的大小、异常表;
常量池:
它是方法区(Method Area)的一部分。用于存放编译期间生成的各种字面量和符号引用。常量池中存储的是对象的引用而不是对象的本身。
堆:
线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以 细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。
可能抛出的异常:
OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法扩展时,将会抛出此异常。
新生代:
是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC(采用复制算法)进行垃圾回收。分为 Eden 区、ServivorFrom、ServivorTo 三个区。
1)Eden区:Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代,如:大的数组)。当Eden区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。
2)ServivorFrom:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
3) ServivorTo:保留了一次 MinorGC 过程中的幸存者。
MinorGC的过程(复制->清空->互换)
1).首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);
2).然后清空 Eden 和 ServicorFrom 中的对象;
3).最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom区。
图解JVM GC过程:https://www.jianshu.com/p/314272e6d35b
老年代:
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
MajorGC 采用标记清除算法:
1).首先扫描一次所有老年代,标记出存活的对象; 2).然后回收没有标记的对象。
MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。
结尾:
本文是最近学习JVM的一些总结和记录,如有不对的地方,欢迎评论吐槽。
更多推荐
所有评论(0)