zoukankan      html  css  js  c++  java
  • JVM面试题总结

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

    Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。

    JDK 1.8之前主要分为:堆、方法区、虚拟机栈、本地方法栈、程序计数器。其中堆和方法区是线程共享的,虚拟机栈、本地方法栈、程序计数器是线程私有的。

    JDK 1.8 的时候,方法区(HotSpot的永久代)被彻底移除了,取而代之是元空间元空间使用的是直接内存

    程序计数器

      可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一次需要执行的字节码的指令,分支、循环、跳转、异常处理等都需要依赖这个计数器来完成。

      在多线程的情况下,程序计数器用于记录当前线程执行的位置,因此为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,即线程私有。

      如果线程正在执行一个Java方法,这个计数器记录的正在执行的虚拟机字节码指令的地址;如果正在执行Native方法,这个计数器值为空。

      【此内存区域是唯一一个不会抛出OutOfMemoryError的区域】

    虚拟机栈

      虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

      【有两种异常StackOverFlowError和 OutOfMemoneyError:当线程请求栈深度大于虚拟机所允许的深度就会抛出StackOverFlowError错误;虚拟机栈动态扩展,当扩展无法申请到足够的内存空间时候,抛出OutOfMemoneyError。】

    局部变量表:存放了编译期可知的各种基本数据类型(boolean、byte、int、long、float、double、char、boolean)、对象引用(reference类型,它可能是一个指向对象起始地址的指针,也可能是指向一个代表对象的句柄)和returnAddress类型(指向了一条字节码指令的地址)。

    【注】64位的long和double类型数据占用2个局部变量空间(slot),其余的数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。

    操作数栈:用来存放操作数。Java 程序编译之后就变成了一条条字节码指令,Java字节码指令的操作数存放在操作数栈中,当执行某条带 n个操作数的指令时,就从栈顶取n个操作数,然后把指令的计算结果(如果有的话)入栈。

    动态链接:class文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池指向的方法的符号引用作为参数,这些符号引用一部分会在类加载阶段(解析阶段)或者第一次使用的时候就转化为直接引用,这种转化成为静态解析,另一部分在没一次运行期间转化为直接引用,这部分成为动态连接。

    方法出口:,即方法返回地址。一个方法在执行时,只有两种方式退出这个方法:正常完成出口和异常完成出口:

    • 正常完成出口:执行引擎遇到一个方法返回的字节码指令,这时候执行引擎读取栈帧中的方法返回地址,将返回值传递给上层的方法调用者。
    • 异常完成出口:在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,也就是在本地异常表内没有搜索到匹配的异常处理器,就会导致方法退出。这时候执行引擎不会读取方法返回地址而直接停止执行,上层调用者不会得到任何返回值。

    本地方法栈

      和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

      Java 虚拟机所管理的内存中最大的一块,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存

      Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆。由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:再细致一点有:Eden空间、From Survivor、To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域存储的都是对象实例,进一步划分的目的是为了更好地回收内存或更快地分配内存。

      【如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoneyError】

    方法区

      用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

      运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。常量池具有一定的动态性,常量并不一定只在编译期产生,运行期间的常量也可以添加进入常量池中,比如string的intern()方法。

      垃圾回收很少光顾方法区,不过也是需要回收的,主要针对常量池回收,类型卸载。

      【当方法区无法满足内存分配要求时,将抛出OutOfMemoneyError异常,当常量池无法再申请到内存时也会抛出OutOfMemoneyError】

    2、Java 对象的创建过程(五步,建议能默写出来并且要知道每一步虚拟机做了什么)

    类加载检查 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程

    分配内存 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定(“标记-清除”、“标记-压缩”)。

     初始化零值 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用。

     设置对象头 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。

     执行 init 方法执行 new 指令之后会接着执行 <init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来

    3、对象的访问定位的两种方式(句柄和直接指针两种方式)

     Java程序需要通过栈上的reference数据来操作堆上的具体对象。

    • 句柄: 如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息

    使用句柄

    • 直接指针 如果使用直接指针访问reference 中存储的直接就是对象的地址

    使用直接指针

    这两种对象访问方式各有优势:

    • 使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改
    • 使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销

    4、内存是如何分配和回收的?

    • Java 堆是垃圾收集器管理的主要区域,Java 堆可以分为:新生代和老年代:再细致一点可以分为:Eden空间、From Survivor、To Survivor、tentired。
    • 大部分对象都会首先在 Eden 区域分配,而大对象(需要大量连续内存空间的对象,比如:字符串、数组)直接进入老年代。
    • 在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1。对象在 Survivor 中每熬过一次年轻代垃圾回收,年龄就增加1岁。当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。
    • 为了更好的适应不同程序的内存情况,虚拟机不是永远要求对象年龄必须达到了某个值才能进入老年代,如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到要求的年龄。

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

    引用计数法可达性分析法

    • 引用计数法给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
      • 难以解决对象之间相互循环引用的问题(这样的话计数器永远不为0)
    • 可达性分析法:通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

     对于可达性分析算法而言,不可达的对象并非是“非死不可”的若要宣判一个对象死亡,至少需要经历两次标记阶段

    1. 如果对象在进行可达性分析后发现没有与GCRoots相连的引用链,则该对象被第一次标记并进行一次筛选,筛选条件为是否有必要执行该对象的finalize方法,若对象没有覆盖finalize方法或者该finalize方法是否已经被虚拟机执行过了,则均视作不必要执行该对象的finalize方法,即该对象将会被回收。反之,若对象覆盖了finalize方法并且该finalize方法并没有被执行过,那么,这个对象会被放置在一个叫F-Queue的队列中,之后会由虚拟机自动建立的、优先级低的Finalizer线程去执行,而虚拟机不必要等待该线程执行结束,即虚拟机只负责建立线程,其他的事情交给此线程去处理。
    2. 对F-Queue中对象进行第二次标记,如果对象在finalize方法中拯救了自己,即关联上了GCRoots引用链,如把this关键字赋值给其他变量,那么在第二次标记的时候该对象将从“即将回收”的集合中移除,如果对象还是没有拯救自己,那就会被回收。

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

    • 强引用:垃圾回收器绝不会回收它当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
    • 软引用:当内存空间不足,垃圾回收器就会回收它;内存空间足够就不会回收。
    • 弱引用:当垃圾回收器扫描它所管辖的内存区域时,一旦发现了具有弱引用的对象,无论内存空间是否足够都会进行回收。
    • 虚引用:在任何时候都可能被垃圾回收,垃圾回收时会收到一个系统通知。

    虚引用与软引用和弱引用的一个区别在于:

       虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动

    使用软引用的好处:

      可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

    7、如何判断一个常量是废弃常量

      假如在常量池中存在字符串 "abc",如果当前没有任何String对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池。

    8、如何判断一个类是无用的类

    类需要同时满足下面3个条件才能算是 “无用的类” :

    • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
    • 加载该类的 ClassLoader 已经被回收。
    • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

    9、垃圾收集有哪些算法,各自的特点?

    标记-清除算法、复制算法、标记-整理算法、分代收集算法

    (1)标记-清除:算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

    • 效率高,但标记清除后会产生大量不连续的碎片。

     (2)复制:将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。

    •  空间浪费,只能使用一半空间

     (3)标记-整理:根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

    (4)分代收集:一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

    10、HotSpot 为什么要分为新生代和老年代?

     将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

    11、常见的垃圾回收器有那些?

     Serial收集器、ParNew收集器、Parallel Scavenge收集器、CMS收集器、G1收集器

    (1)Serial(串行)收集器是一个单线程收集器。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( "Stop The World" ),直到它收集结束

    (2)ParNew收集器:就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。

    (3)Parallel Scavenge收集器关注点是吞吐量高效率的利用CPU)。CMS等垃圾收集器关注点更多的是用户线程的停顿时间(提高用户体验)

    (4)CMS垃圾收集器:以获取最短回收停顿时间为目标的收集器,是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。 “标记-清除”算法实现的

    (5)G1收集器:是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征

    12、介绍一下 CMS,G1 收集器

    (1)CMS收集器

      CMS收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。它是一种以获取最短回收停顿时间为目标的收集器,非常符合在注重用户体验的应用上使用。

      CMS 收集器是基于 “标记-清除”算法实现的,它的运作过程分为四个步骤:

    1. 初始标记: 暂停所有的其他线程,并标记直接与 GC Roots 相连的对象,速度很快
    2. 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象(标记可达对象)。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
    3. 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
    4. 并发清除: 开启用户线程,同时 GC 线程开始对为标记的区域做清扫。

    CMS 垃圾收集器

      CMS垃圾收集器主要优点:并发收集、低停顿

      但是它有下面三个明显的缺点:

    • 对 CPU 资源敏感;
    • 无法处理浮动垃圾;
    • 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生

    (2)G1收集器

      G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.

      与其他GC收集器相比,G1具备如下特点:

    • 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
    • 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
    • 空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现
    • 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内

      在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,但G1不再是这样。 使用G1收集器时,它将整个Java堆划分成多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都是一部分独立区域的集合

      G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值)在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率(把内存化整为零)。

      G1收集器的运作大致分为以下几个步骤:

    • 初始标记:标记GC Roots能直接关联到的对象,并修改TAMS的值让下一阶段用户程序并发运行时能在正确可用的Region中创建新对象。这阶段需要停顿线程,但耗时很短。
    • 并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活的对象。这阶段耗时较长,但可与用户并发执行。
    • 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remember Set Logs里面,最终标记阶段需要把Remember Set Logs的数据合并到Rememberd Set中,这阶段需要停顿线程,但是可并发执行。
    • 筛选回收:首先对各个Region的回收价值和成本进行排序,然后根据用户所期望的GC停顿时间来制定回收计划。

    13、Minor Gc 和 Full GC 有什么不同呢?

    • 新生代GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
    • 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。

     14、JDK 监控

    JDK 命令行,这些命令在 JDK 安装目录下的 bin 目录下:

    • jps (JVM Process Status): 类似 UNIX 的 ps 命令。用户查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息;
    • jstat( JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据;
    • jinfo (Configuration Info for Java) : Configuration Info forJava,显示虚拟机配置信息;
    • jmap (Memory Map for Java) :生成堆转储快照;
    • jhat (JVM Heap Dump Browser ) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果;
    • jstack (Stack Trace for Java):生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。

    15、Java类加载过程

    系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析

      类加载过程

    (1)在加载阶段,虚拟机需要完成以下3件事情:

    1. 通过一个类的全限定名来获取定义此类的二进制字节流 到JVM内部。
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

      (注:数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。)

    (2)验证这一阶段的目的是为了确保Class文件的字节流中所包含的信息符合当前虚拟机的要求,并且不会危害迅疾自身的安全

      从整体上看,验证阶段大致上会完成下面4个阶段的校验动作:文件格式验证、元数据验证、字节码验证、符号引用验证

      

    (3)准备:正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

       注:

    1. 这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中
    2. 这里所设置的初始值"通常情况"下是数据类型默认的零值(如0、0L、null、false等)

       (比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是111把value赋值为111的动作在初始化阶段才会执行)。特殊情况:比如给 value 变量加上了 fianl 关键字public static final int value=111 ,那么准备阶段 value 的值就被复制为 111。)

     (4)解析:是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量

      在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方发表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。

     (5)初始化:开始真正执行类中定义的Java程序代码(或者说是字节码)。

    看我的这篇文章:https://www.cnblogs.com/toria/p/11161080.html

     何时触发初始化?

    1. 当遇到 new 、getstatic、putstatic或invokestatic 这4条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
    2. 使用 java.lang.reflect 包的方法对类进行反射调用时 ,如果类没初始化,需要触发其初始化。
    3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
    4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
    5. 当使用 JDK1.7 的动态动态语言时,如果一个 MethodHandle 实例的最后解析结构为 REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且这个句柄没有初始化,则需要先触发器初始化。

    16、什么是类加载器?

      通过一个类的全限定名来获取描述此类的二进制字节流,实现这个动作的代码模块称为“类加载器”。

      (负责读取 Java 字节代码,并转换成java.lang.Class类的一个实例)

    17、类加载器与类的”相同“判断

      比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

      这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。

    18、类加载器的种类?

    启动类加载器(Bootstrap ClassLoader):加载 %JAVA_HOME%/lib/ext 目录下的jar包和类,或者被-Xbootclasspath参数指定的路径中的所有类。启动类加载器无法被java程序直接调用。

    扩展类加载器(Extension ClassLoade):加载 %JRE_HOME%/lib/ext 目录下的jar包和类,或者被java.ext.dirs系统变量所指定的路径下的jar包。

    系统类加载器(Application ClassLoader):加载当前应用classpath下的所有jar包和类。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

    自定义类加载器:通过继承ClassLoader实现,一般是加载我们的自定义类

    19、双亲委派模型

      每一个类都有一个对应它的类加载器。系统中的类加载器在协同工作的时候会默认使用双亲委派模型。除了启动类加载器,每个类都有其父类加载器(父子关系由组合(不是继承)来实现)。

    双亲委派模型工作过程:

      如果一个类加载器收到了类加载的请求,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成因此所有的加载请求最终都应该传送到顶层的启动类加载器(Bootstrap ClassLoader)中。只有当父加载器无法处理(它的搜索范围中没有找到所需的类)时,子下载器才会尝试自己去加载 当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。

    双亲委派好处:

    • 避免同一个类被多次加载;
    • 每个加载器只能加载自己范围内的类;

     20、如何创建自定义类加载器?

       继承java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,即指明如何获取类的字节码流。

    • 如果要符合双亲委派规范,则重写findClass方法(用户自定义类加载逻辑);要破坏的话,重写loadClass方法(双亲委派的具体逻辑实现)

    21.Minor Gc和Full GC?

    • 新生代GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
    • 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。

    gc触发条件

    • Minor GC的触发条件:大多数情况下直接在 Eden 区中进行分配。如果 Eden区域没有足够的空间,那么就会发起一次 Minor GC。
    • Full GC(Major GC)的触发条件:如果老年代没有足够空间的话,那么就会进行一次Full GC。

      但这只是一般情况,实际上,需要考虑一个空间分配担保的问题:

      在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果大于则进行Minor GC,如果不大于则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败(不允许则直接Full GC)。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试Minor GC(如果尝试失败也会触发Full GC),如果小于则进行Full GC。

    22.可能触发full GC的机制? Full GC频繁怎么优化?

    (1)老年代空间不足

    【原因】老年代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:java.lang.OutOfMemoryError: Java heap space

    【优化】为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间以及不要创建过大的对象及数组。

    (2)方法区或者元数据空间不足

    【原因】方法区中存放的为一些类信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,方法区可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:

    java.lang.OutOfMemoryError: PermGen space

    【优化】为避免PermGen(方法区)占满造成Full GC现象,可采用的方法为增大PermGen空间或转为使用CMS GC。

    (3)System.gc()方法调用

    【原因】此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率。

    【优化】建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

    (4)CMS GC时出现promotion failed和concurrent mode failure

    【原因】对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。

    promotion failed是在进行Minor GC时,survivor区放不下、对象只能放入老年代,而此时老年代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。

    【优化】对应措施为:增大survivor区、老年代的空间 或 调低触发并发GC的比率。

    (5)Minior GC时晋升老年代的内存平均值大于老年代剩余空间

    【优化】Hotspot为了避免由于新生代对象晋升到老生代导致老年代空间不足的现象,在进行Minor GC时,做了一个判断:如果之前统计所得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间,那么就直接触发Full GC。例如程序第一次触发Minor GC后,有6MB的对象晋升到老年代,那么当下一次Minor GC发生时,首先检查老年代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。

    (6)有连续的大对象需要分配

    【优化】所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。

    为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即-XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。

    参考https://www.jianshu.com/p/27703ef3de65

    23.内存泄漏排查和解决方法?

    • 内存溢出:程序在申请内存时,没有足够的内存空间供其使用,出现OutOfMemoryError。例如你申请了10个字节的空间,但是你在这个空间写入11或以上字节的数据,出现溢出。
    • 内存泄漏:指程序在申请内存后,无法释放已申请的内存空间。例如你用new申请了一块内存,后来很长时间都不再使用了(按理应该释放),但是因为一直被某个或某些实例所持有导致 GC 不能回收,也就是该被释放的对象没有释放。

      内存泄漏过多必然会导致内存溢出。

    jvm内存泄漏排查流程:

    1.查询cpu消耗最大的进程

    • jps 找出正在运行的虚拟机进程
    • top 命令查看哪些些java进程消耗的cpu比较大(看PID)

    2.找到你需要监控的ID,再利用虚拟机统计信息监视工具jstat监视虚拟机各种运行状态信息,找出频繁Full GC对象。(可以得到服务器的Eden区、两个Survivor区、老年代的已使用百分比、程序运行以来共发生Minor GC、Full GC的次数与耗时、以及总耗时等信息)

      jstat -gcutil 20954 1000  (假设209564是要查的ID,意思是每1000毫秒查询一次,一直查。gcutil的意思是已使用空间站总空间的百分比。)

    3.使用jmap命令查看存活对象情况,发现有数据不正常,十有八九就是泄露的。

      使用命令如下:jmap -histo:live 20954

    4.定位到代码,有很多种方法,比如通过MAT查看Histogram即可找出是哪块代码。也可以使用BTrace。

    参考https://cloud.tencent.com/developer/article/1144256

    import java.io.ByteArrayOutputStream;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    
    class TestClassLoad {
        @Override
        public String toString() {
            return "类加载成功。";
        }
    }
    public class PathClassLoader extends ClassLoader {
        private String classPath;
    
        public PathClassLoader(String classPath) {
            this.classPath = classPath;
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            byte[] classData = getData(name);
            if (classData == null) {
                throw new ClassNotFoundException();
            } else {
                return defineClass(name, classData, 0, classData.length);
            }
        }
    
        private byte[] getData(String className) {
            String path = classPath + File.separatorChar
                    + className.replace('.', File.separatorChar) + ".class";
            try {
                InputStream is = new FileInputStream(path);
                ByteArrayOutputStream stream = new ByteArrayOutputStream();
                byte[] buffer = new byte[2048];
                int num = 0;
                while ((num = is.read(buffer)) != -1) {
                    stream.write(buffer, 0, num);
                }
                return stream.toByteArray();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            return null;
        }
    
    
    
        public static void main(String args[]) throws ClassNotFoundException,
                InstantiationException, IllegalAccessException {
            ClassLoader pcl = new PathClassLoader("D:\ProgramFiles\eclipseNew\workspace\cp-lib\bin");
            Class c = pcl.loadClass("classloader.TestClassLoad");//注意要包括包名
            System.out.println(c.newInstance());//打印类加载成功.
        }
    }
    View Code
  • 相关阅读:
    [模板]洛谷T3369 普通平衡树 链表&普通Treap
    C++语法知识点整理
    [模板]洛谷T3373 线段树 模板2
    [模板]洛谷T3372 线段树 模板1
    [模板]洛谷T3368 树状数组 模板2
    JSON
    code first迁移和部署
    序列化 (C#)
    Linq小记
    文件和注册表
  • 原文地址:https://www.cnblogs.com/toria/p/11234779.html
Copyright © 2011-2022 走看看