JVM知识点汇总

JVM是什么?

  • JVM它是 Java Virtual Machine 的缩写,主要是通过在实际计算机模仿各种计算机功能来实现的。(概念)
  • 由堆、方法区、栈、本地方法栈、程序计算器等部分组成的,其中方法回收堆和方法区是共享区,也就是谁都可以使用,而栈和程序计算器、本地方法栈区是归JVM的。(组成)
  • Java能够被称为“一次编译,到处运行”的原因就是Java屏蔽了很多的操作系统平台相关信息,使得Java只需要生成在JVM虚拟机运行的目标代码也就是所说的字节码,就可以在多种平台运行。(特点:平台无关性)

JRE、JDK和JVM的关系?

JRE(Java Runtime Environment, Java运行环境)是Java平台,所有的程序都要在JRE下才能够运行。包括JVM和Java核心类库和支持文件。

JDK(Java Development Kit,Java开发工具包)是用来编译、调试Java程序的开发工具包。包括Java工具(javac/java/jdb等)和Java基础的类库(java API )。

JVM(Java Virtual Machine, Java虚拟机)是JRE的一部分。JVM主要工作是解释自己的指令集(即字节码)并映射到本地的CPU指令集和OS的系统调用。Java语言是跨平台运行的,不同的操作系统会有不同的JVM映射规则,使之与操作系统无关,完成跨平台性。
在这里插入图片描述

JVM生命周期(何时启动,何时退出)

Java实例对应一个独立运行的Java程序(进程级别)

  1. 启动。启动一个Java程序,一个JVM实例就产生。拥有public static void main(String[] args)函数的class可以作为JVM实例运行的起点。

  2. 运行。main()作为程序初始线程的起点,任何其他线程均可由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM使用,程序可以指定创建的线程为守护线程。

  3. 消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。

JVM执行引擎实例则对应了属于用户运行程序线程它是线程级别的。

可以描述一下 class 文件的结构吗?

Class 文件包含了 Java 虚拟机的指令集、符号表、辅助信息的字节码(Byte Code),是实现跨操作系统和语言无关性的基石之一。
一个 Class 文件定义了一个类或接口的信息,是以 8 个字节为单位,没有分隔符,按顺序紧凑排在一起的二进制流。
用 “无符号数” 和 “表” 组成的伪结构来存储数据。

  • 无符号数:基本数据类型,用来描述数字、索引引用、数量值、字符串值,如u1、u2 分别表示 1 个字节、2 个字节
  • 表:无符号数和其他表组成,命名一般以 “_info” 结尾

组成部分

  1. 魔数 Magic Number
    Class 文件头 4 个字节,0xCAFEBABE
    作用是确定该文件是 Class 文件

  2. 版本号
    4 个字节,前 2 个是次版本号 Minor Version,后 2 个主版本号 Major Version
    从 45 (JDK1.0) 开始,如 0x00000032 转十进制就是 50,代表 JDK 6
    低版本的虚拟机跑不了高版本的 Class 文件

  3. 常量池
    常量容量计数值(constant_pool_count),u2,从 1 开始。如 0x0016 十进制 22 代表有 21 项常量
    每项常量都是一个表,目前 17 种
    特点:Class 文件中最大数据项目之一、第一个出现表数据结构

  4. 访问标志
    2 个字节,表示类或接口的访问标志

  5. 类索引、父类索引、接口索引集合
    类索引(this_class)、父类索引(super_class),u2
    接口索引集合(interfaces),u2 集合
    类索引确定类的全限定名、父类索引确定父类的全限定名、接口索引集合确定实现接口
    索引值在常量池中查找对应的常量

  6. 字段表(field_info)集合
    描述接口或类申明的变量
    fields_count,u2,表示字段表数量;后面接着相应数量的字段表
    9 种字段访问标志

  7. 方法表(method_info)集合
    描述接口或类申明的方法
    methods_count,u2,表示方法表数量;后面接着相应数量的方法表
    12 种方法访问标志
    方法表结构与字段表结构一致

  8. 属性表(attribute_info)集合
    class 文件、字段表、方法表可携带属性集合,描述特有信息
    预定义 29 项属性,可自定义写入不重名属性

Java虚拟机在执行Java程序的过程中的内存区域划分是怎样的

根据《Java虚拟机规范》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:
在这里插入图片描述

  • 程序计数器:当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。

  • 虚拟机栈:虚拟机栈中执行每个方法的时候,都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。

  • 本地方法栈:与虚拟机栈发挥的作用相似,相比于虚拟机栈为Java方法服务,本地方法栈为虚拟机使用的Native方法服务,执行每个本地方法的时候,都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。

  • 堆:堆是Java对象的存储区域,任何用new字段分配的Java对象实例和数组,都被分配在堆上,Java堆可使用-Xms -Xmx进行内存控制,值得一提的是从JDK1.7版本之后,运行时常量池从方法区移到了堆上。

  • 方法区:它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据,方法区在JDK1.7版本及以前被称为永久代,从JDK1.8永久代被移除。

JVM类加载顺序是怎样的?什么事双亲委派模型?

这里的双亲会造成理解误差,实际上只是去委派给父级加载器或是父级的父级加载器去进行加载。这里的parents不应该是指双亲,而是多级的父类层层向上传递的意思。
JDK 9 之前

  • 启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。
  • 其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如:
    • 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
    • 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
    • 自定义类加载器

JDK 9 开始 Extension ClassLoader 被 Platform ClassLoader 取代,启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader

类加载代码逻辑

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
 // 首先,检查请求的类是否已经被加载过了
 Class c = findLoadedClass(name);
 if (c == null) {
   try {
     if (parent != null) {
       c = parent.loadClass(name, false);
     } else {
       c = findBootstrapClassOrNull(name);
     }
   } catch (ClassNotFoundException e) {
     // 如果父类加载器抛出ClassNotFoundException
     // 说明父类加载器无法完成加载请求
   }
   if (c == null) {
     // 在父类加载器无法加载时
     // 再调用本身的findClass方法来进行类加载
     c = findClass(name);
   }
 }
 if (resolve) {
   resolveClass(c);
 }
 return c;
} 

列举一些你知道的打破双亲委派机制的例子。为什么要打破?

  • JNDI 通过引入线程上下文类加载器,可以在 Thread.setContextClassLoader 方法设置,默认是应用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。

  • Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。打破的目的是为了完成应用间的类隔离。

  • OSGi,实现模块化热部署,为每个模块都自定义了类加载器,需要更换模块时,模块与类加载器一起更换。其类加载的过程中,有平级的类加载器加载行为。打破的原因是为了实现模块热替换。

  • JDK 9,Extension ClassLoader 被 Platform ClassLoader 取代,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添加模块化的特性。

强引用、软引用、弱引用、虚引用是什么,有什么区别?

  • 强引用,就是普通的对象引用关系,如 String s = new String(“ConstXiong”)

  • 软引用,用于维护一些可有可无的对象。只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
    用处: 软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。

  1. 如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
    2.如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出

如下代码:

Browser prev = new Browser();               // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用        
if(sr.get()!=null){ 
    rev = (Browser) sr.get();           // 还没有被回收器回收,直接获取
}else{
    prev = new Browser();               // 由于内存吃紧,所以对软引用的对象回收了
    sr = new SoftReference(prev);       // 重新构建
}
  • 弱引用,相比软引用来说,要更加无用一些,它拥有更短的生命周期,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。WeakReference 实现

  • 虚引用是一种形同虚设的引用,在现实场景中用的不是很多,它主要用来跟踪对象被垃圾回收的活动。PhantomReference 实现

JVM 如何确定垃圾对象?

判断对象是否可回收的算法有两种:

  • Reference Counting GC,引用计数算法
  • Tracing GC,可达性分析算法

JVM 各厂商采用的基本都是可达性分析算法,通过 GC Roots 来判定对象是否存活,从 GC Roots 向下追溯、搜索,会产生 Reference Chain。当一个对象不能和任何一个 GC Root 产生关系时,就判定为垃圾。

软引用和弱引用,也会影响对象的回收。内存不足时会回收软引用对象;GC 时会回收弱引用对象。

哪些是 GC Roots?

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。

  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。

  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。

  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。

  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。

  • 所有被同步锁(synchronized关键字)持有的对象。

  • 反映 Java 虚拟机内部情况的 JMXBean、JVMTI中注册的回调、本地代码缓存等。

谈谈你知道的垃圾回收算法

大部分垃圾收集器遵从了分代收集(Generational Collection)理论。
针对新生代与老年代回收垃圾内存的特点,提出了 3 种不同的算法:

  1. 标记-清除算法(Mark-Sweep)
    标记需回收对象,统一回收;或标记存活对象,回收未标记对象。
    缺点:
    大量对象需要标记与清除时,效率不高
    标记、清除产生的大量不连续内存碎片,导致无法分配大对象

  2. 标记-复制算法(Mark-Copy)
    可用内存等分两块,使用其中一块 A,用完将存活的对象复制到另外一块 B,一次性清空 A,然后改分配新对象到 B,如此循环。
    缺点:
    不适合大量对象不可回收的情况,换句话说就是仅适合大量对象可回收,少量对象需复制的区域
    只能使用内存容量的一半,浪费较多内存空间

  3. 标记-整理算法(Mark-Compact)
    标记存活的对象,统一移到内存区域的一边,清空占用内存边界以外的内存。
    缺点:
    移动大量存活对象并更新引用,需暂停程序运行

说一下垃圾分代收集的过程

分为新生代和老年代,新生代默认占总空间的 1/3,老年代默认占 2/3。
新生代使用复制算法,有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。
当新生代中的 Eden 区内存不足时,就会触发 Minor GC,过程如下:

  • 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区;

  • Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;

  • 移动一次,对象年龄加 1,对象年龄大于一定阀值会直接移动到老年代

  • Survivor 区相同年龄所有对象大小的总和 > (Survivor 区内存大小 * 这个目标使用率)时,大于或等于该年龄的对象直接进入老年代。其中这个使用率通过 -XX:TargetSurvivorRatio 指定,默认为 50%

  • Survivor 区内存不足会发生担保分配

  • 超过指定大小的对象可以直接进入老年代

  • Major GC,指的是老年代的垃圾清理,但并未找到明确说明何时在进行Major GC

  • FullGC,整个堆的垃圾收集,触发条件:

  1. 每次晋升到老年代的对象平均大小>老年代剩余空间
  2. MinorGC后存活的对象超过了老年代剩余空间
  3. 元空间不足
  4. System.gc() 可能会引起
  5. CMS GC异常,promotion failed:MinorGC时,survivor空间放不下,对象只能放入老年代,而老年代也放不下造成;concurrent mode failure:GC时,同时有对象要放入老年代,而老年代空间不足造成
  6. 堆内存分配很大的对象

谈谈你知道的垃圾收集器

Serial
特点:

  • JDK 1.3 开始提供
  • 新生代收集器
  • 无线程交互开销,单线程收集效率最高
  • 进行垃圾收集时需要暂停用户线程
  • 适用于客户端,小内存堆的回收

ParNew
特点:

  • 是 Serial 收集器的多线程并行版
  • JDK 7 之前首选的新生代收集器
  • 第一款支持并发的收集器,首次实现垃圾收集线程与用户线程基本上同时工作
  • 除 Serial 外,只有它能与 CMS 配合

Parallel Scavenge
特点:

  • 新生代收集器
  • 标记-复制算法
  • 多线程并行收集器
  • 追求高吞吐量,即最小的垃圾收集时间
  • 可以配置最大停顿时间、垃圾收集时间占比
  • 支持开启垃圾收集自适应调节策略,追求适合的停顿时间或最大的吞吐量

Serial Old
特点:

  • 与 Serial 类似,是 Serial 收集器的老年代版本
  • 使用标记-整理算法

Parallel Old
特点:

  • JDK 6 开始提供
  • Parallel Scavenge 的老年代版
  • 支持多线程并发收集
  • 标记-整理算法
  • Parallel Scavenge + Parallel Old 是一个追求高吞吐量的组合

CMS
特点:

  • 标记-清除算法
  • 追求最短回收停顿时间
  • 多应用于关注响应时间的 B/S 架构的服务端
  • 并发收集、低停顿
  • 占用一部分线程资源,应用程序变慢,吞吐量下降
  • 无法处理浮动垃圾,可能导致 Full GC
  • 内存碎片化问题

G1
特点:

  • JDK 6 开始实验,JDK 7 商用
  • 面向服务端,JDK 9 取代 Parallel Scavenge + Parallel Old
  • 结合标记-整理、标记-复制算法
  • 首创局部内存回收设计思路
  • 基于 Region 内存布局,采用不同策略实现分代
  • 不再使用固定大小、固定数量的堆内存分代区域划分
  • 优先回收价收益最大的 Region
  • 单个或多个 Humongous 区域存放大对象
  • 使用记忆集解决跨 Region 引用问题
  • 复杂的卡表实现,导致更高的内存占用,堆的 10%~20%
  • 全功能垃圾收集器
  • 追求有限的时间内最高收集效率、延迟可控的情况下最高吞吐量
  • 追求应付内存分配速率,而非一次性清掉所有垃圾内存
  • 适用于大内存堆

Shenandoah
特点:

  • 追求低延迟,停顿 10 毫秒以内
  • OpenJDK 12 新特性,RedHat 提供
  • 连接矩阵代替记忆集,降低内存使用与伪共享问题出现概率

ZGC
特点:

  • JDK 11 新加的实验性质的收集器
  • 追求低延迟,停顿 10 毫秒以内
  • 基于 Region 内存布局
  • 未设分代
  • 读屏障、染色指针、内存多重映射实现可并发的标记-整理算法
  • 染色指针和内存多重映射设计精巧,解决部分性能问题,但降低了可用最大内存、操作系统受限、只支持 32 位、不支持压缩指针等
  • 成绩亮眼、性能彪悍

生产环境用的什么JDK?如何配置的垃圾收集器?

Oracle JDK 1.8
JDK 1.8 中有 Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1,默认使用 Parallel Scavenge + Parallel Old。

  • Serial 系列是单线程垃圾收集器,处理效率很高,适合小内存、客户端场景使用,使用参数 -XX:+UseSerialGC 显式启用。
  • Parallel 系列相当于并发版的 Serial,追求高吞吐量,适用于较大内存并且有多核CPU的环境,默认或显式使用参数 -XX:+UseParallelGC 启用。可以使用 -XX:MaxGCPauseMillis 参数指定最大垃圾收集暂停毫秒数,收集器会尽量达到目标;使用 -XX:GCTimeRatio 指定期望吞吐量大小,默认 99,用户代码运行时间:垃圾收集时间=99:1。
  • CMS,追求垃圾收集暂停时间尽可能短,适用于服务端较大内存且多 CPU 的应用,使用参数 -XX:+UseConcMarkSweepGC 显式开启,会同时作用年轻代与老年代,但有浮动垃圾和内存碎片化的问题。
  • G1,主要面向服务端应用的垃圾收集器,适用于具有大内存的多核 CPU 的服务器,追求较小的垃圾收集暂停时间和较高的吞吐量。首创局部内存回收设计思路,采用不同策略实现分代,不再使用固定大小、固定数量的堆内存分代区域划分,而是基于 Region 内存布局,优先回收价收益最大的 Region。使用参数 -XX:+UseG1GC 开启。

我们生产环境使用了 G1 收集器,相关配置如下

-Xmx12g
-Xms12g
-XX:+UseG1GC
-XX:InitiatingHeapOccupancyPercent=45
-XX:MaxGCPauseMillis=200
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=512m
-XX:G1HeapRegionSize 未指定

核心思路:
每个内存区域设置上限,避免溢出
堆设置为操作系统的 70%左右,超过 8 G,首选 G1
根据老年代对象提升速度,调整新生代与老年代之间的内存比例
等过 GC 信息,针对项目敏感指标优化,比如访问延迟、吞吐量等

G1适合8/16G以上的内存使用,原因在于G1rescan更快,清除垃圾时虽然是stop the world但是可控,CMS虽然是并发但是不可控,大块内存要回收会影响到应用程序的性能。另外由于G1在清理垃圾时使用STW,所以可以采用标记整理算法,没有内存碎片问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值