JVM篇

199 阅读9分钟

JVM内存

线程私有:

程序计数器

记录正在执行的虚拟机字节码地址

虚拟机栈

方法执行的内存区,每个方法执行时会在虚拟机栈中创建栈帧(局部变量表、操作数栈、动态链接、方法出口等信息)

本地方法栈

虚拟机的Native方法执行的内存区

线程共享:

对象分配内存的区域

方法区

存放类信息、常量、静态变量、编译器编译后的代码等数据;常量池(存放编译器生成的各种字面常量和符号引用)

说一下类加载机制

简述:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转化解析和初始化,最终形成可以被虚拟机直接使用的Java类型,主要是通过类加载器ClassLoader和它的子类来实现的,这就是虚拟机的类加载机制。

  1. 加载:把类读入到内存中
  2. 连接 - 验证:对字节流进行校验(文件格式验证、元数据验证、字节码验证、符号引用验证)
  3. 连接 - 准备:为静态变量分配内存并设置默认值
  4. 连接 - 解析:将符号引用替换成直接引用
  5. 初始化:主要对类变量的初始化,执行类构造器

说一下JVM中一个对象从创建到销毁的过程

  1. 类加载:会先去方法区找类的信息,如果没有的话,就开始类加载(加载 --> 连接(验证,准备,解析)--> 初始化
  2. 为对象分配内存,到eden区(分配内存方式:指针碰撞和空闲列表)
  3. 同时在JVM进程中,线程会创建一个虚拟机栈,用来跟踪运行中一系列方法的调用过程
  4. YGC -> FGC

为对象分配内存方式

  • 指针碰撞:如果JAVA堆的内存是规整的,即用过的在一边,空闲的在一边,分配内存时直接将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,完成分配。
  • 空闲列表:如果JAVA内存时不规整的,则需要由虚拟机维护一个列表来记录哪些内存时可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。

类加载器有哪些

从JDK 1.2开始, 类加载过程采取了双亲委派机制。更好的保证了 Java 平台的安全性,在该机制中,JVM 自带的 Bootstrap 是根加载器, 其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载。

JVM 不会向 Java 程序提供对 Bootstrap 的引用。

  • 根加载器(BootStrap)一般用本地代码实现,负责加载 JVM 基础核心类库(rt.jar)。

  • 扩展加载器( ExtClassLoader)从 java.ext.dirs 系统属性所指定的目录中加载类库,它的父加载器是 Bootstrap。

  • 系统加载器( AppClassLoader) 又叫应用类加载器,其父类是 Extension。它是应用最广泛的类加载器。它从环境变量 classpath 或者系统属性 java.class.path 所指定的目录中记载类,是用户自定义加载器的默认父加载器。

  • 用户自定义类加载器 ( java.lang.ClassLoader 的子类)父类是AppClassLoader。

加载一个类两次

可以加载两次,但是第二次会把第一次覆盖。

双亲委派机制

在某个类加载class文件时,它首先委托父加载器去加载这个类,依次传递到顶层类加载器,如果顶层加载不了,子加载器才会尝试加载这个类。

作用

  1. 每一个类只会被加载一次,防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不再加载一遍。保证数据安全
  2. 保证核心.class不能被篡改,通过委托方式,不回去篡改核心.class,即使篡改也不会去加载,即使加载了也不会是同一个.class,不同的加载器加载同一个.class也不是同一个class对象。这样保证了class执行安全
  3. 每一个类都会被尽可能的加载

error和exception有什么区别?

error 表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出。不可能指望程序能处理这样的情况。exception表示一种设计或实现问题。也就是说,它表示如果程序运行正常,从不会发生的情况。

System.out.println(),System是什么,out是什么,println又是什么?

System是捆绑在java.lang包中的最终类。

out是打印流类的参考,它是系统类的静态成员。

println是一种打印流类的方法,它捆绑在 java.io 包中打印输出。

如何识别出垃圾?

常用有两种方式:

引用计数法,这种难以解决对象之间的循环引用的问题。

可达性分析算法,主流的JVM采用的是这种方式。

简单聊聊垃圾回收算法

  • 标记-清除算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

  • 复制算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。缺点是浪费空间,优点是回收速度快,没碎片。

  • 标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,完成碎片整理。

  • 分代收集算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,我觉得它更像是一种思想,而不是算法。

常见的垃圾回收器

  • Serial 收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。

  • ParNew 收集器,ParNew 收集器其实就是 Serial 收集器的多线程版本。

  • Parallel 收集器,Parallel Scavenge 收集器类似 ParNew 收集器,Parallel 收集器更关注系统的吞吐量。

  • Parallel Old 收集器,Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法

  • CMS 收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

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

Java对象的创建过程清楚吗?

  1. JVM 遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。然后加载这个类(类加载过程在后边讲)

  2. 为对象分配内存。一种办法“指针碰撞”、一种办法“空闲列表”,最终常用的办法“本地线程缓冲分配(TLAB)”

  3. 将除对象头外的对象内存空间初始化为 0

  4. 对对象头进行必要设置

排查GC问题-jmap

  1. jmap -heap {PID} 查看堆heap占用情况
  2. jmap -histo {PID} | head -n 100 输出对象中最大的100个对象

排查CPU过高问题

  1. top 找到cpu消耗高的进程id
  2. top -HP {PID} 找到cpu消耗高的线程id
  3. printf "%x\n" tid 将线程号 10进制转16进制
  4. jstack {pid} | grep {tid} -A 30 在线从线程的堆栈中查看线程信息

OOM的原因和解决方案

  1. 线程数太多
  2. 打开太多文件
  3. 内存不足

堆溢出

代码中可能存在大对象分配;也可能存在内存泄漏,导致多次GC之后,还是无法找到一块足够大的内存容纳当前对象。

解决方法:

  1. 检查是否有大对象的分配,很有可能是有大数组分配
  2. 提供jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否有内存泄漏的问题
  3. 如果没有明显的内存泄漏,使用-Xmx加大堆内存
  4. 检查是否有大量的自定义的Finalable对象,也有可能时框架内部提供的,考虑其存在的必要性

永久代/元空间溢出

永久代时HotSot虚拟机对方法区的具体实现,存放了被虚拟机加载的类信息、常量、静态变量JIT编译后的代码等。JAVA8之后,元空间替换了永久代,元空间使用的时本地内存。可能原因在运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载。也可能时应用长时间运行,没有重启。

解决办法:

  1. 检查是否永久代空间或者元空间设置过小
  2. 检查代码中是否存在大量的反射操作
  3. dump之后通过mat检查是否存在大量由于反射生成的代理类

方法栈溢出

出现这种异常,基本上时创建了大量线程导致的

解决办法:

  1. 通过Xss降低每个线程的容量
  2. 线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制

JVM8为什么要增加元空间

参考:

JVM面试题:mp.weixin.qq.com/s/FEPtidbx7…

对象创建和分配策略:blog.csdn.net/l6108003/ar…