Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。
本文再次更新,经过*里**面试之后,觉得自己理解还是不是很好,有可能紧张(我连类加载器都忘记提了,心塞),有重新找资料书等。如果不嫌弃的话,可以下载我的整理版看看(JVM整理文档链接)。
1.1、Java体系结构包括四个独立的方面:
Java面向网络的核心就是Java虚拟机,它支持Java面向网络体系结构三大支柱的所有方面:平台无关性,完全性和网络移动性。
- Java程序设计语言
- Java class文件格式
- Java应用编程接口(Java API)
- Java虚拟机
1.2、Java程序的执行过程
1.3、加载.class文件的方式
- 从本地系统中直接加载
- 通过网络下载.class文件(URLClassLoader)
- 从zip, jar等归档文件中加载.class文件
- 从专有数据库中提取.class文件(很少用)
- 将Java源文件动态编译为.class文件
1.4、JVM内部结构
1.4.1、JVM与程序的生命周期
- 执行了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程序员提供了访问方法区内的数据结构的接口。 类加载器并不需要等到某个类被“首次主动使用”时再加载它。意思就是在使用之前类加载器已经加载了,只是等到我们进行main函数时才提示我们(比如类没有找到什么的)。
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文件中包含了一个方法,这个方法的字节码中含有一条跳转到方法之外的指令,那么,一旦这个方法被调用,它将导致虚拟机的崩溃,所以,处于对健壮性的考虑,由虚拟机检验它装载的字节码的完整性非常重要。
1.4.5.3、字节码验证(确保字节码流可以被Java虚拟机安全地执行)
字节码流代表Java方法(包括静态方法和实例方法),它是由操作码单字节指令的组成序列,每一个操作数后都跟着一个或者多个操作数。
1.4.6、类的准备
Java虚拟机为类的静态变量分配内存,并设计默认的初始值。比如int类型的类型静态变量a分配4个字节的内存空间,并赋予默认值为0(都知道int类型的默认值吧)。1.4.7、类的解析
1.4.8、类的初始化
Java虚拟机执行初始化语句是有先后顺序依次执行,而且是在加载和链接完成后(如果没有加载链接那就进行加载链接),再为类的静态变量赋值赋予初始值。在静态变量的声明中出进行初始化(就是int a=1;初始化值就是1),在静态代码块中进行初始化(int a;而赋值是在static方法里面进行初始化),如果没初始化的变量将保持默认值。1.5、类的使用方式可分为两种
主动使用被动使用
所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化它们。
• 除了以上六种情况,其他使用Java类的方式都被看作是对类的被看作是对类的被动使用,都不会导致类的初始化。
- 创建类的实例(如new 一个对象)
- 访问某个类或接口的静态变量,或者对该静态变量赋值(类.变量)
- 调用类的静态方法(类.静态方法)
- 反射(如Class.forName(“com.shengsiyuan.Test”))
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类(JavaTest)
1.6、方法区
—保存装载的类信息
- 类型的常量池 。
- 字段,方法信息 。
- 方法字节码 。
—通常和永久区(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、复制算法
与标记-清除算法相比,复制算法是一种相对高效的回收方法 、不适用于存活对象较多的场合 如老年代 。
将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。