JVM

Mr.WyjJanuary 20, 2023About 36 min

JVM

JVM 回收算法

回收算法

1. 标记-清除算法:

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象

缺点:

  1. 效率问题,标记和清除二个过程的效率都不高;
  2. 空间问题,标记清除后会产生大量的不连续的内存碎片;

2. 复制算法

将内容按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的内存对象复制到另外一块上面,然后把已经使用过的内存空间一次性清理掉;

实际中并不是一半一半分,复制算法将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor 空间。

缺点:

  1. 对象存活率较高时就要进行较多的复制操作,效率将会变低;
  2. 每次只能使用一半内容,代价较高;

3. 标记-整理算法

首先标记出所有需要回收的对象,在标记完成后让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存;

4. 分代收集算法

据对象的存活周期的不同将内存划分为几块,一般就分为新生代和老年代,根据各个年代的特点采用不同的收集算法。新生代(少量存活)用复制算法,老年代(对象存活率高)“标记-清理”算法或标记-清除算法

JVM 回收器,CMS 采用哪种回收算法,怎么解决内存碎片问题?

新生代回收器:

  1. Serial:单线程,进行垃圾收集时,需暂停掉其它所有的用户线程,新生代复制算法,老年代标记-整理算法
  2. ParNew:Serial 收集器的多线程版本,需暂停掉其它所有的用户线程,新生代复制算法,老年代标记-整理算法,目前只能与 CMS 收集器配合工作
  3. Parallel Scavenge:多线程并行,复制算法,“吞吐量优先”的收集器。吞吐量就是 CPU 用于运行用户代码的时间与总 CPU 总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间)。并行是指同一时刻一起执行,并发是指同一时刻间隔执行。

老年代回收器:

  1. Serial Old:Serial 的老年代版本,单线程,与 Parallel Scavenge 搭配使用,或作为 CMS 收集器的后备预案,新生代复制算法,老年代标记-整理算法
  2. Parallel Old:Parallel Scavenge 的老年代版本,注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器
  3. CMS:获取最短停顿时间为目标,尤其重视服务的响应速度,基于标记-清除算法; 解决内存碎片问题: 让 CMS 在进行一定次数的 Full GC(标记清除)的时候进行一次标记整理算法,CMS 提供了以下参数来控制:-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5 也就是 CMS 在进行 5 次 Full GC(标记清除)之后进行一次标记整理算法,从而可以控制老年带的碎片在一定的数量以内,甚至可以配置 CMS 在每次 Full GC 的时候都进行内存的整理;
  4. G1:并行的、并发的和增量式压缩短暂停顿的垃圾收集器。G1 收集器不区分年轻代和年老代空间。它把堆空间划分为多个大小相等的区域。当进行垃圾收集时,它会优先收集存活对象较少的区域,因此叫“Garbage First”。

JVM 分区,1.7 和 1.8 的区别,使用元空间的意义

JVM 分区:

  1. 程序计数器:字节码行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,线程私有;

  2. 虚拟机栈:java 方法执行的内存模型,每个方法执行的同时会创建一个栈帧,用于存放局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用到完成的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程,线程私有;

  3. 本地方法栈:存放虚拟机使用到的 Native 方法信息;

  4. 方法区:线程共享,存储已被虚拟机加载的类信息、常量、静态变量,即时编译后的代码数据等。运行时常量池是方法区的一部分,存放编译期生成的各种字面量和符号引用;

  5. 堆:线程共享,存放对象实例,垃圾收集管理的主要区域,java 堆细分为新生代和老年代。

    运行时常量池在 JDK1.6 及之前版本的 JVM 中是方法区的一部分,而在 HotSpot 虚拟机中方法区放在了永久代(Permanent Generation)。所以运行时常量池也是在永久代的。但是 JDK1.7 及之后版本的 JVM 已经将字符串常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

    变动原因:

    1. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
    2. 字符串存在永久代中,现实使用中溢出问题,由于永久代内存经常不够用或发生内存泄露

区别:

jdk 1.8 之前,习惯称方法区为永久代,在 jdk 1.8 之后元数据区取代了永久代。元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。

之所以使用元空间,是因为类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。因此,我们就不会遇到永久代存在时的内存溢出错误,也不会出现泄漏的数据移到交换区这样的事情。

Eden 区,Survivor 区,为什么要二个 Survivor 区?为什么分代?

新生代 Eden 与两个 Survivor 区的解释open in new window

HotSpot JVM 把年轻代分为了三部分:1 个 Eden 区和 2 个 Survivor 区(分别叫 from 和 to)。默认比例为 8:1:1。

一般情况下,新创建的对象都会被分配到 Eden 区(一些大对象特殊处理),这些对象经过第一次 Minor GC 后,如果仍然存活,将会被移到 Survivor 区。对象在 Survivor 区中每熬过一次 Minor GC,年龄就会增加 1 岁,当它的年龄增加到一定程度时,就会被移动到年老代中

在 GC 开始的时候,对象只会存在于 Eden 区和名为 From 的 Survivor 区,Survivor 区 To 是空的。紧接着进行 GC,Eden 区中所有存活的对象都会被复制到 To,而在 From 区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold 来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到 To 区域。经过这次 GC 后,Eden 区和 From 区已经被清空。这个时候,From 和 To 会交换他们的角色,也就是新的 To 就是上次 GC 前的 From,新的 From 就是上次 GC 前的 To。不管怎样,都会保证名为 To 的 Survivor 区域是空的。

二个 Survivor 区原因:

主要是为了解决内存碎片化和效率问题。如果只有一个 Survivor 时,每触发一次 minor gc 都会有数据从 Eden 放到 Survivor,一直这样循环下去。注意的是,Survivor 区也会进行垃圾回收,这样就会出现内存碎片化问题。 碎片化会导致堆中可能没有足够大的连续空间存放一个大对象,影响程序性能。

分代原因:

将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及 GC 频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。

JAVA 虚拟机的作用

解释运行字节码程序消除平台相关性。 jvm 将 java 字节码解释为具体平台的具体指令。一般的高级语言如要在不同的平台上运行,至少需要编译成不同的目标代码。而引入 JVM 后,Java 语言在不同平台上运行时不需要重新编译。Java 语言使用模式 Java 虚拟机屏蔽了与具体平台相关的信息,使得 Java 语言编译程序只需生成在 Java 虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java 虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。

GC 中如何判断对象需要被回收

  1. 首先,进行可达性算法分析,可达性算法基本思路是定义一些列称为"GC-Roots"的对象作为起始阶段,从这些节点向下搜索,搜索走过的路径称为引用链,当一个对象到 GCRoots 没有任何引用链时,即从 GCRoots 到这个对象不可达,则证明此对象是不可用的;
  2. 其次,可达性算法中的不可达对象在真正宣告“死亡”需要回收之前,至少要经过两轮标记过程:
    1. 如果对象不可达,会被第一次标记并且进行一次筛选,筛选条件是此对象有没有必要执行 finalize()方法,当对象没有覆盖 finalize()方法或者这个方法以及被执行过了,那么就视为没有必要再执行;对于那些有必要执行 finalize()方法的对象会被放在一个队列 F-Queue 中,稍后由虚拟机的一个线程去执行逐一执行队列中对象的 finalize 方法
    2. finalize()方法是对象逃脱死亡命运的最后一次机会,稍后 GC 会对 F-Queue 中的对象进行第二次小规模的标记,如果能在 finalize 中成功重新引用,第二次标记时就会将该对象从 F-Queue 集合中移除,而成功脱逃,否则就判定为该对象可回收。

JAVA 虚拟机中,哪些可作为 ROOT 对象?

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中的类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中 JNI(即一般说的 native 方法)引用的对象。

JVM 内存模型呢?

JVM 内存模型:

Java 内存与垃圾回收调优open in new window

JVM 内存模型:虚拟机栈、本地方法栈、方法区、堆、程序计数器

image

jvm 是如何实现线程?

java 虚拟机的多线程是通过线程轮流切换分配处理执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条程序中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。

简单点说,对于单核处理器,是通过快速切换线程执行指令来达到多线程的,因为单核处理器同时只能处理一条指令,只是这种切换速度很快,我们根本不会感知到。

jvm 最大内存限制多少

VM 内存的最大值跟操作系统有很大的关系, 限制于实际的最大物理内存。简单的说就 32 位处理器虽然可控内存空间有 4GB,但是具体的操作系统会给一个限制,这个限制一般是 2GB-3GB(一般来说 Windows 系统下为 1.5G-2G,Linux 系统 下为 2G-3G),而 64bit 以上的处理器就不会有限制了。 JVM 主要管理两种类型的内存:堆和非堆。

  1. 堆内存分配 :JVM 初始分配的内存由 -Xms 指定,默认是物理内存的 1/64;JVM 最大分配的内存由 -Xmx 指定,默认是物理内存的 1/4。默认空余堆内存小于 40%时,JVM 就会增大堆直到 -Xmx 的最大限制;空余堆内存大于 70%时,JVM 会减少堆直到 -Xms 的最小限制。因此服务器一般设置 -Xms、-Xmx 相等以避免在每次 GC 后调整堆的大小。
  2. 非堆内存分配: JVM 使用 -XX:PermSize 设置非堆内存初始值,默认是物理内存的 1/64;由 XX:MaxPermSize 设置最大非堆内存的大小,默认是物理内存的 1/4。

什么是 Java 虚拟机?为什么 Java 被称作是“平台无关的编程语言”?

java 虚拟机是执行字节码文件(.class)的虚拟机进程。java 源程序(.java)被编译器编译成字节码文件(.class)。然后字节码文件,将由 java 虚拟机,解释成机器码(不同平台的机器码不同)。利用机器码操作硬件和操作系统。

因为不同的平台装有不同的 JVM,它们能够将相同的.class 文件,解释成不同平台所需要的机器码。正是因为有 JVM 的存在,java 被称为平台无关的编程语言。

描述一下 JVM 加载 class 文件的原理机制?

JVM 中类的装载是由 ClassLoader 和它的子类来实现的,Java ClassLoader 是一个重要的 Java 运行时系统组件。它负责在运行时查找和装入类文件的类。 Java 中的所有类,都需要由类加载器装载到 JVM 中才能运行。类加载器本身也是一个类,而它的工作就是把 class 文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。

类装载方式,有两种:

  1. 隐式装载,程序在运行过程中当碰到通过 new 等方式生成对象时,隐式调用类装载器加载对应的类到 jvm 中
  2. 显式装载,通过 class.forname()等方法,显式加载需要的类

隐式加载与显式加载的区别:两者本质是一样的。 Java 类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到 jvm 中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。

内存分配与回收策略

  1. 对象优先在 Eden 区分配。当 Eden 区没有内存可分配的时候,虚拟机发出一次 Minor GC(垃圾回收);
  2. 大对象直接进入老年代。大对象指需要大量连续内存空间的 java 对象,典型的很长的字符串以及数组;
  3. 长期存活的对象进入老年代。虚拟机为每一个对象定义一个对象年龄计数器,对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当年龄增加到一定程度(默认 15 岁),就将会被晋升到老年代中;
  4. 动态对象年龄判断。如果在 Surivivor 空间中相同年龄对象大小的总和大于 Surivivor 空间的一半,年龄大于或者等于该年龄的对象直接进入老年代;
  5. 空间分配担保。

JVM 调优

JVM 调优浅谈open in new window

Java 内存与垃圾回收调优open in new window

堆大小设置:

  • 典型设置:
  1. java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
  2. java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
命令含义
-Xmx3550m设置JVM最大可用内存为3550m
-Xms3550m设置JVM初始内存为3550m。此值可以设置与 -Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存
-Xmn2g设置年轻代大小为2G
-Xss128k设置每个线程的堆栈大小, 根据应用的线程所需内存大小进行调整
-XX:NewRatio=4设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxPermGen=16m设置永久代最大值为16m
-XX:MaxTenuringThreshold=0设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代
-XX:PermGen设置永久代内存的初始化大小

选择合适的垃圾收集算法:

  • 吞吐量优先的并行收集器:

    1. java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
    2. java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
    3. 还可以加入参数 -XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM 会自动调整年轻代大小,以满足此值
  • 响应时间优先的并发收集器:

    1. 典型配置:java -Xmx3550m -Xms3550 -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
    2. 还可以使用参数
      -XX:CMSFullGCsBeforeCompaction=5 : 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间后会产生“碎片”,使得运行效率降低。此值设置运行多少次 GC 以后对内存空间进行压缩、整理。 -XX:+UseCMSCompactAtFullCollection : 打开对年老代的压缩。可能会影响性能,但是可以消除碎片
    参数含义
    -XX:+UseParallelGC选择垃圾收集器为并行收集器。此配置仅对年轻代有效。 而年老代仍旧使用串行收集
    -XX:ParallelGCThreads=20配置并行收集器的线程数
    -XX:+UseParallelOldGC配置年老代垃圾收集方式为并行收集。JDK1.6 支持对年老代并行收集
    -XX:+UseConcMarkSweepGC设置年老代为并发收集
    -XX:+UseParNewGC设置年轻代为并行收集。可与 CMS 收集同时使用
    适用情况缺点如何配置
    串行处理器数据量比较小(100M 左右),单处理器下并且对相应时间无要求的应用只能用于小型应用
    并行处理器对吞吐量有高要求,多 CPU,对应用过响应时间无要求的中、大型应用。如后台处理、科学计算垃圾收集过程中应用响应时间可能加长
    并发处理器对响应时间有高要求,多 CPU,对应用响应时间有较高要求的中大型应用。如 Web 服务器/应用服务器

java 垃圾收集监控:

JAVA VisualVM 介绍以及 IDEA 下使用open in new window

  1. IDEA 安装 VisualVM 插件,之后运行工程的时候选择 Run With VisualVM
  2. 本地安装 VisualVM,之后启动进入 VisualVM 页面,安装 java VisualVM 插件,之后在左侧便可以选择监视的相应 IDE(例如 idea 或者 eclipse)

之后便可以看到监控数据:

image-20230120121245897

整个界面分为三个区域,分别为:Spaces、Graphs 和 Histogram:

Visual GC 插件使用open in new window

  • Spaces 窗口:

    image-20230120121140668

    上图呈现了程序运行时我们比较关注的几个区域的内存使用情况:

    • Metaspace:方法区,如果 JDK1.8 之前的版本,就是 Perm,JDK7 和之前的版本都是以永久代(PermGen)来实现方法区的,JDK8 之后改用元空间来实现(MetaSpace)。
    • Old:老年代
    • Eden: 新生代 Eden 区
    • S0 和 S1:新生代的两个 Survivor 区
  • Graphs 窗口:

    该窗口区域包含 8 个图标,以时间为横坐标动态展示各个指标的运行状态。

    image-20230120121109095

    • Compile Time:编译情况,包括编译总数和编译耗时,一个脉冲表示一次 JIT 编译;
    • Class Loader Time:类加载情况,包括加载的类,卸载的类以及耗时
    • GC Time:总的(包含新生代和老年代)gc 情况记录
    • Eden Space:新生代 Eden 区内存使用情况,包括 Eden 区的最大容量、当前容量、当前已使用容量、从开始监控到现在该内存区域一共发生的 Minor GC 次数以及 GC 耗时情况
    • Survivor 0 和 Survivor 1:新生代的两个 Survivor 区内存使用情况
    • Old Gen:老年代内存使用情况
    • Metaspace:方法区内存使用情况
  • Histogram 窗口:

    Histogram 窗口是对当前正在被使用的 Survivor 区内存使用情况的详细描述

    • Tenuring Threshold:晋升到老年代的年龄阈值,但是有动态年龄判断情况出现
    • Max Tenuring Threshold:表示新生代中对象的最大年龄值

Java 垃圾回收调优:

一次针对 idea 启动的 JVM 调优过程记录open in new window

为了能看到 idea 运行时的 gc 日志,添加如下参数输出 gc 日志

-verbose:gc
-XX:+PrintGCDetails
-Xloggc:D:/gc.log

接下来打开 VisualVM 工具,准备监控 idea。

  • 如果在 gc 日志看到大量 Minor GC 信息,是由于年轻代空间不足而导致内存分配失败(Allocation Failure)而发起的,解决方案就是扩大新生代容量,以减少 Minor GC 发生的次数

  • 观察堆空间的变化曲线图, idea 的初始堆空间分配如下:

    -Xms 128m
    -Xmx 750m
    

    当内存占用过高时启动垃圾回收,虚拟机为了减少过于频繁的垃圾回收,会对堆空间进行扩容。 如果看到堆内容扩容次数较多,说明初始堆空间设置的过小,可以将初始值跟最大值一样大,防止因为堆扩容引发的消耗。

  • 老年代空间设置过小或者 Matespace(元空间)空间不足都会发生 Full GC。

    如果 Matespace 的初始值非常小,程序在启动过程中由于空间不足会发生多次的扩容。如果在日志中看到[Full GC (Metadata GC Threshold),说明这次的 Full GC 发生是由于 Metadata GC Threshold 导致的,因为元空间内存不足够而产生扩容导致的 GC。通过参数-XX MetaspaceSize可以设置一个足够大的元空间初始容量

理解 CMS GC 日志open in new window

GC 性能优化文集open in new window

  • 如果你在日志里看到 java.lang.OutOfMemoryError: PermGen space 错误,表达的意思是: 永久代(Permanent Generation) 内存区域已满,主要原因,是加载到内存中的 class 数量太多或体积太大。那么可以尝试使用 -XX:PermGen 和 -XX:MaxPermGen JVM 选项去监控并增加 Perm Gen 内存空间。

  • 如果你在日志里看到 java.lang.OutOfMemoryError: Java heap space 错误,主要是由代码问题导致的:

    • 超出预期的访问量/数据量
    • 内存泄露(Memory leak)

    解决方案: 如果设置的最大内存不满足程序的正常运行, 只需要增大堆内存即可。但很多情况下, 增加堆内存空间并不能解决问题,要从根本上解决问题, 则需要排查分配内存的代码. 简单来说, 需要解决这些问题:

    1. 哪类对象占用了最多内存?
    2. 这些对象是在哪部分代码中分配的。
  • 如果你在日志里看到 java.lang.OutOfMemoryError: Metaspace 错误所表达的信息是: 元数据区(Metaspace) 已被用满,Metaspace 错误的主要原因, 是加载到内存中的 class 数量太多或者体积太大。

    如果抛出与 Metaspace 有关的 OutOfMemoryError , 第一解决方案是增加 Metaspace 的大小. 使用下面这样的启动参数:

    -XX:MaxMetaspaceSize=512m
    

类加载机制,双亲委派模型

参考网址 1open in new window参考网址 2open in new window

概念:

  • 类加载机制:虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,转化解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。类加载的全过程包括加载、验证、准备、解析和初始化五个阶段。
  • 双亲委派模型:每次收到类加载请求时,先将请求委派给父类加载器完成,如果父类加载器无法完成加载,那么子类尝试自己加载

优点:

采用双亲委派的一个好处是: 双亲委派模型很好地解决了各个类加载器的基础类统一问题 (越基础的类由越上层的加载器进行加载)。

如果不是同一个类加载器加载,即时是相同的 class 文件,也会出现判断不想同的情况,从而引发一些意想不到的情况,为了保证相同的 class 文件,在使用的时候,是相同的对象,jvm 设计的时候,采用了双亲委派的方式来加载类。

对于任意一个类,都需要由加载它的类加载器和这个类本身来一同确立其在 Java 虚拟机中的唯一性。如果不是同一个类加载器加载,即时是相同的 class 文件,也会出现判断不想同的情况,从而引发一些意想不到的情况,为了保证相同的 class 文件,在使用的时候,是相同的对象,jvm 设计的时候,采用了双亲委派的方式来加载类。

比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象

jvm 提供了三种系统加载器:

  • 启动类加载器(Bootstrap ClassLoader):C++实现,在 java 里无法获取,负责加载/lib 下的类。
  • 扩展类加载器(Extension ClassLoader): Java 实现,可以在 java 里获取,负责加载/lib/ext 下的类。
  • 系统类加载器/应用程序类加载器(Application ClassLoader):是与我们接触对多的类加载器,我们写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader 返回的就是它。

如何打破:

需要重写 loadClass()方法,双亲委派模型主要体现在 ClassLoader 类中的 loadClass()方法中:

  protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //双亲委派模型的体现
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // If still not found, then invoke findClass in order to find the class.
                    c = findClass(name);
                }
              .......
            }
            return c;
        }
    }

为什么需要破坏双亲委派

因为在某些情况下父类加载器需要委托子类加载器去加载 class 文件。受到加载范围的限制,父类加载器无法加载到需要的文件。

以 Driver 接口为例,由于 Driver 接口定义在 jdk 当中的,而其实现由各个数据库的服务商来提供,比如 mysql 的就写了 MySQL Connector,那么问题就来了,DriverManager(也由 jdk 提供)要加载各个实现了 Driver 接口的实现类,然后进行管理,但是 DriverManager 由启动类加载器加载,只能加载 JAVA_HOME 的 lib 下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载 Driver 实现,从而破坏了双亲委派。

实现自定义加载器

打破双亲委派机制则不仅要继承 ClassLoader 类,还要重写 loadClass 和 findClass 方法。

  1. 如果不想打破双亲委派模型,那么只需要重写 findClass 方法即可
  2. 如果想打破双亲委派模型,那么就重写整个 loadClass 方法

应用场景:

Java 中所有涉及 SPI 的加载动作基本上都采用这种方式,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等, 也就是父类加载器请求子类加载器去完成类加载动作。

例如 JDBC 中,引入线程上下文件类加载器, 程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。

Java SPI 机制:为某个接口寻找服务实现的机制。有点类似 IOC 的思想,就是将装配的控制权移到程序之外。

JVM 如何判断两个类是否相同

Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。

比如一个 Java 类  com.example.Sample,编译之后生成了字节代码文件  Sample.class。两个不同的类加载器  ClassLoaderA 和  ClassLoaderB 分别读取了这个  Sample.class 文件,并定义出两个 java.lang.Class 类的实例来表示这个类。这两个实例是不相同的。对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常  ClassCastException

Java 类的加载机制

面试:类的加载机制open in new window

包括:加载、验证、准备、解析、初始化、使用和卸载

  • 加载:在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口。
  • 验证:确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证的主要过程分为四个阶段,分别是文件格式验证、元数据验证、字节码验证和符号引用验证。
  • 准备:为类变量分配内存并设置类变量的初始值阶段。例如 public static int v = 100;,实际上变量 v 在准备阶段过后的初始值为 0 而不是 100
  • 解析:将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的:
    • CONSTANT_Class_info
    • CONSTANT_Field_info
    • CONSTANT_Method_info
  • 初始化:执行类构造器 <client> 方法的过程。

new 一个对象,JVM 如何操作

  1. 首先检查这个指令的参数能否在常量池中定位到这个类的符号引用,并检查这个符号代表的类是否已经被加载、解析和初始化
  2. 类加载检查通过之后,接下来虚拟机将为新生对象分配内存
  3. 之后对对象进行必要的设置,把一些元数据信息、对象的哈希码和 GC 年龄分代信息存在对象的对象头之中
  4. 执行 <init> 方法,把对象按照程序员的意愿进行初始化

GC

1、java 中内存泄露是啥,什么时候出现内存泄露?内存溢出?

  • 内存泄露
    • 概念:不再会被使用的对象的内存不能被回收,就是内存泄露。
    • 场景:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,例如:
      1. 集合类。集合类仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类如果仅仅是局部变量,根本不会造成内存泄露,在方法栈退出后就没有引用了会被 jvm 正常回收。而如果这个集合类是全局性的变量(比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减,因此提供这样的删除机制或者定期清除策略非常必要。
      2. 单例模式。不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在 JVM 的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被 jvm 正常回收,导致内存泄露
  • 内存溢出 out of memory
    • 概念:是指程序在申请内存时,没有足够的内存空间供其使用,出现 out of memory,包括:
      1. 方法区内存溢出(outOfMemoryError:permgem space)方法区主要存放的是类信息、常量、静态变量等。所以如果程序加载的类过多,或者使用反射、gclib 等这种动态代理生成类的技术,就可能 导致该区发生内存溢出
      2. 线程栈溢出(java.lang.StackOverflowError)线程栈时线程独有的一块内存结构,所以线程栈发生问题必定是某个线程运行时产生的错误。 一般线程栈溢出是由于递归太深或方法调用层级过多导致的。
    • 引起原因:
      1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
      2. 代码中存在死循环或循环产生过多重复的对象实体;
      3. 集合类中有对对象的引用,使用完后未清空,使得 JVM 不能回收;
    • 解决方案:
      1. 修改 JVM 启动参数,直接增加内存。(-Xms,-Xmx 参数一定不要忘记加。)
      2. 检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
      3. 对代码进行排查和分析,找出可能发生内存溢出的位置。

2、minor gc 如果运行的很频繁,可能是什么原因引起的,minorgc 如果运行的很慢,可能是什么原因引起的?

运行的很频繁可能是因为:

  1. 产生了太多朝生夕灭的对象导致需要频繁 minor gc
  2. 新生代空间设置的比较小

运行的很频繁可能是因为:

  1. 新生代空间设置过大。
  2. 对象引用链较长,进行可达性分析时间较长。 3。 新生代 survivor 区设置的比较小,清理后剩余的对象不能装进去需要移动到老年代,造成移动开销。
  3. 内存分配担保失败,由 minor gc 转化为 full gc
  4. 采用的垃圾收集器效率较低,比如新生代使用 serial 收集器

3、阐述 GC 算法

同 JVM 回收算法,标记清除算法、复制算法、标记整理算法和分代收集算法

4、GC 是什么? 为什么要有 GC?

GC 是垃圾回收。

内存处理是编程人员容易出现问题的地方,忘记或错误的内存回收,会导致程序或系统不稳,甚至崩溃。Java 的 GC 功能可自动监控对象是否超过作用域,从而达到自动回收内存的目的。Java 语言没有提供释放已分配的显示操作方法。

5、垃圾回收的优点和原理。并考虑 2 种回收机制

参考open in new window

  • 优点:垃圾回收机制使得 java 程序员在编写程序的时候不再需要考虑内存管理。 有效的防止了内存泄露,可以有效的使用可使用的内存。
  • 原理: 当创建对象时,GC 就开始监视这个对象的地址、大小以及使用情况。通常,GC 采用有向图的方式记录和管理 heap(堆)中的素有对象。通过这种方式确定哪些对象时“可达的”,哪些是“不可达的”。垃圾回收器通常作为一个单独的低级别的线程运行,在不可预知的情况下对内存堆中已经死亡的或很长时间没有用过的对象进行清除和回收, 程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收。
  • 回收机制:分代复制垃圾回收和标记垃圾回收,增量垃圾回收。

6、垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?(垃圾回收)

可以马上回收内存。

程序员可以手动执行 System.gc(),通知 GC 运行,但是 Java 语言规范并不保证 GC 一定会执行。

JVM 性能调优监控工具 jps、jstack、jmap、jhat、jstat、hprof 使用。系统 CPU 过高和 GC 频繁,如何排查

Linux 系统异常排查(main)open in new window

JVM 性能调优监控工具 jps、jstack、jmap、jhat、jstat、hprof 使用详解open in new window

Full GC 次数过多:

  1. top 分析 cpu 使用情况,shift + mshift + p 来按 memory 和 CPU 使用对进程进行排序。找到占用内存较高的进程 id 号
  2. top -Hp 进程id,查看该进程的各个线程运行情况,查看各个线程的 CPU 使用情况,复制其线程的 id 号
  3. 通过线程 id 的十六进制表示在 jstack 日志中查看当前线程具体的堆栈信息

JVM 性能调优监控工具有:

  • jps:输出 JVM 中运行的进程状态信息
  • jmap:查看堆内存使用状况,一般结合 jhat 使用
  • Jstat:JVM 统计监测工具
  • Jstack:查看某个 Java 进程内的线程堆栈信息
jstack pid
jstack -l pid 观察锁持有情况,发生死锁时使用

使用 top -Hp pid 定位到具体的最耗费 CPU 进行对应的线程 PID 之后,使用 printf ”%x\n” pid 打印出十六进制表示方式,例如 PID = 21742,其十六进制为 54ee

使用 jstack 输出进程 21711 的堆栈信息 jstack 21711 | grep 54ee,然后根据线程 ID 的十六进制值 grep

root@ubuntu:/# jstack 21711 | grep 54ee
"PollIntervalRetrySchedulerThread" prio=10 tid=0x00007f950043e000 nid=0x54ee in Object.wait() [0x00007f94c6eda000]

可以看到 CPU 消耗在 PollIntervalRetrySchedulerThread 这个类的 Object.wait() 方法,之后便可以定位具体的方法上

Comments
  • Latest
  • Oldest
  • Hottest
Powered by Waline v2.13.0