什么是JVM?
- JVM(Java Virtual Machine的简称,意为Java虚拟机)
虚拟机又是啥?
指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。常见的虚拟机有:JVM、VMwave、VirtualBox
JVM与VMwave、Virtual Box的区别
VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器
JVM则是通过软件模拟Java字节码的指令集,JVM中只是保留了PC寄存器,其他的寄存器都进行了裁剪,JVM是一台被制定过的现实中不存在的计算机
Java内存区域
运行时数据区域
线程私有区域:程序计数器、Java虚拟机栈、本地方法栈
线程共享区域:Java堆、方法区、运行时常量池
1、程序计数器(线程私有)
概念:程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器
如果当前线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器为空。
#什么叫做线程私有?
由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器(多核处理器则指的是一个内核))都只会执行一条线成中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各线程之间计数器互不影响,独立存储,我们把类似这类区域称之为“线程私有”的内存
2、Java虚拟机栈(线程私有)
虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈和出栈的过程
#什么是局部变量表?
存放了编译器可知的各种基本数据类型(8八大基本数据类型)、对象引用。局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量完全是确定的,在执行期间不会改变局部变量表大小
有可能产生两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的深度(-Xss设置栈容量),将会抛出StackOverFlowError异常。
- 虚拟机在动态扩展时无法申请到足够的内存,会抛出OOM(OutOfMemoryError)异常
3、本地方法栈(线程私有)
本地方法与虚拟机栈的作用完全一样,他两的区别无非是本地方栈虚拟机使用的Native方法服务,而虚拟机栈为JVM执行的Java方法服务
4、Java堆(线程共享)
Java堆是JVM所管理的最大内存区域。Java堆是所有线程共享的一块区域,在JVM启动时创建,此内存区域存放的都是对象实例
Java堆是垃圾回收器管理的主要区域,因此很多时候可以称之为GC
如果在堆中没有足够的内存完成实例分配并且堆也无法再拓展时,将会抛出OOM
5、方法区(线程共享)
是各个线程共享的内存区域。用于存储已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。
在JDK8以前的HotSpot虚拟机中,方法区也被称为”永久代”(JDK8已经被元空间取代)。
JVM规范规定:当方法区无法满足内存分配需求时,将抛出OOM异常。
运行时常量池(方法区的一部分)
运行时常量池是方法区的一部分,存放字面量与符号引用。
字面量 : 字符串(JDK1.7后移动到堆中) final常量、基本数据类型的值。
符号引用 :类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符
Java堆溢出
Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免来避免GC清除这些对象,那么在对象数量达到最大堆容量后就会产生内存溢出异常。
内存泄漏 : 泄漏对象无法被GC
内存溢出 : 内存对象确实还应该存活。此时要根据JVM堆参数与物理内存相比较检查是否还应该把JVM堆内存调大;或者检查对象的生命周期是否过长。
虚拟机栈和本地方法栈溢出
由于我们HotSpot虚拟机将虚拟机栈与本地方法栈合二为一,因此对于HotSpot来说,栈容量只需要由-Xss参数来设置。
关于虚拟机栈会产生的两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出StackOverFlow异常
如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OOM异常
坚持住!下面的内容很重要
垃圾回收与内存分配
程序计数器、Java虚拟机栈、本地方法栈其生命周期与线程有关,随线程而生,随线程而灭。并且这三个区域的内存分配与回收具有确定性,因为当方法结束或者线程结束时,内存就自然而然跟着线程回收了。
Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经“死去”。如何判断?
判断方法如下:
1、引用计数法
描述:给对象增加一个引用计数器,每当有一个地方引用他时,计数器就+1;当引用失效时就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已“死”(JVM不能使用此方法)
缺点:引用计数法无法解决对象的循环引用问题
2、可达性分析算法
核心思想:通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称之为“引用链”,当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达),证明此对象不可用,如图所示:
对象object 4、object 5、object 6与GS Roots是不可达的,因此他们被判定可回收对象
可作为GC Roots的对象包含下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
引用
- 强引用:强引用指的是在程序代码之中普遍存在的,类似于“Object obj = new Object()”这类的引用,只要强引用还在,垃圾回收器永远不会回收掉被引用的对象实例
- 软引用:是用来描述一些还有用但是不是必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用
- 弱引用:也是用来描述非必须对象。但是他的强度要弱于软引用。被弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾回收器开始进行工作时,无论当前内容是否够用,都只会回收掉只被被弱引用关联的对象。在JDK1.2之后提供了WeakReference类来实现弱引用。
- 虚引用:也被称为幽灵引用或者幻影引用。他是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在JDK1.2之后,提供了
PhantomReference类来实现虚引用。
如何决定是生存还是死亡?
要宣告一个对象的真正死亡,至少要经历两次标记过程:如果对象在进行可达性分析之后发现没有与GC Roots相连接的引用链,那他将会被第一次标记并且进行一次筛选,刷选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者finalize()方法已经被JVM调用过,虚拟机将会将这两种情况视为“没有必要执行”,此时的对象才是真正的死亡。
如果这个对象被判定有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的队列之中,并在稍后有一个虚拟机自动建立的,低优先级的finalizer线程去执行它(这里所说的执行指的是虚拟机会触发finalize()方法)。fianlize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中对象进行第二次小规模标记,果对象在finalize()中成功拯救自己(只需要重新与引用链上的任何一个对象建立起关联关系即可),那第二次标记时他就会被移除“即将回收”的集合;如果对象这时候还是没有逃脱,那基本上他就是真的被回收了
回收方法区
方法区(永久代)的垃圾回收主要收集两部分内容:废弃常量和无用的类
判定一个类是否是”无用类”则相对复杂很多。类需要同时满足下面三个条件才会被算是”无用的类” :
- 该类所有实例都已经被回收(即在Java堆中不存在任何该类的实例)
- 加载该类的ClassLoader已经被回收
- 该类对应的Class对象没有在任何其他地方被引用,无法在任何地方通过反射访问该类的方法
JVM可以对同时满足上述3个条件的无用类进行回收,也仅仅是”可以”而不是必然。在大量使用反射、动态代理等场
景都需要JVM具备类卸载的功能来防止永久代的溢出。
垃圾回收算法
1、标记-清除算法
思想:算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足:
- 效率问题:标记和清除这两个过程的效率都不高
- 空间问题:标记清楚后会产生大量不连续的内存碎片,空间碎片太多会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集
2、复制算法(新生代回收算法)
“复制”算法是为了解决”标记-清理”的效率问题。他将可用内存按容量划分为大小相等的两块,每次只使用其中的一块当这块。当这块内存需要进行垃圾回收时,会将此区域还存活的对象复制到另外一块上面,然后再把已经使用过的内存区域一次清理掉这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。
新生代中98%的对象都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存(新生代内存)分为一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存者)空间,每次使用Eden和其中一块Survivor(两个Survivor区域一个称为From区,另一个称为To区域)。当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理到Eden和刚才用过的的Survivor空间。
当Survivor空间不够用的时候,需要依赖其他内存(老年代)进行分配担保
HotSpot默认Eden与Survivor的大小比例是8:1也就是说Eden:Survivor From : Survivor To = 8:1:1。所以每次
新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象。
HotSpot实现的复制算法流程如下:
(1)当Eden区满的时候,会触发第一次Minor gc,把还活着的对象拷贝到Survivor From区;当Eden区再次触发Minor gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则则直接复制到To区域,并将Eden和From区域清空
(2)当后续Eden又发生Minor gc的时候,会对Eden和To区域进行垃圾回收,存活的的对象复制到From区域,并将Eden和To区域清空
(3)部分对象会在From和To区域中复制来复制去,,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活就存入到老年代
3、标记-整理算法(老年代回收算法)
标记过程仍与”标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存
4、分代收集算法
当前JVM垃圾收集都采用的是”分代收集(Generational Collection)”算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。
一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采
用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用”标记-清理”或者”标记-整理”算法。
面经
请问了解Minor GC和Full GC么,这两种GC有什么不一样吗
(1)Minor GC又称为新生代GC:指的是发生在新生代的垃圾收集器。因为Java对象都具备朝生夕灭的特性
因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
(2)Full GC又称老年代GC或者或者Major GC:指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
垃圾收集器
(1)并行:指多条垃圾收集线程并行工作,用户线程仍处于等待状态
(2)并发:指用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户程序继续运行,而垃圾收集程序到另外一个CPU上
(3)吞吐量:就是CPU用于用户代码的时间与CPU消耗时间的比值,即吞吐量 = 运行用户代码时间/(运行用户代码时间 + 垃圾收集时间)
1、Serial收集器(新生代收集器,串行GC)
a、特性:这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束
b、应用场景:Serial收集器是虚拟机运行在Client模式下的默认新生代收集器
c、优势:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。 实际
2、ParNew收集器(新生代收集器,并行GC)
ParNew收集器其实是Serial收集器的多线程版本,除了使用多条多线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,
a、特性:Serial收集器的多线程版本
b、:除了Serial收集器外,目前只有它能与CMS收集器配合工作作,所以在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
c、与Serial收集器对比分析:
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。
然而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。
3、Parallel Scavenger收集器(新生代收集器,并行GC)
a、Parallel Scavenger收集器是一个新生代收集器,他也是使用复制算法的收集器,又是并行的多线程收集器
Parallel Scavenger收集器使用两个参数控制吞吐量:
- XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间
- XX:GCRatio直接设置吞吐量的大小
直观上,只要最大的垃圾收集停顿时间越小,吞吐量越高,但是**GC停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。
b、应用场景:停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
c、对比分析:
- Parallel Scavenge收集器 VS CMS等收集器:
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量
- Parallel Scavenge收集器 VS ParNew收集器:
Parallel Scavenge收集器与ParNew收集器的一个重要区别是它具有自适应调节策略
GC自适应的调节策略:
Parallel Scavenge收集器有一个调节参数:
XX:+UseAdaptiveSizePolicy 。
当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。
4、Serial Old收集器(老年代收集器,串行GC)
a、特性:Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法
b.应用场景:
Client模式
Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。Server模式
如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
5、Parallel Old收集器(老年代收集器,并行GC)
a、特性:Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
b、应用场景:在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
6、CMS收集(老年代收集器,并发GC)
a、特性:CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
CMS收集器是基于“标记—清除”算法实现的,过程:
- 初始标记(CMS initial mark):初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
- 并发标记(CMS concurrent mark):并发标记阶段就是进行GC Roots Tracing的过程
- 重新标记(CMS remark):重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”。
并发清除(CMS concurrent sweep):并发清除阶段会清除对象
CMS收集器的内存回收过程是与用户线程一起并发执行的
b.优点: CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。
c.缺点:CMS收集器对CPU资源非常敏感
- CMS收集器无法处理浮动垃圾
- CMS收集器会产生大量空间碎片
7、G1收集器(唯一一款全区域的垃圾回收器)
G1垃圾回收器再清除实例所占用的内存空间后,还会内存压缩
内存分配与回收策略
1、对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配,虚拟机将发生一次Minor GC
2、大对象直接进入老年代
所谓的大对象是指,需要大量连续空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(上述代码中的byte[] 数组就是典型的大对象)。大对象对虚拟机的内存分配是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发GC以获取足够的连续空间来放置大对象。
虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的在于避免Eden区以及两个Survivor区之间发生大量的内存复制(新生代采用复制算法收集内存)
3、长期存活的对象将进入老年代
1、动态对象年龄判定
为了能更好的的适应不同程序的内存状况,JVM并不是永远要求和对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等MaxTenuringThreshold中要求的年龄。
2、空间分配担保
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间
- 如果大于,则此次Minor GC是安全的
- 如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者
HandlePromotionFailure=false,则改为进行一次Full GC。
老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些
对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对
象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更
多空间。