JVM面试题
1.基本数据类型一定存储在栈中吗?
参照:Java内存区域
基本数据类型是放在栈中还是放在堆中,这取决于基本类型在何处声明。 1.在方法中声明的变量,即该变量是局部变量,每当程序调用方法时,系统都会为该方法建立一个方法栈,其所在方法中声明的变量就放在方法栈中,当方法结束系统会释放方法栈 在方法中声明的变量可以是基本类型的变量,也可以是引用类型的变量。 1.1当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在方法栈中 1.2当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在方法的栈中,该变量所指向的对象是放在堆类存中的。
2.在类中声明的变量是成员变量,也叫全局变量,放在堆中的(因为全局变量不会随着某个方法执行结束而销毁) 同样在类中声明的变量即可是基本类型的变量 也可是引用类型的变量 2.1当声明的是基本类型的变量其变量名及其值放在堆内存中的 2.2当声明的是引用类型时,其声明的变量仍然会存储一个内存地址值,该内存地址值指向所引用的对象。引用变量名和对应的对象仍然存储在相应的堆中
2.什么情况下会发生栈内存溢出
参照:Java内存区域
(1)栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用类型; (2)如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常,方法递归调用产生这种结果; (3)如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出一个OutOfMemory异常; (4)参数-Xss 去调整JVM栈的大小。
3.JVM内存模型简述
参照:Java内存区域
1.程序计数器:当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。 2.Java虚拟栈:存放基本数据类型、对象的引用、方法出口等,线程私有。包括局部变量表、操作数栈、动态链接、方法返回地址 3.Native方法栈:和虚拟栈相似,只不过它服务于Native方法,线程私有。 4.Java堆:java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,线程共享。 5.方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),回收目标主要是常量池的回收和类型的卸载,各线程共享。常量池包含文本字符串、final常量值、基本类型等
4.堆和栈的区别
参照:Java内存区域
栈是运行时单位,代表着逻辑,内含基本数据类型和堆中对象引用,所在区域连续,没有碎片;堆是存储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在区域不连续,会有碎片。 1. 功能不同:栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变量, 还是类变量,它们指向的对象都存储在堆内存中。 2. 共享性不同:栈内存是线程私有的,堆内存是所有线程共有的。 3. 异常错误不同:如果栈内存或者堆内存不足都会抛出异常。栈空间不足:java.lang.StackOverFlowError。堆空间不足:java.lang.OutOfMemoryError。 4. 空间大小:栈的空间大小远远小于堆的
5.JVM组成
参照:Java内存区域
JVM 是 Java Virtual Machine(Java 虚拟机)的缩写,顾名思义它是一个虚拟计算机,也是 Java 程序能够实现跨平台的基础。它的作用是加载 Java 程序,把字节码翻译成机器码再交由 CPU 执行的一个虚拟计算器。 JVM 主要组成部分如下:类加载器(ClassLoader)、运行时数据区(Runtime Data Area)、执行引擎(Execution Engine)、本地库接口(Native Interface)
6.Java对象创建过程
参照:Java内存区域
1.当java字节码遇到一条字节码new指令时,首先检查这个指令的参数是否可以在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化。如果没有,则必须先执行相应的类加载过程。 2.类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可以完全确定,为对象分配空间的任务实际上等同于把一块确定大小的内存从Java堆中划分出来。 3.Java虚拟机还要对对象进行必要的设置。对象头包含两部分信息,第一类是用于存储对象自身运行时数据,如HashCode,GC分代年龄,锁状态标志,线程支持的锁等。对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例
7.JVM内存新生代中为什么要分为Eden和Survivor
(1)如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor; (2)设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1。
这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生。
8.对象分配的规则
(1)对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC; (2)大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存); (3)长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,直到达到阀值对象进入老年区。 (4)动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。 (5)空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
9.什么时候对象可以被收回
引用记数:个对象维护一个引用计数,假设A对象引用计数为零说明没有任务对象引用A对象,那A对象就可以被回收了,但是引用计数有个缺点就是无法解决循环引用的问题。 GC Roots:通过一系列的名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明对象是不可用的。 在Java中,可以作为GC Roots的对象包括下面几种: 1.虚拟机栈中引用的对象; 2.方法区中类静态属性引用的对象; 3.方法区中的常量引用的对象; 4.本地方法栈中JNI(即一般说的Native方法)的引用的对象;
10.垃圾回收器算法
标记清除:分为标记、清除两个阶段。首先先标记出那些对象需要被回收,在标记完成后会对这些被标记了的对象进行回收。缺陷就是会产生内存碎片。 标记整理:标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 复制:将内存分为两块大小一样的区域,每次是使用其中的一块。当这块内存块用完了,就将这块内存中还存活的对象复制到另一块内存中,然后清空这块内存。运行效率很高。但是浪费空间
11.什么是类加载器,类加载器有哪些
参照:虚拟机的类加载机制
实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器。主要有以下四种类加载器: (1)启动类加载器(Bootstrap ClassLoader)用来加载java核心类库(<JRE_HOME>/lib路径下),无法被java程序直接引用; (2)扩展类加载器(extensions class loader):它用来加载java的扩展库。Java 虚拟机的实现会提供一个扩展库目录(<JRE_HOME>/lib/ext路径下)。该类加载器在此目录里面查找并加载java类; (3)系统类加载器(system class loader):它根据java应用的类路径(CLASSPATH)来加载Java类。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它; (4)用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。遵守双亲委派模型:继承ClassLoader,重写findClass()方法;破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。
12.简述java类加载机制
参照:虚拟机的类加载机制
1.加载:通过一个类的全限定名来获取定义此类的二进制字节流,并在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口 2.验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求。包括文件格式验证、元数据验证、字节码验证、符号引用验证 3.准备:为类的变量分配内存,并设置类变量初始值的阶段,这些变量使用的内存,都在方法区中进行分配。这时候进行内存分配的仅包含类变量,而不包含实例变量,实例变量会在对象实例化时随着对象一起分配在堆中。 4.解析:解析阶段是虚拟机将常量池内的符号引用(以一组符号来描述所引用的目标)替换为直接引用(直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄)的过程. 5.初始化:初始化是类加载过程的最后一步,此时虚拟机才开始真正执行类中编写的Java程序代码,将主导权移交给应用程序
13.类加载器双亲委派模型机制
参照:虚拟机的类加载机制
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。 采用双亲委派的好处是使用不同的类加载器最终得到的都是同样一个Object对象:可以避免重复加载,父类已经加载了,子类就不需要再次加载
14.调优命令
jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。 jstat:JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。 jmap:JVM Memory Map命令用于生成heap dump文件 jhat:JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看 jstack:用于生成java虚拟机当前时刻的线程快照。 jinfo:JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。
15.String 对象的两种创建方式
String t = "abc";--abc对象会直接存储在常量池中 String t = new("abc"); --new出来的对象会存储在堆中
16.主要的JVM参数
堆栈配置相关: java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:MaxPermSize=16m -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxTenuringThreshold=0 -Xmx3550m: 最大堆大小为3550m。 -Xms3550m: 设置初始堆大小为3550m。 -Xmn2g: 设置年轻代大小为2g。 -Xss128k: 每个线程的堆栈大小为128k。 -XX:MaxPermSize: 设置持久代大小为16m -XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。 -XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6 -XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。 垃圾收集相关: -XX:+UseParallelGC: 选择垃圾收集器为并行收集器。 -XX:ParallelGCThreads=20: 配置并行收集器的线程数 -XX:+UseConcMarkSweepGC: 设置年老代为并发收集。 -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。 -XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片
17.JAVA8 与元数据
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于: 元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
类的元数据放入nativememory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。
18.java对象结构
Java对象由三个部分组成:对象头、实例数据、对齐填充。 对象头:由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。 实例数据:用来存储对象真正的有效信息(包括父类继承下来的和自己定义的) 对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)