JVM面试总结
1.class 文件格式
2. 垃圾回收器的相关参数
3. JVM内存分配与回收
堆空间的基本结构
最近看jvm时遇到了“字面量”和“符号引用”这两个概念,它们被存放在运行时常量池(方法区的一部分),看了一些博客以后对这两个概念有了初步认识。
字面量可以理解为实际值,int a = 8中的8和String a = "hello"中的hello都是字面量
符号引用就是一个字符串,只要我们在代码中引用了一个非字面量的东西,不管它是变量还是常量,它都只是由一个字符串定义的符号,这个字符串存在常量池里,类加载的时候第一次加载到这个符号时,就会将这个符号引用(字符串)解析成直接引用(指针)。
直接内存Direct Memory
不是JVM运行时数据区的一部分。用于管理堆外内存
4. 垃圾收集算法?都有什么特点?
MinorGC = YoungGC (年轻代) MajorGC = FullGC(老年代)一般MajorGC要比MinorGC慢10倍以上
标记-清除
首先标记出不需要回收的对象,标记完成后统一回收所有没有标记的对象。(1,效率问题。2,空间问题。标记清楚后会产生大量不连续的碎片)
标记-整理
和标记清除的标记一样,后续不是直接对可回收对象回收,而是让所有存活的对象向另一端移动,然后清理掉端边界以外的内存。
复制
为了解决效率问题。将内存分为大小相同的两块,每次使用其中的一块。当第一块内存使用完成后,就将还存活的对象复制到另一块,然后把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
5. 分代收集算法
1)当前虚拟机的垃圾收集都采⽤分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为⼏块。⼀般将 java 堆分为新⽣代和⽼年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
2) ⽐如在新⽣代中,每次收集都会有⼤量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。⽽⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集 。(HotSpot分为新生代和老年代就是因为上述原因。—》提升垃圾回收的效率)
新生代中为什么要有两个survivor区域
1)为什么要有survivor
当新生代发生minor GC时,可以将存活的对象转移到survivor中,直到存活的对象old enough再转移到老年代中,从而减小进入老年代对象的数量,减少发生Full GC的次数。
2)为什么要有两个survivor
假如只有一个survivor
如果只有一个survivor,当Eden区满的时候,发生一个minor gc,如果有存活对象,将存活对象转移到survivor中,那么下次Eden区再满的时候,再次minor gc,这时Eden区和Survivor都有存活的对象(都部分被清空)此时空间是不连续的。
3) 两个survivor
如果存在两个survivor区,当Eden区满的时候,发生minor gc,有存活对象,将对象转移到Survivor0中,当下次再发生minor gc的时候,将Eden区和S0区的存活对象复制到S1中(这种复制算法可以保证S1中来自Eden和S0中对象的地址是连续的),清空Eden区和S0的空间,然后交换S0和S1的角色,之后发生minor gc时,循环往复。直到存活对象old enough,升入老年代。
6. 垃圾回收器(具体实现)
1)Serial:工作在年轻代,单线程GC算法。
stop-the-world(STW)所有的线程都必须停止,等着垃圾回收器回收垃圾完成后才开始运行。
2)Parallel Scavenge:工作在年轻代,多线程GC算法。
多线程GC算法,多个线程同时进行垃圾回收。(单个线程忙不过来)
并行:多个垃圾回收线程同时执行。
3)ParNew:工作在年轻代。
和parallel scavenge的区别就是ParNew配合CMS使用。
4)CMS(concurrent mark sweep):工作在年轻代时和parNew一样,老年代。
并发:垃圾回收线程和任务执行线程同时执行。降低STW的时间
四个过程(注意:标记的是不被回收的对象)
(1)初始化标记
(2)并发标记(任务执行线程和垃圾回收线程同时运行,此时某个任务执行线程有可能变为垃圾,从而产生漏标或者错标)
(3)重新标记
(4)清除
-
JVM调优(调参数)
-
Java对象的创建过程
对象在内存中的布局
Object o = new Object()在内存中占了多少个字节
如果JVM开启了class pointer压缩,则class pointer占4个字节。加上markword 8个字节,此时没有成员变量,则instance data为0,4+8=12不能被8整除,不上padding4个字节,一共16个字节。
如果如果JVM没有开启class pointer压缩,则class pointer占8个字节。加上markword 8个字节,此时没有成员变量,则instance data为0,8+8=16能被8整除,一共16个字节。-
类加载检查
虚拟机根据new关键字对应的指令去常量池中定位类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须执行相应的类加载过程。 -
分配内存
在类加载检查通过后,虚拟机将为新生对象分配内存。所需内存大小在类加载完成后就确定了,为对象分配内存就是从java堆中分配出一块空闲区域出来。
分配方式
指针碰撞
适用于堆内存规整的情况下
原理:用过的内存全部整合到一边,没有用过的放另一边,中间有一个分界值指针。划分内存的时候只需要向着没用过的内存方法移动对象内存大小位置即可。
GC收集器:Serial、ParNew
空闲列表
适合堆内存不规整的情况
原理:虚拟机维护一个列表,该列表中会记录哪些内存块是可用的。再分配的时候,找一块足够大的内存块划分给对象实例,最后更新列表记录。
GC收集器:CMS内存分配并发问题:内存分配的过程中需要保证线程安全
CAS+失败重试
CAS是乐观锁的一种实现方式。核心思想就是持有乐观态度,每次对于资源的访问都假设该资源可以被访问,如果冲突失败,可重试到成功为止。
*TLAB(*Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。每个线程在伊甸区Eden自己的一亩三分地。)可以通过-XX:+/-UserTLAB
为每一个线程预先在Eden区(堆中的一块区域)分配一块内存。JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB的剩余内存或TLAB的内存已经用尽时,在采用上述CAS进行内存分配。 -
初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值 。这⼀步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。
-
设置对象头
初始化零值完成之后, 虚拟机要对对象进⾏必要的设置,例如这个对象是哪个类的实例(class pointer)、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息 -
执行init()方法
-
-
对象访问定位有哪两种方式
对象的创建就是为了使用,Java程序通过栈上的reference数据操作堆上的具体数据。对象的访问由虚拟机实现而定。
句柄:稳定的句柄地址,在对象被移动时(垃圾收集时会移动对象)只会改变句柄中的实例数据指针,⽽ reference 本身不需要修改 。
直接指针:接指针访问⽅式最⼤的好处就是速度快,它节省了⼀次指针定位的时间开销 。
-
如何判断对象已经死亡(如何定位一个垃圾)
堆中几乎存放着所有创建的对象,对堆垃圾回收前的第一步就是判断哪些对象已经死亡。
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用,计数器加一。当引用失效,计数器减一。计数器为0的对象就是不可能再被使用的。(无法解决循环引用的问题)
根可达算法
通过一系列的名为“GCRoots”的对象作为起始点,从这些节点开始向下搜索,搜索过的路径称为引用链,当一个对象到GCRoots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明对象是不可用的。从根节点开始到达不了的都是垃圾。
GCroots都包括那些东西
(1) JVM stack 存放对象引用,通过引用可以找到被引用对象的地址。
(2) native method stack:本地方法栈中引用的对象,c/c++语言编写
(3) 方法区引用的对象
run-time constant pool:运行时常量池引用的变量,方法区的一部分(⽤于存放编译期⽣成的各种字⾯量和符号引⽤ )
static references in method area:方法区内的静态属性引用的对象
(4) clazz:load进入的class对象 -
判定一个类是不是无用的类
该类所有的实例都被回收
加载该类的ClassLoad已经被回收
该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 -
JVM回收策略
对象优先分配在Eden区,当Eden区没有足够的空间时,虚拟机将发起一次MinorGC,这时会把存活的对象转移进Survivor区。大对象(需要占用大量连续空间的java对象)直接进入老年代
目的是避免在Eden区及两个Survivor区之间发生大量的内存复制长期存活的对象放入老年代
(看对象的age大小,没发生一次GC,age加一)担保机制
进行MinorGC之前,JVM首先检查老年代最大的连续可用空间是否大于新生代所有对象的总空间。
如果条件成立,则MinorGC是安全的。
如果不是,则JVM会查看HandlePromotionFailure设置值是否允许担保失败。
如果允许,会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小。如果大于,将尝试进行一次MinorGC,尽管是有风险的。如果小于,或者HandlePromotionFailure设置不允许风险,则要进行一次FullGC。 -
简述类的加载过程(当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化三步来实现对这个类进行初始化。)
加载
将class文件读入内存,并为之创建一个Class对象。任何类被使用时系统都会建立一个Class对象。
首先 类加载器通过类的全路径限定名读取类的二进制字节流,然后 将二进制字节流代表的类结构转化到运行时数据区的 方法区中,最后 在jvm堆中生成代表这个类的java.lang.Class实例连接(验证、准备、解析)
验证(验证二进制字节流代表的字节码文件是否合格,主要从一下几方面判断:)
文件格式:经过文件格式验证之后的字节流才能进入方法区分配内存来存储。
元数据验证:是否符合java语言规范
字节码验证:数据流和控制流的分析
符号引用验证:符号引用转化为直接引用时(解析阶段),检测对类自身以外的信息进行存在性、可访问性验证
准备:负责为类的静态成员分配内存,并设置默认初始化值。
解析:将常量池内的符号引用替换为直接引用的过程初始化
才真正开始执行java代码,静态代码块和设置变量的初始值为程序员设定的值 -
类加载时机
当Java程序首次通过下面6种方式使用某个类或接口时,系统会初始化该类或接口
1. 创建类的实例。
2. 访问类的静态变量,或者为静态变量赋值。
3. 调用类的静态方法。
4. 初始化某个类的子类。
5. 直接使用java.exe命令来运行某个主类。
6. 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。类加载器的概述和分类
Bootstrap ClassLoader 根类加载器
Extension ClassLoader 扩展类加载器
System ClassLoader 系统类加载器类加载器的作用
Bootrap ClassLoader
被称为引导(也称为原始或跟)类加载器,它负责加载Java的核心类。跟类加载器不是java.lang.ClassLoader的子类,而是JVM自身实现的。在JDK中JRE的lib目录下rt.jar文件中。
Extension ClassLoader
负责加载JRE拓展目录中的JAR包的类,它的父类加载器是跟类加载器。在JDK中JRE的lib目录下ext目录。
System ClassLoader
负责在JVM启动时加载来自Java命令的classpath选项、java.class,path系统属性,或CLASSPATH指定的jar包和类历经。系统可通过ClassLoader的静态方法或区该系统类加载器。如果没有特别指定,则用户自定义的类加载器都已类加载器作为父加载器。 -
逃逸分析
是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上。
如果一个对象的指针被多个方法或者线程引用时,那么我们就称这个对象的指针发生了逃逸。
优点
栈上分配,可以降低垃圾收集器运行的频率。
同步消除,如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。
标量替换,把对象分解成一个个基本类型,并且内存分配不再是分配在堆上,而是分配在栈上。这样的好处有,一、减少内存使用,因为不用生成对象头。二、程序内存回收效率高,并且GC频率也会减少。 -
双亲委派及类的加载
定义:当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。作用
避免类的重复加载
保证核心.class不能被篡改