首页 > 应用开发 > Java > Java面试备战篇——JVM篇(一)
JVM是Java运行基础,面试时一定会遇到JVM的有关问题,内容相对集中,但对只是深度要求较高。
其中内存模型,类加载机制,GC是重点方面.性能调优部分更偏向应用,重点突出实践能力.编译器优化和执
行模式部分偏向于理论基础,重点掌握知识点.
需了解
内存模型各部分作用,保存哪些数据.
类加载双亲委派加载机制,常用加载器分别加载哪种类型的类.
GC分代回收的思想和依据以及不同垃圾回收算法的回收思路和适合场景.
性能调优常有JVM优化参数作用,参数调优的依据,常用的JVM分析工具能分析哪些问题以及使用方法.
执行模式解释/编译/混合模式的优缺点,Java7提供的分层编译技术,JIT即时编译技术,OSR栈上替换,C1/C2编译器针对的场景,C2针对的是server模式,优化更激进.新技术方面Java10的graal编译器
编译器优化javac的编译过程,ast抽象语法树,编译器优化和运行器优化.
1、JVM内存模型:
线程独占:栈,本地方法栈,程序计数器
线程共享:堆,方法区
2、栈:
又称方法栈,线程私有的,线程执行方法是都会创建一个栈阵,用来存储局部变量表,操作栈,动态链接,方法
出口等信息.调用方法时执行入栈,方法返回式执行出栈.
3、本地方法栈
与栈类似,也是用来保存执行方法的信息.执行Java方法是使用栈,执行Native方法时使用本地方法栈.
4、程序计数器
保存着当前线程执行的字节码位置,每个线程工作时都有独立的计数器,只为执行Java方法服务,执行
Native方法时,程序计数器为空.
5、堆
JVM内存管理最大的一块,对被线程共享,目的是存放对象的实例,几乎所欲的对象实例都会放在这里,当堆
没有可用空间时,会抛出OOM异常.根据对象的存活周期不同,JVM把对象进行分代管理,由垃圾回收器进行
垃圾的回收管理
6、方法区:
又称非堆区,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器优化后的代码等数据.1.7的永
久代和1.8的元空间都是方法区的一种实现
7、JVM 内存可见性
JMM是定义程序中变量的访问规则,线程对于变量的操作只能在自己的工作内存中进行,而不能直接对主
内存操作.由于指令重排序,读写的顺序会被打乱,因此JMM需要提供原子性,可见性,有序性保证.

加载过程
其中
验证,准备,解析合称链接
加载通过类的完全限定名,查找此类字节码文件,利用字节码文件创建Class对象.
验证确保Class文件符合当前虚拟机的要求,不会危害到虚拟机自身安全.
准备进行内存分配,为static修饰的类变量分配内存,并设置初始值(0或null).不包含final修饰的静态变量,
因为final变量在编译时分配.
解析将常量池中的符号引用替换为直接引用的过程.直接引用为直接指向目标的指针或者相对偏移量等.
初始化主要完成静态块执行以及静态变量的赋值.先初始化父类,再初始化当前类.只有对类主动使用时才
会初始化.
触发条件包括,创建类的实例时,访问类的静态方法或静态变量的时候,使用Class.forName反射类的时候,
或者某个子类初始化的时候.
Java自带的加载器加载的类,在虚拟机的生命周期中是不会被卸载的,只有用户自定义的加载器加载的类
才可以被卸
1、加载机制-双亲委派模式
双亲委派模式,即加载器加载类时先把请求委托给自己的父类加载器执行,直到顶层的启动类加载器.父类
加载器能够完成加载则成功返回,不能则子类加载器才自己尝试加载.*
优点:
2、分代回收
分代回收基于两个事实:大部分对象很快就不使用了,还有一部分不会立即无用,但也不会持续很长时间.
年轻代->标记-复制
老年代->标记-清除
3、回收算法
a、G1算法
1.9后默认的垃圾回收算法,特点保持高回收率的同时减少停顿.采用每次只清理一部分,而不是清理全部的
增量式清理,以保证停顿时间不会过长
其取消了年轻代与老年代的物理划分,但仍属于分代收集器,算法将堆分为若干个逻辑区域(region),一部分
用作年轻代,一部分用作老年代,还有用来存储巨型对象的分区.
同CMS相同,会遍历所有对象,标记引用情况,清除对象后会对区域进行复制移动,以整合碎片空间.
年轻代回收:
并行复制采用复制算法,并行收集,会StopTheWorld.
老年代回收:
会对年轻代一并回收
初始标记完成堆root对象的标记,会StopTheWorld.
并发标记 GC线程和应用线程并发执行.
最终标记完成三色标记周期,会StopTheWorld.
复制/清楚会优先对可回收空间加大的区域进行回收
b、ZGC算法
前面提供的高效垃圾回收算法,针对大堆内存设计,可以处理TB级别的堆,可以做到10ms以下的回收停顿
时间.

roots标记:标记root对象,会StopTheWorld.
并发标记:利用读屏障与应用线程一起运行标记,可能会发生StopTheWorld.
清除会清理标记为不可用的对象. roots重定位:是对存活的对象进行移动,以腾出大块内存空间,减少碎片产生.重定位最开始会
StopTheWorld,却决于重定位集与对象总活动集的比例.
并发重定位与并发标记类似.
1.JVM内存模型简介
JVM定义了不同运行时数据区,他们是用来执行应用程序的。某些区域随着JVM启动及销毁,另外一些
区域的数据是线程性独立的,随着线程创建和销毁。jvm内存模型总体架构图如下:(摘自oracle官方
网站)
JVM在执行Java程序时,会把它管理的内存划分为若干个的区域,每个区域都有自己的用途和创建销毁
时间。如下图所示,可以分为两大部分,线程私有区和共享区。下图是根据自己理解画的一个JVM内存
模型架构图:
JVM内存分为线程私有区和线程共享区
线程私有区
1、程序计数器
当同时进行的线程数超过CPU数或其内核数时,就要通过时间片轮询分派CPU的时间资源,不免发生线
程切换。这时,每个线程就需要一个属于自己的计数器来记录下一条要运行的指令。如果执行的是JAVA
方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为空。
2、虚拟机栈
线程私有的,与线程在同一时间创建。管理JAVA方法执行的内存模型。每个方法执行时都会创建一个桢
栈来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法
调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小)。栈
的大小可以是固定的,或者是动态扩展的。如果请求的栈深度大于最大可用深度,则抛出
stackOverflowError;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出
OutofMemoryError。
使用jclasslib工具可以查看class类文件的结构。下图为栈帧结构图:
3、本地方法栈
与虚拟机栈作用相似。但它不是为Java方法服务的,而是本地方法(C语言)。由于规范对这块没有强
制要求,不同虚拟机实现方法不同。
线程共享区
1、方法区
线程共享的,用于存放被虚拟机加载的类的元数据信息,如常量、静态变量和即时编译器编译后的代
码。若要分代,算是永久代(老年代),以前类大多“static”的,很少被卸载或收集,现回收废弃常量和
无用的类。其中运行时常量池存放编译生成的各种常量。(如果hotspot虚拟机确定一个类的定义信息
不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装载该类的
ClassLoader被回收)
2、堆
存放对象实例和数组,是垃圾回收的主要区域,分为新生代和老年代。刚创建的对象在新生代的Eden
区中,经过GC后进入新生代的S0区中,再经过GC进入新生代的S1区中,15次GC后仍存在就进入老年
代。这是按照一种回收机制进行划分的,不是固定的。若堆的空间不够实例分配,则
OutOfMemoryError。

栈是运行时单位,代表着逻辑,内含基本数据类型和堆中对象引用,所在区域连续,没有碎片;堆是存
储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在区域不连
续,会有碎片。
1、功能不同
栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变量,
还是类变量,它们指向的对象都存储在堆内存中。
2、共享性不同
栈内存是线程私有的。
堆内存是所有线程共有的。
3、异常错误不同
如果栈内存或者堆内存不足都会抛出异常。
栈空间不足:java.lang.StackOverFlowError。
堆空间不足:java.lang.OutOfMemoryError。
4、空间大小
栈的空间大小远远小于堆的。
除直接调用System.gc外,触发Full GC执行的情况有如下四种。
1. 旧生代空间不足
旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空
间仍然不足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space
为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生
代多存活一段时间及不要创建过大的对象及数组。
2. Permanet Generation空间满
PermanetGeneration中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较
多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经
过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
3. CMS GC时出现promotion failed和concurrent mode failure
对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent
mode failure两种状况,当这两种状况出现时可能会触发Full GC。
promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代
也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此
时旧生代空间不足造成的。
应对措施为:增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本
中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可
通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
4. 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的
现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小
大于旧生代的剩余空间,那么就直接触发Full GC。
例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先
检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。
当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次
Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。
除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一
次Full GC。可通过在启动时通过- java-Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的
间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
Java虚拟机是一个可以执行Java字节码的虚拟机进程。Java源文件被编译成能被Java虚拟机执行的字节
码文件。 Java被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或
者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
方法区和对是所有线程共享的内存区域;而java栈、本地方法栈和程序员计数器是运行是线程私有的内
存区域。
Java堆(Heap),是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存
区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在
这里分配内存。
方法区(Method Area),方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,
它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
程序计数器(Program Counter Register),程序计数器(Program Counter Register)是一块较
小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
JVM栈(JVM Stacks),与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线
程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执
行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法
出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出
栈的过程。
本地方法栈(Native Method Stacks),本地方法栈(Native Method Stacks)与虚拟机栈所发挥
的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本
地方法栈则是为虚拟机使用到的Native方法服务。
对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在
Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次
Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到
阀值对象进入老年区。
动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一
半,年龄大于或等于该年龄的对象可以直接进入老年代。
空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如
果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设
置,如果true则只进行Monitor GC,如果false则进行Full GC。
JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的
Java运行时系统组件,它负责在运行时查找和装入类文件中的类。 由于Java的跨平台性,经过编译的
Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确
保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据
读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载
完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括
验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步
骤。最后JVM对类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先
初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。 类的加载是由类加载器完成
的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和
用户自定义类加载器(java.lang.ClassLoader的子类)。从Java 2(JDK 1.2)开始,类加载过程采取了
父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是
根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器
无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个
类加载器的说明:
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69981066/viewspace-2712375/ ,如需转载,请注明出处,否则将追究法律责任。