JVM面试总结(一)
JVM是面试必问的模块,整个JVM我个人感觉可以分为内存模型、类加载机制、GC垃圾回收和性能优化四个大块;
1、Java 是如何实现跨平台的?
我们写的 Java 源码,编译后会生成一种 .class 文件,称为字节码文件。Java 虚拟机(JVM)就是负责将字节码文件翻译成特定平台下的机器码然后运行,也就是说,只要在不同平台上安装对应的 JVM,就可以运行字节码文件,运行我们编写的 Java 程序。
而这个过程,我们编写的 Java 程序没有做任何改变,仅仅是通过 JVM 这一 “中间层” ,就能在不同平台上运行,真正实现了 “一次编译,到处运行” 的目的。
接下来就会问JVM是什么,由什么组成,怎么运行的?
2、JVM是什么?(谈谈你对JVM的理解)
JVM,即 Java Virtual Machine,Java 虚拟机。它通过模拟一个计算机来达到一个计算机所具有的的计算功能。JVM 能够跨计算机体系结构来执行 Java 字节码,主要是由于 JVM 屏蔽了与各个计算机平台相关的软件或者硬件之间的差异,使得与平台相关的耦合统一由 JVM 提供者来实现。
3、为什么可以跨平台?
1、Java 文件经过编译后生成和平台无关的. class 文件。
2、Java虚拟机(JVM)是不跨平台的,Java工具会把统一的.class文件,加载到对应的JVM,又因为该JVM是和这个系统是对应的,所以就可以运行。
4、JVM由什么组成的?
JVM 主要由四大部分组成:ClassLoader(类加载器),Runtime Data Area(运行时数据区或者内存分区),Execution Engine(执行引擎),Native Interface(本地库接口);

1、ClassLoader(类加载器):负责加载字节码文件即 class 文件,class 文件在文件开头有特定的文件标示,并且 ClassLoader 只负责class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。
2、Runtime Data Area(运行时数据区或者内存分区):是存放数据的,分为五部分:Stack(虚拟机栈),Heap(堆),Method Area(方法区),PC Register(程序计数器),Native Method Stack(本地方法栈)。几乎所有的关于 Java 内存方面的问题,都是集中在这块。
3、Execution Engine(执行引擎):也叫 Interpreter;Class 文件被加载后,会把指令和数据信息放入内存中,Execution Engine 则负责把这些命令解释给操作系统,即将 JVM 指令集翻译为操作系统指令集。
4、Native Interface(本地库接口):负责调用本地接口的。他的作用是调用不同语言的接口给 JAVA 用,他会在 Native Method Stack 中记录对应的本地方法,然后调用该方法时就通过 Execution Engine 加载对应的本地 lib。
运行时数据区按照Java虚拟机规定分为以下5个部分:
1、程序计数器:通过改变计数器的值,来选取下一条需要执行的字节码指令。
2、Java虚拟机栈:用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
3、本地方法栈:为虚拟机调用Native方法服务的。
4、Java堆(线程共享):Java虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存,也是垃圾收集器管理的主要区域,因此也被称为GC堆。
5、方法区(线程共享):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据,存在一个叫运行时常量池的区域,用来存放编译器生成的各种字面量和符号引用。
5、JVM怎么运行的?(JVM的工作流程)
JVM的启动过程分为如下四个步骤:
1、JVM的装入环境和配置
java.exe负责查找JRE,并且它会按照如下的顺序来选择JRE:
自己目录下的JRE;
父级目录下的JRE;
查注册中注册的JRE。
2、装载JVM
通过第一步找到JVM的路径后,Java.exe通过LoadJavaVM来装入JVM文件。LoadLibrary装载JVM动态连接库,然后把JVM中的到处函数JNI_CreateJavaVM和JNI_GetDefaultJavaVMIntArgs 挂接到InvocationFunction 变量的CreateJavaVM和GetDafaultJavaVMInitArgs 函数指针变量上。JVM的装载工作完成。
3、初始化JVM,获得本地调用接口
调用InvocationFunction -> CreateJavaVM,也就是JVM中JNI_CreateJavaVM方法获得JNIEnv结构的实例。
4、运行Java程序
JVM运行Java程序的方式有两种:jar包 与 class。
运行jar 的时候,java.exe调用GetMainClassName函数,该函数先获得JNIEnv实例然后调用JarFileJNIEnv类中getManifest(),从其返回的Manifest对象中取getAttrebutes("Main-Class")的值,即jar 包中文件:META-INF/MANIFEST.MF指定的Main-Class的主类名作为运行的主类。之后main函数会调用Java.c中LoadClass方法装载该主类(使用JNIEnv实例的FindClass)。运行Class的时候,main函数直接调用Java.c中的LoadClass方法装载该类。
6、Java程序是怎么运行的?
Java程序从源文件创建到程序运行要经过两大步骤:
1、源文件由编译器编译成字节码(ByteCode);
2、字节码由java虚拟机解释运行。
第一步(编译): 创建完源文件之后,程序会先被编译为.class文件。Java编译一个类时,如果这个类所依赖的类还没有被编译,编译器就会先编译这个被依赖的类,然后引用,否则直接引用。(如果java编译器在指定目录下找不到该类所其依赖的类的.class文件或者.java源文件的话,编译器话报“cant find symbol”的错误。)
编译后的字节码文件格式主要分为两部分:常量池和方法字节码。常量池记录的是代码出现过的所有token(类名,成员变量名等等)以及符号引用(方法引用,成员变量引用等等);方法字节码放的是类中各个方法的字节码。
第二步(运行):Java类运行的过程大概可分为两个过程:1、类的加载 2、类的执行。需要说明的是:JVM主要在程序第一次主动使用类的时候,才会去加载该类。也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。 下面是程序运行的详细步骤:1、在编译好Java程序得到MainApp.class文件后,在命令行上敲Java AppMain。系统就会启动一个JVM进程,JVM进程从classpath路径中找到一个名为MainApp.class的二进制文件,将MainApp的类信息加载到运行时数据区的方法区内,这个过程叫做MainApp类的加载。2、然后JVM找到AppMain的主函数入口,开始执行main函数。3、main函数的第一条命令是Animal animal = new Animal("Puppy");就是让JVM创建一个Animal对象,但是这时候方法区中没有Animal类的信息,所以JVM马上加载Animal类,把Animal类的类型信息放到方法区中。4、加载完Animal类之后,Java虚拟机做的第一件事情就是在堆区中为一个新的Animal实例分配内存, 然后调用构造函数初始化Animal实例,这个Animal实例持有着指向方法区的Animal类的类型信息(其中包含有方法表,Java动态绑定的底层实现)的引用。5、当使用animal.printName()的时候,JVM根据animal引用找到Animal对象,然后根据Animal对象持有的引用定位到方法区中Animal类的类型信息的方法表,获得printName()函数的字节码的地址。6、开始运行printName()函数。
概况来说
1、写好的 Java 源代码文件经过 Java 编译器编译成字节码文件;
2、通过类加载器加载到内存中,被实例化;
4、然后到 Java 虚拟机中解释执行;
5、最后通过操作系统操作 CPU 执行获取结果。
7、 JDK、JRE和JVM之间的关系
JDK:Java Development Kit,Java 开发工具包 - 开发人员进行 Java 软件开发测试的一套工具
JRE:Java Runtime Environment,Java 运行时环境 - Java 软件成品运行所依赖的环境
JVM:Java Virtual Machine,Java 虚拟机 - Java 语言实现跨平台运行的一种软件
三者的关系是:JDK包含JRE,JRE包含JVM
8、JVM内存分布
1、程序计数器
程序计数器:程序计数器就是临时记录方法运行到哪一行了,程序运行实际并不存在并行,而是不同的线程不断的抢占cpu,然后执行一段时间,又重新开始竞争,只是执行时间太短,导致人根本感受不到,当一个方法运行到某一行的时候开始重新竞争了,就需要记录下当前方法运行到哪了,用来下次cpu被该方法抢占时,从上次中断的地方继续执行。
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存
2、Java虚拟机栈(Java栈)
线程私有,生命周期和线程,每个方法在执行的同时都会创建一个 栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程;栈里面存放着各种基本数据类型和对象的引用;
3、本地方法栈
Java 中有些代码的实现是依赖于其他非 Java 语言的(C++),本地方法栈存储的是维护非 Java 语句执行过程中产生的数据,一般我们认为本地方法栈不会出现内存的问题;本地方法栈则是为虚拟机使用到的本地(Native)方法服务。也是线程私有的。
4、Java堆(堆)
方法区存放着class信息,而堆中存放了实例化的对象,同一个类的对象可以被实例化多次,对象是可以被其他线程使用的,所以堆也是共享数据区。实例化对象时,对象中有一个对象头,其中有个类型指针会指向方法区的类元信息。
Java堆是程序员需要重点关注的一块区域,因为涉及到内存的分配(new关键字,反射等)与回收(回收算法,收集器等);
5、方法区
方法区:也叫永久区,用于存储已经被虚拟机加载的类信息,常量("zdy","123"等),静态变量(static变量)等数据。 (jdk1.8已经将方法区去掉了,将方法区移动到直接内存)
在 Java8 之后,我们把方法区称之为元空间(MetaSpace),方法区在逻辑上属于堆
的一部分,但一些具体机制和堆有所区别,如:一些 JVM 的方法区是可以不进行垃圾回收
的,关闭 JVM 时才会释放方法区内存。所以方法区还有一个别名叫非堆,目的是和堆分开。
方法区会存储类信息、静态变量、常量(JDK8 之后不存放字符串常量)、本地机器指 令。
如果加载大量 class 文件,也会造成方法区内存溢出,如一个 tomcat 运行 20~30 个 项目。
6、运行时常量池
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面("abc","123"等)和符号引用。
7、 直接内存:
直接内存:不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域;
1、如果使用了NIO,这块区域会被频繁使用,在Java堆内可以用directByteBuffer对象直接引用并操作;
2、这块内存不受Java堆大小限制,但受本机总内存的限制,可以通过MaxDirectMemorySize来设置(默认与堆内存最大值一样),所以也会出现OOM异常;
9、扩展
一般重点会问你,类放在哪,常量,变量的位置,线程共享的内存和独有的内存;
Java中new处的对象存放在Java堆中,而对象的引用存放在虚拟机栈中。
Java中的Class也是一个类,所以Class对象也存放在堆当中,存放在方法区当中的是类的元数据,即类加载器从class文件中提取出来的类型信息、方法信息、字段信息等。
jdk1.7静态变量存放在方法区中
jdk1.8以后类型信息,字段,方法,常量,保存在本地内存的元空间(方法区),但字符串常量池,静态变量仍在堆,new申请的内存是在堆中。
1、堆和栈功能上的区别:
以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量(int、short、long、byte、float、double、boolean、char等)以及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放;
而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中;
2、堆和栈在线程共享和线程私有区别
栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
栈的内存要远远小于堆内存,栈的深度是有限制的,如果递归没有及时跳出,很可能发生StackOverFlowError问题。
可以通过-Xss选项设置栈内存的大小( 这个参数是设定单个线程的栈空间)。-Xms选项可以设置堆的开始时的大小,-Xmx选项可以设置堆的最大值。
以上内容仅供参考,请合理利用搜索引擎!