深入理解JVM虚拟机总结

    本文是阅读周志明的《深入理解Java虚拟机》一书所做的总结,非常经典的一本书,内容很多话语除了自己总结外采用了书中的内容进行涵盖。以下的题目都是JVM虚拟机的面试题来源于 https://blog.csdn.net/qq_34337272/article/details/80294328这篇博文,问题的回答都是自己找书中内容片段进行摘取总结,用来对自己翻阅这本书时留下笔记供以后复习时好翻阅,如果有疑问可以留言回复共同进步。

 

1、介绍一下Java内存区域(运行时数据区)

(1) 程序计数器

    程序计数器是一块较小的空间,在虚拟机的概念模型中,字节码解释器工作时就是通过这个计数器的值来获取下一条执行字节码的指令。程序计数器绑定的是线程,即每条线程只会有一个独立的程序计数器。如果执行的是Java方法,这个计数器记录的则是正在执行虚拟机字节码指令的地址。如果方法是native修饰的则程序计数器为空且此内存区域也是唯一一个在Java虚拟机中没有规定任何OutMemoryError情况的区域。

(2) Java虚拟机栈

    与程序计数器一样,都是线程私有的,且它的生命周期和线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行同时都会创建一个栈帧用来存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

    局部变量表存放了编译期的各种基本数据类型(boolean,char,byte,int,short,long,float,double)、对象引用(reference类型、它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是一个指向代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

    在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度则抛出StackOverflowError异常,如果虚拟机栈可以动态扩展,扩展时无法申请到足够的内存将会抛出OutOfMemoryError异常。

(3) 本地方法栈

    和上述一样都是线程私有的,与虚拟机栈所发挥的作用都是非常相似,它们之间最大的区别无非就是虚拟机栈执行的是Java方法服务,而本地方法栈执行的是native方法服务。本地方法栈同时也会抛出OutOfMemoryError异常和StackOverflowError异常。

(4) Java堆

    线程共享的数据区域,同时也是Java虚拟机管理的内存最大的区域。在虚拟机启动时创建,此内存区域唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

    Java堆也是GC堆(垃圾收集器)管理的主要区域,堆中细分可以分为新生代和老年代,再细分可以分为Eden控件、Form Survivor空间、ToSurvivor空间。

当Java堆无法扩展时,抛出OutOfMemoryError异常。

(5) 方法区

    所有线程共享,方法区又称为永生代或者持久区,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。

    当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。

(6) 运行时常量池

    运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项就是常量池用于存放编译期生成的各种字面量和符号引用,这部分内存在类加载进方法区时在常量池中存放。

既然是运行时的常量池是方法区的一部分,自然而然也受到方法区的限制,当常量池无法再申请到内存时就会抛出OutOfMemoryError异常。

(7) 直接内存

    并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。

    JDK1.4加入了NIO,引入一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。因为避免了在Java堆和Native堆中来回复制数据,提高了性能。

    当各个内存区域总和大于物理内存限制,抛出OutOfMemoryError异常。

 

2、对象的访问定位的两种方式

    建立对象的目的是为了使用对象,我们的Java程序需要通过Java栈是哪个的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用通过何种方式去定位,访问堆中的对象具体位置,所以对象访问方式也是取决于虚拟机实现而定,目前主流的访问方式是使用句柄和直接指针两种。

    1、通过句柄方式来访问

    如果使用的是句柄访问,Java堆就会划分一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄包含了对象实例数据与类型数据各种的具体地址信息。

    2.直接指针访问方式

    如果使用的是指针直接访问,那么Java堆对象的布局就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。

    这两种对象的访问方式各有特点,使用句柄最大好处就是reference中存储的是稳定的句柄地址,在对象移动的时只会改变实例数据的指针,而reference本身不需要修改。

    直接使用指针访问方式最大好处就是速度更快,因为其节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,所有这种方式就HotSpot虚拟机而言采用的就是直接指针访问的方式。

 

3、如何判断对象是否死亡(两种方法)

(1)引用计数算法

    引用计数算法就是给对象中添加一个引用计数器,每当有一个地方引用它时,计数器则加一,如果引用失效则计数器减一。任何时刻引用计数器为0时对象就是不可能再被使用了。

(2)可达性分析算法

    在主流的商用程序语言中。都是使用可达性分析来判断对象是否存活,这个算法的基本思想就是通过一系列的“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象不可用。

    如图所示 Object5、Object6、Object7虽然互有关联,但是GC Roots是不可达的,所以判定是可回收的对象。

    在Java语言中,可作为GC Roots的对象包括下面几种

    1:虚拟机栈(栈帧中的本地变量表)中引用的对象。

    2:方法区中类的静态属性引用对象。

    3:方法区中常量引用的对象。

    4:本地方法栈中引用的对象。

 

4、简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。

    JDK1.2后,Java对引用概率进行了扩充,将引用分为了强引用、软引用、弱引用和虚引用。下面对这4个引用进行简单的讲解。

    强引用:指程序代码中普遍存在的,类似new一个对象即Object obj = new Object()这类引用,只要强引用还存在,垃圾收集器永远不会回收掉这被引用的对象。

    软引用:用来描述一些有用但并非必须的对象,对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列为回收范围进行第二次回收。如果这次回收还没有足够内存,才会抛出内存溢出的异常。在JDK1.2后,提供了SoftReference类来实现软引用。

    弱引用:用来描述非必需对象,强度比软引用更弱,被弱引用的关联对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论内存是否足够,都会回收弱引用关联的对象。JDK1.2 使用WeakReference类来实现弱引用。

    虚引用:最弱的一种引用关系,每次垃圾回收都会被回收,虚引用的get方法用于获取到的数据为null,虚引用主要是用来检测对象是否已经被内存中删除。JDK1.2使用 PhantomReference类来实现虚引用。

软引用的好处?

    软引用的好处可以用来实现高速缓存区域,例如某一次操作需要加载大量图片,如果每次都从硬盘读取会严重影响性能,如果你全部放内存中又会导致内存泄露,这时候就需要用来软引用来来实现高速缓存,高速缓存的特点如果命中则能够加快响应,如果未命中还能重新获取原始数据。对于某更新频率低,但查询很慢的数据可以将其放入软引用类中,当GC发现将要内存溢出时就释放这软引用中的数据来提供给其他对象使用。

/**

* @ClassName SoftReferenceTest

* @Description 软引用好处实例

* @Author huangwb

* @Date 2019-02-18

* @Version 1.0

**/

public class SoftReferenceTest {

    private static Map<String, SoftReference<Object>> cacheMap = new HashMap<>();



    public static Object getStudent(String stuId){

        SoftReference<Object> softReference = cacheMap.get(stuId);

        //判断缓存中是否存在

        Object cacheStudent = softReference==null?null:softReference.get();

        if(cacheStudent==null){

            cacheStudent = getMysqlQuery(stuId);

            cacheMap.put(stuId,new SoftReference<>(cacheStudent));

        }

        return  cacheStudent;

    }

    //从数据库中查询

    public static Object getStudentById(String stuId){

        //模拟查询数据过程

        Object object = new Object();

        return object;

    }

}

 

5、垃圾收集器有哪些算法,各自的特点。

    垃圾收集器的算法有标记-清除算法、复制算法、标记整理算法、分代收集算法。下面给大家详细介绍这四种算法的优点缺点。

(1)标记-清除算法

    最基础的收集算法,如果它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记处所有需要回收的对象,标记完成后统一回收所有被标记的对象。之所以是最基础的收集算法,因为后续的算法都是基于这种算法的不断改进的。

    缺点:效率问题,标记清除两个过程效率都不高。空间问题,标记清除后产生了大量不连续的内存碎片,空间碎片太多导致对象进来时无法给其分配足够的连续内存空间,不得不再次触发一次垃圾收集操作。

(2)复制算法

    它将可用内存按容量分为两块等大的内存区域,每次只使用其中一块内存区域,当这一块内存区域使用完毕之后,就将还存活的对象复制到另外一块区域中去,然后把已经使用过的内存空间再清理一次。这样就使得每次回收都是一半内存进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶的指针,按照顺序分配内存即可。

    优点:实现简单,运行高效。

    缺点:代价太大,将原有的内存缩小一半。

(3)标记-整理算法

    复制算法对对象存活率较高时就需要进行较多的复制操作,效率会很低。

    根据老年代的特点,标记-整理算法就诞生了,不是直接对可回收对象进行清理,而是让所有存货对象都向一段移动,然后直接清理掉端边界以外的内存。

(4)分代收集算法

    先介绍一下什么是年轻代、老年代heapspace分为年轻代和年老代,年轻代的垃圾回收叫MonorGC 年老代的垃圾回收叫FullGC。

    年老代和年轻代比例会2:1年轻代中又有Eden空间 8/10、To Survivor空间 1/10、FromSurvior空间 1/10的空间比例。

    HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC(young GC),年龄就会增加1岁,当它的年龄增加到一定程度(15岁)时,就会被移动到年老代中。

    当前的商业虚拟机的垃圾收集器都采用“分代收集算法”,这种算法将Java堆分为新生代和老年代,这样就能够根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量对象存活于是可以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代因为对象存活率高、没有额外的空间对它分配担保,就必须采用“标记-清理”或者“标记-整理”算法来进行回收。

 

6、HotSpot为什么要分为年轻代和老年代?

    由于80%以上的对象都是“朝生夕死”,如果不按照分代进行隔离虚拟机就只能扫描整个Java堆去拿取对象、清楚对象这样会非常影响垃圾回收的效率。如果分代的话,我们把新创建的对象放到一地方,当GC的时候我们只需要把这一块区域给回收,这样会节省很多的内存空间。

    年轻代中就可以使用复制算法,因为复制算法的特点就是复制存活的对象,而年轻代中的对象大部分都是朝生夕死的,所以能够提升GC的回收效率运行更加高效。而年老代的特点都是一些生命周期比较长的对象,我们则可以采用标记清楚的垃圾回收算法更好的提高效率。

 

7、常见的垃圾回收期有哪些?

一共7种垃圾收集器,根据分代的不同选择不同的垃圾收集器

(1)Serial收集器

    Serial收集器是最基本的、发展历史最悠久的垃圾收集器。Serial垃圾收集器的特点是单线程收集器,它的单线程不是只会使用一个CPU或一条收集线程去完成工作。这里的单线程收集器指的是在它进行垃圾回收的过程中需要停止所有的工作线程,直到它收集完成。例如你工作一小时停顿5分钟一样,这样想可能没问题但是在程序中这5分钟会非常影响用户体验。

    下面这张图介绍了Serial收集器的垃圾回收算法的实现。

(2)ParNew收集器

    ParNew收集器是Serial收集器的多线程版本,除了多条现场进行垃圾收集之外其余各种特点和Serial类似,其中最大的亮点就是能够和跨时代的CMS收集器配合工作,后面我们会讲解到CMS收集器是什么,这款收集器简单介绍就是一款真正意义上的并发收集器,能够让垃圾回收线程和用户线程工作同时进行。

    ParNew和Serial的性能区别:ParNew和Serial在单线程的环境下,ParNew的性能弱于Serial,但ParNew的特点就是多线程垃圾收集器,随着CPU的数量增加它对于GC时的系统资源有效利用很有好处的。

(3)Parallel Scavenge收集器

    Parrallel Scavenge收集器是一个新生代的多线程并行收集器,它也是一个采用复制算法的收集器。可能各位觉得和ParNew收集器一样。但它最大的特点是可以控制吞吐量,什么是吞吐量呢就是CPU用于运行用户代码时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码+垃圾收集时间),如果此时虚拟机共运行了100分钟,其中的垃圾收集花掉了一分钟,则此时的吞吐量就是99%,停顿时间越短在需要和用户交互的程序,良好的响应速度就能提升用户体验,而高吞吐量就可以高效率的利用CPU时间,尽快完成程序的运算任务,主要适用于后台运算而不需要太多交互的任务。大家可以去搜一搜Parallel Scavenge收集器 会有更加深刻的理解。

(4)Serial Old收集器

    Serial Old收集器就是Serial收集器的老年代版本,它同样是一个单线程收集器,适用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下虚拟机使用。如果再Server模式下,它有两大用途:一种是和JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用户就是作为CMS收集器的备选预案。

(6)CMS收集器

    终于讲到这个跨时代的收集器了,这一种以最短回收停顿时间为目标的收集器。目前很大一部分的Java应用几种在互联网或者B/S系统服务端上,这类应用由其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

    CMS收集器是一种基于“标记-清除”算法实现的,比前几种来说实现更加复杂,整个过程分为4步:

    1:初始标记

    2:并发标记

    3:重新标记

    4:并发清除

    初始标记和重新下标记这两个步骤仍然会造成用户线程的挺短,初始标记仅仅只是标记一下GC Roots能直接关联的对象,速度非常快。而重新标记则是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记要短。

    整个过程中耗时最长的是并发标记和并发清除过程的收集器线程但都可以和用户线程一起工作,总体来说CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS的优点:并发收集、低停顿。

CMS的缺点:

    1:CMS收集器对CPU资源非常的敏感。其实面向并发设计的程序都对CPU资源比较敏感,在并发阶段虽然不会导致用户线程的挺短,但是会占用了一部分线程从而导致应用程序变慢,总吞吐量降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是在CPU为4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降,但是当CPU不足4个时(例如两个),CMS对用户程序影响就变得很大了,如果本来CPU的负载就比较大,还分出一半运算能力去执行收集器线程,则会导致用户程序执行速度忽然降低50%,其实也让人无法接受。

    2:CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序运行自然就会有新的垃圾不断产生,这一部分垃圾在标记过程之后,CMS无法再当次手机中处理掉它们,只好等待下一次GC时再清理掉。这一份垃圾就称为“浮动垃圾”。

    3:这个缺点就是由于CMS是一款基于“标记-清楚”算法实现的收集器,则必然会在垃圾收集之后造成大量空间碎片产生。空间碎片过多时,将会给大对虾分配带来很大麻烦,往往会出现老年代还有很大空间,却无法找到足够大的连续空间来保存大对象,不得不触发一次Full GC操作。

(7)G1垃圾收集器

    G1收集器是当今收集器技术发展最前沿的成果,具体可以看看以下这篇博文。

    https://blog.csdn.net/u011546953/article/details/78994882

 

8、介绍一下CMS、G1收集器 

    讲得非常清楚https://blog.csdn.net/u011546953/article/details/78994882

 

9、Minor GC和Full GC有什么不同?

    年轻代GC(Minor GC):指符合年轻代大多数对象“朝夕生死”的特性,所以Minor GC使用非常频繁,回收速度也比较快。    

    老年代GC(Full GC/Major GC):FullGC是清理整个堆空间的包括年轻代、老年代和元空间(JDK1.8之前定义为持久区或永久代)FullGC一般消耗的时间远比MinorGC ,因此我们必须降低FullGC发生的频率。

    Minor GC的触发机制:当年轻代满时就会触发Minor GC 这里的年轻代满指的是Eden空间 不是Survivor空间,因为To Survivor和From Survivor总是保持一端有数据另一端无数据的情况是用来执行复制算法的。

    Full GC的触发机制:(1)调用System.gc时,系统建议执行Full Gc的操作 (2)老年代空间不足时(3)方法区空间不足时(4)通过Minor GC后进入老年代的对象大于老年代的空间时(5)由Eden区、Form Survivor区向ToSurvivor区复制时,对象大于ToSurvivor空间时,则把该对象放入老年代,且老年代可用连续空间小于该对象大小时。

 

10、JVM调优的常见命令行工具有哪些?

 

11、简单介绍一下Class类文件结构(常量池主要存放的是那两大常量?Class文件的继承关系是如何确定的?字段表、方法表、属性表主要包含那些信息?)

 

12、简单说说类加载过程,里面执行了哪些操作?

 

    类加载过程中最重要的就是加载、验证、准备、初始化和卸载的顺序是确定的,类的加载过程必须按照这种顺序按部就班的进行,而解析却不一定了,为了支持Java语言的晚期绑定或者动态绑定,通常会在一个阶段执行过程中激活另外一个阶段。

    加载:是类加载的一个过程通常会通过一个类的全限定名来获取定义的二进制字节流。将这个字节流所代表的静态存储结构转化为方法区的运行时的数据结构。在内存中生成一个代表这个类的的Class对象,作为方法区这个类的各个数据的访问入口。

    验证:是连接阶段的第一步,主要是确保Class文件的字节流包含的信息符合当前虚拟机的要求,并且不会危害到虚拟机的自身安全。

    准备:正是为类变量分配内存并设置初始化值的阶段,这些变量所使用的内存都在方法区中进行分配。说明:这时候进行内存分配的仅仅只是类变量(static修饰的变量)不是实例变量,实例变量会在对象实例化时随着对象一起被分配在Java堆中。另外,这里说的初始化只是将类变量设置为数据类型的零值

public static int value = 666;

    那变量value在准备阶段初始化的值为0而不是666,只有当程序编译完成之后才开始执行赋值的操作。

    解析:解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。

    初始化:初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。

 

13、对类加载器有了解嘛?

    链接如下,真大佬解析的 太厉害了。https://blog.csdn.net/javazejian/article/details/73413292

 

14、什么是双亲委派模型?

    链接如下,真大佬解析的 太厉害了。https://blog.csdn.net/javazejian/article/details/73413292

 

15、双亲委派模型的工作过程以及使用它的好处。

    链接如下,真大佬解析的 太厉害了。https://blog.csdn.net/javazejian/article/details/73413292

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

难搞哦!!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值