JVM 常见内容汇总

面试题

对象

对象的创建

    【创建方式】:
    
        1. new 关键字
        2. 通过反射机制,使用 Constructor 类的 newInstance

    【创建过程】:
    
        1. 在常量池查找类的符号引用,确定类是已经被加载,否则执行类加载过程。
        2. 为对象分配内存并赋初始值 。
        3. 设置对象头信息。
        4. 对象初始化。

分配内存

    【确定内存大小】对象需要分配的内存在类加载年阶段就已经确定
        
        1. 对象头
        2. 实例数据
        3. 对齐填充
        
    【分配内存】
    
        指针碰撞(用于内存规整的情况):只需将内存指针移动相应大小的位置即可。
        空闲列表(用于内存非规整的情况):记录内存的使用情况
    
    【线程安全】
        CAS+失败重试
        本地线程分配缓冲(TLAB):为每个线程分配一块私有的内存。
    

对象头

    三部分组成:Mark Word、指向类元对象的指针、数组长度(对象为数组时存在)
    
    【Mark Word】
        1. 锁信息
        2. GC 分代年龄
        3. 线程ID
        4. HashCode

内存溢出

内存溢出与内存泄漏

  • 内存溢出:系统无法再分配内存空间。
  • 内存泄漏:分配的内存未释放持续占用,会导致内存溢出。

JVM 哪些区域会内存溢出

    1. 堆      java.lang.OutOfMemoryError: Java heap space
    2. 元数据区 java.lang.OutOfMemoryError: Metaspace
    3. 直接内存 java.lang.OutOfMemoryError: Direct buffer memory

内存泄漏排查

    1. 生成dump文件。
        1. jmap 命令主动 dump。       jmap -dump:live,format=b,file=/data/program/micro/dump.hprof 1343
        2. jvm 参数 OOM 时自动 dump。 -XX:+HeapDumpOnOutOfMemoryError
    
    2. Eclipse MAT 工具分析 dump 文件
        1. Overview 显示内存占用情况
        2. Dorminator Tree(支配树) 显示对象与其持有的所有对象的内存情况
            1. shallow heap:指的是某一个对象所占内存大小
            2. retained heap:指的是一个对象的 retained se t所包含对象所占内存的总大小
            3. retained set: 指的是这个对象本身和他持有引用的对象和这些对象的 retained set所占内存大小的总和
        3. List Objects 罗列出对象在引用树中的节点,可以顺着节点查看具体是哪些类出现泄漏
        4. OQL 对象查询语句 
            1. select * from com.example.MyClass
            2. select * from instanceof java.lang.String
            
    

GC收集

GC Roots

可达性分析算法:以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。

  1. 虚拟机栈/本地方法栈中引用的对象
  2. 静态变量、常量对象

元数据区的垃圾回收条件

  1. 该类所有的实例都已经被回收
  2. 加载该类的 ClassLoader 已经被回收
  3. 该类对应的 Class 对象没有在任何地方被引用

引用类型

  1. 强引用:不会被回收
  2. 软引用:内存不足时,会被回收
  3. 弱引用:下一次 GC 会被回收
  4. 虚引用:不对对象的 GC 构成影响

垃圾回收算法

    【标记-清除】

        1. 标记存活的对象,清除无标记对象
        2. 会产生内存碎片,在无法分配内存时触发 FullGC

    【标记-整理】

        1. 标记存活对象,清除无标记对象
        2. 移动存活对象进行内存整理
        3. 不会产生内存碎片 
    
    【复制】
        
        1. 将内存划分为 2 块区域,每次只使用 1 块区域
        2. 将存活的对象复制到另 1 块区域,清除当前区域
        3. 内存空间利用率低,不会产生内存碎片
        
        【HotSpot】
            
            1. 内存划分为 1 个 Eden 和 2 个 Survivor 8:1:1,即内存利用率 90%
            2. 优先使用 1 个 Eden 和 1 个 Survivor,将存活对象复制到另 1 个 Survivor
            3. 依赖于老年代进行空间分配担保,担保失败触发 FullGC

垃圾收集器

GC收集器

内存分配策略

内存分配策略

  1. 对象优先在 Eden 分配。 Eden 空间不够时,发起 Minor GC
  2. 大对象直接进入老年代。
  3. 长期存活的对象进入老年代。-XX:MaxTenuringThreshold 设置年龄阈值。
  4. 动态对象年龄判定。 某年龄对象数超过 Survivor 空间的一半,则所有>=该年龄对象进入老年代。
  5. 空间分配担保。担保失败触发 Full GC。

分代GC

  • 【MinorGC/YoungGC】对新生代 GC
  • 【OldGC】对老年代 GC
  • 【FullGC】对所有 GC

FullGC 触发条件

  1. 调用 System.gc() 建议 JVM FullGC
  2. 老年代空间不足
  3. 空间分配担保失败

GC 日志

[ GC [ PSYoungGen:  1351K -> 288K (18432K) ]  1351K -> 288K (59392K), 0.0012389 secs ]  [ Times: user=0.00 sys=0.00, real=0.00 secs ] 
[ Full GC (System)  [ PSYoungGen:  288K -> 0K (18432K) ]  [ PSOldGen:  0K -> 160K (40960K) ]  288K -> 160K (59392K)  [ PSPermGen:  2942K -> 2942K (30720K) ],  0.0057649 secs ] [ Times:  user=0.00  sys=0.00,  real=0.01 secs ] 

 
Heap 
PSYoungGen      
      total 18432K, used 327K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)  
      eden  space 16384K, 2% used [0x00000000fec00000,0x00000000fec51f58,0x00000000ffc00000)   
      from  space 2048K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x0000000100000000)  
      to    space 2048K, 0% used [0x00000000ffc00000,0x00000000ffc00000,0x00000000ffe00000) 
 
PSOldGen        
      total  40960K, used 1184K [0x00000000fc400000, 0x00000000fec00000, 0x00000000fec00000)  
 
 PSPermGen       
      total  30720K, used 2959K [0x00000000fa600000, 0x00000000fc400000, 0x00000000fc400000)  
      object space 30720K, 9% used [0x00000000fa600000,0x00000000fa8e3ce0,0x00000000fc400000)
  1. 新生代

    • PSYoungGen : Parallel Scanvenge
    • ParNew : ParNew
    • defNew : Serial
  1. 老年代

    • ParOldGen : Parallel Old
    • PSOldGen : Serial Old

虚拟机类加载机制

类加载过程

类加载包括以下 5 个阶段:

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)

JVM 加载 Class 文件的过程

    1. 【加载】读入二进制 .class 文件字节流 
    2. 【验证】对二进制验证完整性与 JVM 兼容性
    3. 【准备】为类变量分配内存并赋初值 0
    4. 【解析】符号引用替换为直接引用
    5. 【初始化】执行类初始化语句

类加载时机

    1. new、getstatic、putstatic、invokestatic 这四条字节码指令时,即 new 对象、查询/设置类静态字段、执行静态方法
    2. 对类进行反射调用时
    3. 加载一个类的时候,如果发现其父类还没有加载时,先对父类进行加载。
    4. jvm 启动时,对 main 方法所在类加载

    *特殊场景*
    
    1. 通过子类引用父类的静态字段,不会导致子类初始化
    System.out.println(SubClass.value); // value 字段在 父类中定义 中定义
    
    2. 通过数组定义来引用类,不会触发此类的初始化
    SuperClass[] array = new SuperClass[10]; //该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
    
    3. 常量引用
    System.out.println(ConstClass.HELLOWORLD); // 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

类加载器分类

        1. 启动类加载器(Bootstrap ClassLoader)
        启动类加载器无法被 Java 程序直接引用,是虚拟机自身的一部分。负责将存放在 \lib 或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中
        
        2. 扩展类加载器(Extension ClassLoader)
        负责将 /lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中。
        
        3. 应用程序类加载器(Application ClassLoader)
        ClassLoader.getSystemClassLoader() 获取,负责加载用户类路径(ClassPath)上所指定的类库

双亲委派模型

该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。
这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。

双亲委派模型
    1. 一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
    2. 通过双亲委派模型加载自定义 java.lang.String 类时,由于顶层为启动类加载器,因此会加载到 \lib 中的 java.lang.String ,而不是自定义的 java.lang.String 类。
    3. 自定义类加载器时,父类 loadClass() 实现了双亲委派,子类只需重写 Class findClass(String name); (通过权限定类名 获取 Class 对象) 即可,通过 defineClass 方法 将 byte 数组转为 Class 对象。
// 自定义类加载器
public class FileSystemClassLoader extends ClassLoader {

    protected Class findClass(String name) throws ClassNotFoundException {

        // 通过全权限定类名读取 byte[]
        byte[] classData = getClassData(name);
        // defineClass 方法将 byte[] 转为 Class 对象
        return defineClass(name, classData, 0, classData.length);
    }

    // 通过全权限定类名读取 byte[]
    private byte[] getClassData(String className) {return ...;}

    // 返回文件位置
    private String classNameToPath(String className) {return ....;}
    
    private String rootDir;
    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }
}

JVM调优

JVM 调优的工具

    【jps】输出 JVM 进程信息
        jps -l 输出 main 方法所在类全限定类名
        jps -m 输出 main 方法参数
        jps -v 输出 JVM 参数
    【jstack】输出线程堆栈信息
        jstack -l [pid] 堆栈+锁信息
    【jmap】 输出堆信息
        jmap -dump:live,format=b,file=${filePath}.hprof [pid]
    
    【jstat】输出 JVM 类加载、内存、垃圾收集等运行数据统计信息
        jstat -class [pid]      // 类加载统计
        jstat -compiler [pid]   // 编译统计
        jstat -gc [pid]         // GC 统计
        
        【JVisualVM】可视化工具,提供了对内存、线程、类加载、GC等信息的可视化数据展示
    【JConsole】可视化工具,提供了对内存、线程、类加载等信息的可视化数据展示

排查 CPU 100%

  • 定位到 CPU 占用率最高的线程
    1. top -o %CPU   按CPU使用率排序查看进程信息
    2. top -Hp [pid] 查看进程下所有线程信息,通过 TIME+ 可以确定 CPU 占用最多的线程
    3. printf "%x" [线程id] 获得16进制线程id
    4. jstack  [JVM进程id] | grep  [16进制线程id]
    5. 通过堆栈信息定位代码

排查内存溢出

  • 定位到内存泄漏点
    1. 获取 dump 文件 。 jmap -dump:live,format=b,file=${filePath}.hprof
    2. 通过 Eclipse MAT 工具载入 dump 文件。
    3. 查看 overview、支配树、list objects 查看引用树、OQL语句查询某些类的对象情况。
    4. 找到内存泄漏点,通过堆栈信息定位代码。

排查程序无响应

  • 可能的原因:线程被阻塞、线程死锁
    【线程阻塞】
        1. jstack -l [pid] | grep -E 'BLOCKED|TIMED_WAITING'
        
         // block 
         state = BLOCKED
         // sleep(time)
         java.lang.Thread.State: TIMED_WAITING (sleeping)
         // parkUntil(deadline)           
         java.lang.Thread.State: TIMED_WAITING (parking)
         // wait(time)     
         java.lang.Thread.State: TIMED_WAITING (on object monitor)  
        
        2. 观察是否线程均被 BLOCKED(阻塞) 或 TIMED_WAITING(期限等待) 消耗导致无法执行任务
        
    【线程死锁】
        1. jstack -l [pid] | grep 'waiting on condition'

        "VM Periodic Task Thread" os_prio=0 tid=0x00007f602c01bc80 nid=0x44f waiting on condition

你可能感兴趣的:(JVM 常见内容汇总)