面试专题之——JVM
谈谈你对java的理解
1、它是一种面向对象语言,它的特性有:封装、继承、多态、泛型、lambda
2、支持跨平台,编译成.class文件后,再由jvm虚拟机翻译运行
3、垃圾回收器,程序员不用过多关注内存分配和回收
JVM架构是怎么样的?
1、ClassLoader类加载器子系统:加载(双亲委派)、连接、初始化
2、RuntimeData运行时数据区:方法区、堆区、栈区(虚拟机栈)、本地方法栈、寄存器栈
3、ExecutionEngine执行引擎:解释器、编译器、垃圾回收器
4、NativeInterface:本地方法的接口
java8内存模型(运行时数据区):虚拟机栈、本地方法栈、寄存器栈、堆区、方法区
线程私有:虚拟机栈、本地方法栈、寄存器栈
线程共享:堆区、方法区
什么是反射?
在运行的时候,通过类加载器ClassLoder加载.class字节码文件到内存中生成Class对象,然后可以任意获取它的属性和方法,并且对任意对象都能调用它的任意方法和属性
什么是Classloader?
Classloader在java中有非常重要的作用,它主要工作在Class的加载阶段,主要作用是获取Class二进制数据流然后交给jvm虚拟机进行加载、连接、初始化等操作,所有的Class都是由它加载的
什么情况下要开始类的加载?
1、在创建类、获取以及设置静态变量、调用静态方法时加载类
2、对类进行反射调用时
3、初始化一个类时,它的父类还没初始化
4、创建虚拟机时,虚拟机会自动创建main方法所在的类
类加载器的分类有哪些?
Bootstarp ClassLoder:根类加载器,负责java核心类的加载,例如System、String等
Extension ClassLoder:扩展类加载器,负责jar包的加载
System ClassLoder:系统类加载器,负责jvm启动时,把环境变量目录中的类库加载到内存中
Custom ClassLoader:自定义类加载器
它们依次向下继承
什么是类加载器的双亲委派机制?
首先从底向上查询是否加载类(cust > sys > ext > boot),已经加载了就不加载了直接返回,没加载就从上向下加载,依次为:Bootstarp Classloder > Extension ClassLoder > System ClassLoder
为什么要用双亲委派机制去加载类?
避免重复的加载类
类的加载过程是怎么样的?
加载:通过ClassLoder类加载器加载class字节码文件,生成Class对象存放在方法区中
连接:检查class的正确性和安全性、为变量分配空间设置初始值、将常量池中的符号引用转换为直接引用
初始化:执行类变量赋值和静态代码块
LoadClass(getClassLoader获得)和forName的区别
LoadClass是只加载了的类
forName是 加载 连接 初始化 过的类
什么是永久代和元空间,它们的区别是什么?
元空间与永久代:元空间与永久代其实都是方法区的实现,jdk1.7把字符串常量池从方法区移动到了堆中,并且1.8之后元空间替代了永久代,此时常量池存放在元空间中
它们的区别:
1、元空间使用本地内存,而永久代用jvm内存,解决了空间不足的问题
2、字符串常量池在永久代中,容易出现性能问题、内存溢出
3、类和方法的信息大小难确定,不好给永久代指定大小
4、永久代给GC带来了麻烦,回收效率低,因为老年代和永久代无论谁满了,它们一起进行垃圾清理,影响性能,而元空间用本地内存,快用完了不会触发GC
jvm三大性能调优参数-Xms -Xmx -Xss的含义:
-Xss:规定了每个线程虚拟机栈的大小,一半256k够了,影响并发线程数的大小
-Xms:初始堆的大小,如果对象容量超过了这个,那么会根据-Xmx大小扩容
-Xmx:堆能扩展到的最大值,一半-Xms和-Xmx设置成一样,因为在扩容的时候会内存的抖动,影响程序运行
堆和栈有哪些区别?
管理方式:栈自动释放,堆需要GC垃圾回收
空间大小:栈比堆小
碎片相关:栈产生的碎片远小于堆
分配方式:栈能静态分配也能动态分配,堆只能动态分配
效率:栈的效率更高
不同版本之间intern方法有哪些区别?
1.6以及之前:会拷贝一份字符串到常量池中
1.7以及之后:会把在堆内存中的字符串地址拷贝到常量池中
GC垃圾回收机制
对象被判定为垃圾的标准:没有被其他对象引用
如何判断没有被引用?
1、引用计数算法
2、可达性分析算法
引用计数算法:
1、通过判断对象的引用数量来决定对象是否可以被回收
2、每个对象实例都有一个引用计数器,被引用+1,用完-1
3、任何引用计数为0的对象实例可以被当作垃圾收集
优点:执行效率高,程序执行受执行影响小
缺点:无法检测出循环引用的情况,导致内存泄漏。比如:实例A有一个成员变量引用了实例B,实例B又反过来引用了A,那么这两个对象永远不会被垃圾收集了
可达性分析算法:通过判断对象的引用链是否可达决定是否被回收,引用链从GC Root开始向下判断可达性
可作为GC Root的对象:
1、虚拟机栈中引用的对象,比如:在方法中创建一个对象赋值给局部变量,在局部变量没被销毁之前,这个对象为GC Root
2、方法区中的常量引用对象,比如:在类中定义一个常量,保存的是某个对象的地址,那么这个对象为GC Root
3、方法区中的类静态属性引用的对象,和常量差不多
4、本地方法栈中JNI(Native方法)引用的对象
5、活跃线程的引用对象
垃圾回收算法有哪些?
1、标记-清除算法:
标记:从根节点(GC Root)进行扫描,对存活的对象进行标记
清除:从堆内存从头到尾进行线性遍历,回收被标记不可达的对象内存
缺点:产生很多内存碎片
2、复制算法:分为对象面和空闲面,对象在对象面上创建,当对象面快用完了,就把存活的对象复制到空闲面,然后把对象面所有对象清除。
这种算法适用在对象存活率低的地方,适用于年轻代,因为每次复制的对象少,这样可以解决内存碎片化的问题
优点:顺序分配内存,简单高效
3、标记-整理算法:适用于老年代这种存活率高的场景
标记:和标记-清除算法一样
清除:移动所有存活的对象,且按照内存地址依次排序,最后一个存活对象的最后一个地址之后的内存全部回收
与标记-清除算法区别:在清除的时候进行整理,成本更高,但是解决了内存碎片化问题,适合用在老年代
优点:不需要像复制算法那样设置两个内存互换
4、分代收集算法:它是垃圾回收算法的组合拳,按照对象生命周期的不同,划分区域采用不同的垃圾回收算法
年轻代:尽可能快速收集生命周期短的对象
老年代:存放生命周期较长的对象
GC的分类有哪些?什么是年轻代?什么是老年代?
Minor GC:用在年轻代
Full GC:用在老年代
年轻代:它默认占1/3堆空间,分为一个Eden区(默认8/10),和两个Survivor区(默认一个1/10)。
年轻代垃圾回收机制:采用复制算法
1、对象刚创建出来放在Eden区,当Eden区满了就调用Minor GC,把存活的对象年龄+1并且复制到一块Survivor区,把Eden区清空
2、Eden再次满了,就把Eden区存活的对象和之前存活的对象一起放进另一个Survivor区,年龄继续+1,剩下两个区域再次清空
3、当对象年龄到达某个值时,放到老年代中,默认是15,可以通过-XX:MaxTenuringThreshold修改这个值,不过不是绝对的,对于需要分配较大连续内存的对象,并且年轻代装不下,那么直接放进老年代
对象如何晋升到老年代:
1、经历一定数量的Minor GC次数依然存活的对象,默认15次
2、Survivor区放不下的对象
3、新生成的大对象,这个值可以根据-XX:PretenuerSizeThreshold设置
常用性能调优参数
-XX:SurvivorRatio:设置Eden和Survivor的比值,默认8:1
-XX:NewRatio:老年代和年轻代内存大小的比例,值为2代表老年代是年轻代的两倍
-XX:MaxTenuringThreshold:从年轻代晋升到老年代的年龄大小,也就是GC次数的大小
老年代:它默认占用2/3堆空间,它通过标记-清理、标记-整理算法回收垃圾,调用Full GC,只要调用就清理整个堆内存,包括年轻代
Full GC与Minor GC区别:
1、Full GC清理整个堆内存,Minor GC清理年轻代,所以Full GC更慢
2、因为老年代对象不容易死掉,所以调用的不频繁
触发Full GC的条件:
1、老年代空间不足
2、JDK1.7以及之前永久代空间不足
3、调用System.gc(),但调用了也不一定垃圾收集,垃圾收集还是由JVM管理
4、Minor GC要晋升到老年代的对象大小超过了老年代所剩空间
JVM的两种运行模式是哪两种?
Server:启动较慢,但启动完成进入稳定期后,它的速度更快,因为采用的重量级虚拟机,优化更多
Client:启动较快
可以用java -version查询
常用垃圾收集器有哪些?
年轻代常用垃圾收集器:
Serial收集器:-XX:+UseSerialGC
1、采用复制算法
2、单线程收集,进行垃圾收集时必须暂停所有工作线程
3、简单高效,Client模式下默认年轻代收集器
ParNew收集器:-XX:+UseParNewGC
1、采用复制算法
2、多线程收集,其他和Serial收集器一样
3、单核执行效率不如Serial,在多核CPU下执行才有优势,默认开启线程数量和cpu核数相同
Parallel Scavenge收集器:-XX:+UseParallelGC
1、采用复制算法
2、更关注系统的吞吐量,而不是用户线程停顿时间。吞吐量:用户代码运行时间占总运行时间的占比,总运行时间加上垃圾收集停顿时间
3、更佳适合后台运算多,交互任务少的时候,高吞吐量则可以最高效率的利用CPU时间,尽快的完成程序的运算任务
4、在多核下更有优势,它也是Server模式下默认垃圾收集器
-XX:+UseAdaptiveSizePolicy:内存调优任务交给JVM自动调优
老年代常用垃圾收集器:
serial Old收集器:-XX:+UseSerialOldGC
1、采用标记-整理算法
2、单线程收集,收集的时候暂停所有工作线程
3、简单高效,Client模式下默认收集器
parallel Old收集器:-XX:+UseParallelOldGC
1、采用标记-整理算法
2、多线程,吞吐量优先
CMS收集器:-XX:UseConcMarkSweepGC
1、采用标记-清除算法
2、适合硬件配置好的时候,它尽可能缩短收集停顿时间,如果有很多对象存活时间长也适合用它
执行过程:
1、初始标记 CMS initial mark 标记GC Root直接关联到的对象, 需暂停用户线程 – ->速度很快
2、并发标记 CMS concurrent mark 从GC Root进行可达性分析,找出存活的对象,与用户线程并发执行
3、重新标记 CMS remark 修改并发标记,标记出用户程序在并发标记时变动的内容 ,需暂停用户线程 – ->速度很快
4、并发清除 CMS concurrent sweep
优点:并发收集、低停顿(停顿时间短)
缺点:产生大量空间碎片、并发阶段会降低吞吐量
Garbage First收集器(G1收集器):-XX:+UseG1GC
1、采用复制 + 标记-整理算法
2、适用多个cpu并发执行,减少用户停顿时间
3、分代收集
4、空间整合(标记整理算法)
5、可预测的停顿
执行原理:
1、将整个Java堆内存划分成多个大小相等的独立区域Region
2、年轻代和老年代不在物理隔离,它们都是一部分Region的集合
执行过程:
1、初始标记(Initial Marking) 标记一下GC Root能够关联的对象,需要暂停用户线程
2、并发标记(Concurrent Marking) 从GC Root进行可达性分析,找出存活的对象,与用户线程并发执行
3、最终标记(Final Marking) 修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需暂停用户线程
4、筛选回收(Live Data Counting and Evacuation) 对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划
优点:
1、保留了逻辑上的分代收集概念
2、空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
3、可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)
Object的Finalize()方法的作用是否与C++的析构函数作用相同?
finalize()作用:当对象被回收时调用
1、与C++的析构函数不同,析构函数调用确定,而它是不确定的,因为在调用finalize时不确定执行顺序
2、方法执行时随时可能被终止
3、给予对象最后一次重生的机会,只会调用一次
强引用、软引用、弱引用、虚引用:
强引用:从来不会被回收
软引用:内存不足时回收
弱引用:只要垃圾回收就回收
虚引用:未知,昙花一现,生命很短