深入JVM内核
Posted 陈郑游
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入JVM内核相关的知识,希望对你有一定的参考价值。
1、Java与JVM介绍
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。
本文再次更新,经过*里**面试之后,觉得自己理解还是不是很好,有可能紧张(我连类加载器都忘记提了,心塞),有重新找资料书等。如果不嫌弃的话,可以下载我的整理版看看(JVM整理文档链接下载)。
GitHub库:https://github.com/andyczy/czy-study-jvm
1.1、Java体系结构包括四个独立的方面:
- Java程序设计语言
- Java class文件格式
- Java应用编程接口(Java API)
- Java虚拟机
Java面向网络的核心就是Java虚拟机,它支持Java面向网络体系结构三大支柱的所有方面:平台无关性,完全性和网络移动性。
1.2、Java程序的执行过程
Java技术的核心就是Java虚拟机,因为所有的Java程序都在虚拟机上运行。Java程序的运行需要Java虚拟机、Java API和Java Class文件的配合。Java虚拟机实例负责运行一个Java程序。当启动一个Java程序时,一个虚拟机实例就诞生了。当程序结束,这个虚拟机实例也就消亡。
1.3、加载.class文件的方式
- 从本地系统中直接加载
- 通过网络下载.class文件(URLClassLoader)
- 从zip, jar等归档文件中加载.class文件
- 从专有数据库中提取.class文件(很少用)
- 将Java源文件动态编译为.class文件
1.4、JVM内部结构
1.4.1、JVM与程序的生命周期
• 在如下几种情况下, Java虚拟机将结束生命周期
- 执行了System.exit()方法(看英文解释:非0退出)
java.lang.Object java.lang.System public static void exit(int status) Terminates the currently running Java Virtual Machine. The argument serves as a status code; by convention, a nonzero status code indicates abnormal termination. This method calls the exit method in class Runtime. This method never returns normally.
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
1.4.1、类装载器(ClassLoader)
1.4.2、类装载器的装载、链接与初始化
类装载器子系统除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。这些动作必须严格按以下顺序进行:
(1)装载——查找并装载类型的二进制数据
(2)连接——指向验证、准备、以及解析(可选)
● 验证:确保被导入类型的正确性(java可以自定义安全策略等)
● 准备:为类变量分配内存,并将其初始化为默认值
● 解析:把类型中的符号引用转换为直接引用
(3)初始化——把类变量初始化为正确初始值
1.4.3、有两种类型的类加载器(父委托机制)
Java虚拟机自带的加载器
• 根类加载器( Bootstrap【C++编写,无法在Java代码获取】)
• 扩展类加载器( Extension【Java代码实现】)
• 系统类加载器( System【AppClassLoader】)
用户自定义的类加载器
•java.lang.ClassLoader的子类(还要实现构造方法等挺麻烦的)
1.4.4、类的加载器与类
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
图1.4.4
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟中的唯一性。说通俗一些,比较两个类是否“相等”,只有在两个类是由同一个类加载器的前提之下才有意义,否则,即使这两个类来源于同一个class文件,只要加载它的类加载器不同,那这两个类必定不相等。这里所指的“相等”包括代表类的Class对象的equal方法、isAssignableFrom()、isInstance()方法及instance关键字返回的结果。
类加载器并不需要等到某个类被“首次主动使用”时再加载它。意思就是在使用之前类加载器已经加载了,只是等到我们进行main函数时才提示我们(比如类没有找到什么的)。
JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误( LinkageError错误)。
java.lang
Class LinkageError
java.lang.Object
java.lang.Throwable
java.lang.Error
java.lang.LinkageError
如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
类被加载后,就进入连接阶段。
1.4.5、类的验证(class文件检查器)
类被加载后,就进入连接阶段。连接就是将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。
1.4.5.1、类文件的结构检查(保证正确的内部结构)
是通过class文件检查器保证装载的class文件内容有正确的内部结构,并且这些class文件互相间协调一致。Class文件检查器实现的安全目标之一就是程序的健壮性。如果某个有漏洞的编译器,产生了一个class文件,而这个class文件中包含了一个方法,这个方法的字节码中含有一条跳转到方法之外的指令,那么,一旦这个方法被调用,它将导致虚拟机的崩溃,所以,处于对健壮性的考虑,由虚拟机检验它装载的字节码的完整性非常重要。
这次扫描的目的就是保证这个字节序列正确的定义了一个class。它必须遵从Java的class文件的固定格式,这样它才能被编译成在方法区中的(基于实现的)内部数据结构。
1.4.5.2、语义检查(比如字符串必须符合特定的语法规范)
检查器对每个组成都分进行检查的目的之一,是为了确认每个方法描述符合特定的语法规则,格式正确的字符串。
1.4.5.3、字节码验证(确保字节码流可以被Java虚拟机安全地执行)
字节码流代表Java方法(包括静态方法和实例方法),它是由操作码单字节指令的组成序列,每一个操作数后都跟着一个或者多个操作数。
1.4.5.4、符号引用的验证(必须检查被检测的class文件以外的其他类)
可能引入装载的类,大多数Java虚拟机的实现采用延迟加载类的策略,就是用到了才加载。将符号引用替换为直接引用,就是一个类指向类,字段或者方法的指针或者偏移量。
1.4.5.5、二进制兼容性的验证(确保相互引用之间协调一致)
比如引用的jar包中的类之间方法引用,在Java虚拟机在验证类中的方法,检查在方法区内是否存在这个方法,如果不存在就抛出NoSuchMethodError错误。或者版本不兼容,比如有时低版本的引用到高版本可以会出错,但是Java的版本都是向上兼容的,在高版本引用低版本不会出现这种错误。
1.4.6、类的准备
Java虚拟机为类的静态变量分配内存,并设计默认的初始值。比如int类型的类型静态变量a分配4个字节的内存空间,并赋予默认值为0(都知道int类型的默认值吧)。
1.4.7、类的解析
Java虚拟机会把类的二进制数据中的符号引用替换为直接引用,比如在一个类A中引用到类B的方法,在类A的二进制数据中,包含了这个方法的符号引用,由方法的全名和相关的描述组成。在此阶段会把这个符号引用替换成一个指针,该指针指向类B中的方法在方法区的内存位置。
1.4.8、类的初始化
Java虚拟机执行初始化语句是有先后顺序依次执行,而且是在加载和链接完成后(如果没有加载链接那就进行加载链接),再为类的静态变量赋值赋予初始值。在静态变量的声明中出进行初始化(就是int a=1;初始化值就是1),在静态代码块中进行初始化(int a;而赋值是在static方法里面进行初始化),如果没初始化的变量将保持默认值。
1.5、类的使用方式可分为两种
主动使用
被动使用
所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化它们。
主动使用( 六种)
- 创建类的实例(如new 一个对象)
- 访问某个类或接口的静态变量,或者对该静态变量赋值(类.变量)
- 调用类的静态方法(类.静态方法)
- 反射(如Class.forName(“com.shengsiyuan.Test”))
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类(JavaTest)
• 除了以上六种情况,其他使用Java类的方式都被看作是对类的被看作是对类的被动使用,都不会导致类的初始化。
1.6、方法区
方法区在一个jvm实例的内部,类型信息被存储在一个称为方法区的内存逻辑区中。类型信息是由类加载器在类加载时从类文件中提取出来的,类(静态)变量也存储在方法区中。
—保存装载的类信息
- 类型的常量池 。
- 字段,方法信息 。
- 方法字节码 。
—通常和永久区(Perm)关联在一起
—通常和永久区(Perm)关联在一起
—JDK6时,String等常量信息置于方法 、JDK7时,已经移动到了堆。
1.7、Java堆
堆是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
—和程序开发密切相关。
—应用系统对象都保存在Java堆中 (new对象)。
—所有线程共享Java堆 。
—对分代GC来说,堆也是分代的 。
—GC的主要工作区间 。
1.8、Java栈
线程私有,它的生命周期与线程相同。虚拟机栈描述的是Java 方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。
—线程私有
—栈由一系列帧组成(因此Java栈也叫做帧栈)
—帧保存一个方法的局部变量、操作数栈、常量池指针—每一次方法调用创建一个帧,并压栈
1.9、本地方法栈
本地方法栈(Native MethodStacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。
1.10、PC寄存器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
—每个线程拥有一个PC寄存器。
—在线程创建时创建 。
—指向下一条指令的地址。
—执行本地方法时,PC的值为undefined 。
1.11、本地方法
Java程序通过调用本地方法和主机交互,Java中有两种方法,一是Java方法,二是本地方法。Java方法是有Java语音编写的,编译成字节码文件,存储到class文件中。本地方法是其他语言编写的(c,c++等)编译成和处理器相关的机器代码。本地方法保存在动态的链接库中,格式是各个平台专有的。一个本地方法接口(Java native interface ,JNI),使用本地方法可以在特定的主机系统的任何一个Java平台运行。
Java给提供两个选择,如果希望使用待定主机上的资源,他们又无法从Java API访问,那么可以写入一个平台相关的Java程序来调用本地方法。如果希望保证程序的平台无关性,那么只能通过Java API来访问底层系统资源。
1.12、JVM启动流程
1.13、内存模型
1.14、垃圾回收机制GC
java 语言中一个显著的特点就是引入了java回收机制,是c++程序员最头疼的内存管理的问题迎刃而解,它使得java程序员在编写程序的时候不在考虑内存管理。由于有个垃圾回收机制,java中的额对象不在有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存;
内存泄露:指该内存空间使用完毕后未回收,在不涉及复杂数据结构的一般情况下,java的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有是也将其称为“对象游离”;
1.14.1—算法分析
1.14.2、老式算法
引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
1.14.3、标记-清除算法
是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
1.14.4、标记-压缩算法
适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。
1.14.5、复制算法
与标记-清除算法相比,复制算法是一种相对高效的回收方法 、不适用于存活对象较多的场合 如老年代 。
将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
以上是关于深入JVM内核的主要内容,如果未能解决你的问题,请参考以下文章