在作为中高程序员的面试中,JVM是必备的基础知识,但很多童鞋会怀疑。 即使我完全不知道这些,我也一样顺利地完成了很多项目。 在正常工作中,必须承认,在小公司里,只根据要求编写代码和修复错误是没有机会接触JVM的。 因为这些知识的应用对于普通程序员来说,遇到大项目总是有高度和高度的。 如果在生产中遇到奇怪的问题,像往常一样纠正错误,逻辑思维和解决方法不顺利的时候,熟悉JVM相关知识,就可能会突破问题。 如果你想成为中高级以上的程序员,请继续往下看。
一.常见的JVM面试主题
1、介绍Java存储器区域(运行时数据区域)吗?
2、Java对象的创建流程?
3、对象访问定位的两种方式?
4、内存型号和分区需要在每个分区中放入什么?
5、什么是JVM?
面试问题的种类很多,所以这里不列举。 万变不离其宗。 首先要弄清楚JVM的基础逻辑和结构,然后这些就是小菜一碟。 一定会得到的吧。
二. Java虚拟机的结构
1、每个JVM有两种机制。
类加载子系统:加载具有相应名称的类或接口
执行引擎(负责执行加载的类或接口中包含的指令)
每个JVM包括以下内容:
方法区域、Java堆、Java堆栈、本地方法堆栈、指令计数器和其他隐式寄存器
每个JVM有两种机制。
类加载子系统:加载具有相应名称的类或接口
执行引擎(负责执行加载的类或接口中包含的指令)
每个JVM包括以下内容:
方法区域、Java堆、Java堆栈、本地方法堆栈、指令计数器和其他隐式寄存器
对于JVM的学习,我认为这些部分是最重要的:
编译和运行Java代码的整个过程
JVM内存管理和垃圾回收的工作原理
对这些部分分别进行说明。
2、编译和运行Java代码的整个过程
如上所述,编译和运行Java代码的整个过程是:开发人员创建一个Java代码. Java文件,将其编译为字节代码. class文件,然后将字节代码加载到内存中并放入虚拟机
)1) Java代码的编译由Java源代码编译器完成。 这意味着从Java代码到JVM字节代码(.class文件)的过程。 流程图如下。
)2) Java字节码的执行由JVM执行引擎进行,流程图如下。
编译和运行Java代码的整个过程包含三个重要机制:
Java源代码的编译机制
班级装载机构
班级执行机制
(1) Java源代码的编译机制
编译Java源代码由三个进程组成:
分析和输入符号表
注释处理
语义分析和生成class文件
流程图如下。
最后生成的class文件由以下部分组成:
结构信息)包括class文件格式的版本号和各部分的数量和大小信息
元数据:与Java源代码中的声明和常量相对应的信息。 包括类/继承的超类/实现的接口的声明信息、域和方法的声明信息以及常量池
方法信息)与Java源代码中的语句和表达式相对应的信息。 包括字节代码、异常处理器表、评估堆栈和局部变量区域大小、评估堆栈类型记录、调试符号信息
)2)班级加载机制
JVM的类加载由ClassLoader及其子类完成,可以在下图中说明类的层次关系和加载顺序。
bootstrap类加载器
负责加载$JAVA_HOME中的jre/lib/rt.jar中的所有class,并由c而不是ClassLoader子类实现
扩展类加载器
负责加载java平台中的扩展功能的jar包,包括在$JAVA_HOME中由jre/lib/*.jar或-Djava.ext.dirs指定的目录中找到的jar包
app类加载器
负责描述在classpath中指定的jar包和目录中的class
自定义类加载器
ClassLoader是由APP应用程序(如tomcat和jboss )定制的,用于根据j2ee规范实现自己的ClassLoader
加载过程中首先检查是否加载了类。 检查顺序为自下而上,从Custom ClassLoader到BootStrap ClassLoader进行分层检查。 如果加载了classloader,则会将类视为已加载,并确保所有classloader只加载一次。 加载顺序为自上而下,也就是从上层逐层尝试这样的加载。
)3)班级执行机制
JVM是基于堆栈的虚拟机。 JVM为每个新创建的线程分配堆栈。 也就是说,对Java程序来说,它是通过堆栈操作执行的。 堆栈以帧为单位保存线程的状态。 JVM只对堆栈进行两种操作:帧单位的堆栈操作和堆栈操作。
JVM执行class字节码,线程创建后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。栈的结构如下图所示:3、JVM内存管理及垃圾回收机制
JVM内存结构分为:方法区(method),栈内存(stack),堆内存(heap),本地方法栈(java中的jni调用),结构图如下所示:
(1)堆内存(heap)
所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。
操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的delete语句才能正确的释放本内存空间。但由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。这时由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,它不是在堆,也不是在栈,而是直接在进程的地址空间中保留一块内存,虽然这种方法用起来最不方便,但是速度快,也是最灵活的。堆内存是向高地址扩展的数据结构,是不连续的内存区域。由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
(2)栈内存(stack)
在Windows下, 栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是固定的(是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。 由系统自动分配,速度较快。但程序员是无法控制的。
堆内存与栈内存需要说明:
基础数据类型直接在栈空间分配,方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收。引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量 。方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完成后从栈空间回收。局部变量new出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC回收。方法调用时传入的literal参数,先在栈空间分配,在方法调用完成后从栈空间收回。字符串常量、static在DATA区域分配,this在堆空间分配。数组既在栈空间分配数组名称,又在堆空间分配数组实际的大小。
如:
(3)本地方法栈(java中的jni调用)
用于支持native方法的执行,存储了每个native方法调用的状态。对于本地方法接口,实现JVM并不要求一定要有它的支持,甚至可以完全没有。Sun公司实现Java本地接口(JNI)是出于可移植性的考虑,当然我们也可以设计出其它的本地接口来代替Sun公司的JNI。但是这些设计与实现是比较复杂的事情,需要确保垃圾回收器不会将那些正在被本地方法调用的对象释放掉。
(4)方法区(method)
它保存方法代码(编译后的java代码)和符号表。存放了要加载的类信息、静态变量、final类型的常量、属性和方法信息。JVM用持久代(Permanet Generation)来存放方法区,可通过-XX:PermSize和-XX:MaxPermSize来指定最小值和最大值。
垃圾回收机制
堆里聚集了所有由应用程序创建的对象,JVM也有对应的指令比如 new, newarray, anewarray和multianewarray,然并没有向 C++ 的 delete,free 等释放空间的指令,Java的所有释放都由 GC 来做,GC除了做回收内存之外,另外一个重要的工作就是内存的压缩,这个在其他的语言中也有类似的实现,相比 C++ 不仅好用,而且增加了安全性,当然她也有弊端,比如性能这个大问题。
编译后在命令行模式下键入: java HelloApp run virtual machine
将通过调用HelloApp的方法main来启动java虚拟机,传递给main一个包含三个字符串"run"、“virtual”、"machine"的数组。现在我们略述虚拟机在执行HelloApp时可能采取的步骤。
开始试图执行类HelloApp的main方法,发现该类并没有被装载,也就是说虚拟机当前不包含该类的二进制代表,于是虚拟机使用ClassLoader试图寻找这样的二进制代表。如果这个进程失败,则抛出一个异常。类被装载后同时在main方法被调用之前,必须对类HelloApp与其它类型进行链接然后初始化。链接包含三个阶段:检验,准备和解析。检验检查被装载的主类的符号和语义,准备则创建类或接口的静态域以及把这些域初始化为标准的默认值,解析负责检查主类对其它类或接口的符号引用,在这一步它是可选的。类的初始化是对类中声明的静态初始化函数和静态域的初始化构造方法的执行。一个类在初始化之前它的父类必须被初始化。整个过程如下: