JVM的相关知识总结


前言

下面总结了一些个人在学习和面试过程中的一些JVM的相关问题的总结,如若有误,欢迎指正。


1.JVM由哪些部分组成:

类加载器:在JVM启动时或类运行时将所需要的class加载到JVM
执行引擎:执行class文件中的字节码指令
运行时数据区:将内存分为若干的区用于存储
本地方法调用:调用C/C++实现的本地方法

2.JVM的运行时数据区域划分:

先上一张《深入理解Java虚拟机》书中经典的JVM运行时数据区图
在这里插入图片描述
程序计数器:一块较小的内存空间,可以看作当前线程执行的字节码的行号指示器。
虚拟机栈:Java方法执行的内存模型,每个方法在执行时都会创建一个栈帧,每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈的过程。
本地方法栈:与虚拟机栈的作用类似,不过本地方法栈是虚拟机使用到的Native方法。
堆:堆是Java虚拟机中内存最大的一块,是所有线程共享的一块内存区域,此内存区域的唯一作用就是存放对象实例。这里也是JVM自动内存管理的主要区域。
方法区:和堆一样是线程共享的内存区域,用于存储虚拟机加载的类信息、常量、静态变量等数据。

3.简单描述一下垃圾回收机制:

在内存中为新创建的对象分配空间,如果对象只增加不减少,那么内存空间会被耗尽,因此当这些对象失去使用意义时,需要释放掉这些对象,对于对象内存的释放就是垃圾回收机制
由于程序计数器、虚拟机栈、本地方法栈都是线程私有的,这三个区域内存随线程结束就回收,因此垃圾回收主要针对Java堆和方法区这两个线程共享的区域

4.垃圾回收算法:

①标记-清除算法:先标记不需要回收的对象,标记完成后统一回收未标记的对象;效率低、产生内存碎片
②复制算法:将内存按容量分为相等的两块,每次使用其中的一块,将存活的对象移到另一块,把使用空间全部清理;多用于年轻代
③标记-整理算法:标记阶段和标记清除算法一样,只不过在清理阶段,先将存活的对象向一端移动,然后再清理端边界以外的内存;
④分代收集算法:根据不同年龄分区,一般分为年轻代和老年代,然后对不同的分区选择合适的算法

5.JVM自动内存管理,MinorGC和FullGC的触发机制:

在《深入理解Java虚拟机》中写道,JVM的自动内存管理可以归结为自动化地解决两个问题:给对象分配内存、回收分配给对象地内存
内存分配规则:
①对象优先在Eden区分配,当Eden区没有足够的空间进行分配时,JVM发起一次MinorGC
②大对象直接进入老年代,所谓的大对象是指需要大量连续内存空间的对象
③长期存活的对象进入老年代,对象在Eden区经过一次MinorGC后还存活,age+1,当age达到阈值(默认15)晋升到老年代,-XX:MaxTenuringThreshold=15
④动态对象年龄判定,如果在survivor区的相同年龄的所有对象总和大于survivor空间的一半,年龄大于等于该年龄的对象进入老年代
⑤空间分配担保,在发生MinorGC前,JVM会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立,说明MinorGC是安全的;否则,要看是否允许担保失败
若允许,检查空间是否大于历次晋升到老年代对象平均大小,若大于,尝试MinorGC;若小于或者不允许担保失败,进行一次FullGC
MinorGC:当Eden区域满了之后触发,采用复制算法回收新生代垃圾
FullGC:调用System.gc()、老年代空间不足、空间分配担保失败

6.可达性分析导致的STW,或者说HotSpot虚拟机的算法实现:

可达性分析需要在能确保一致性的快照下进行,这时整个系统就像停在某个时间节点,不会出现在分析过程中对象的引用关系还在发生变化,这就导致GC进行时
必须停顿所有Java执行线程(STW),而这个停顿的点也被称为安全点(safePoint),而这时应该考虑怎样控制线程到达安全点就停下来,一种方案是抢先式中
断(在发生GC时中断所有线程,检查是否到达安全点,没有就恢复线程,基本不使用),另一种是主动式中断(设置一个标志,各个线程轮询检查,发现中断标
志就将自己挂起)

7.怎么判断对象能否回收:

引用计数法:简单来说就是如果对象被引用,就把它的引用数加一,不被引用就减一,引用数为零的对象可以回收。但是这里有个致命的问题,有两个对象互相引用,即循环引用,这样会导致两个对象的引用数不为零,永远不会被回收。

可达性分析算法:从一些列的GC Roots出发,看是否有一条完整的引用链达到这个对象,如果没有这样一条引用链,说明这个对象可以被回收。
在这里插入图片描述

8.哪些可以作为GC ROOTS:

虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、被同步锁持有的对象

9.GC日志分析:

控制台打印日志:-XX:+PrintGCDetail(详细日志)、-XX:+PrintGC

[GC (Allocation Failure) [PSYoungGen: 2040K->504K(2048K)] 5225K->5257K(7680K), 0.0101039 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 504K->0K(2048K)] [ParOldGen: 4753K->4929K(5632K)] 5257K->4929K(7680K), [Metaspace: 3453K->3453K(1056768K)], 0.0841143 secs] [Times: user=0.23 sys=0.00, real=0.09 secs]

GC和Full GC用来区分这次垃圾收集的停顿类型,而不是区分新生代和老年代,如果是Full GC说明这次垃圾回收发生STW
PSYoungGen代表垃圾收集器以及年轻代
方括号内2040K->504K(2048K)代表 GC前该内存已用容量->GC后该内存已用容量(该内存的总容量),一一对应的关系
方括号外5225K->5257K(7680K)代表 GC前Java堆已用容量->GC后Java堆已用容量(Java堆总容量),一一对应
0.0101039 secs代表GC所用时间,单位是秒
[Times: user=0.00 sys=0.00, real=0.00 secs]与Linux的time命令输出时间一致

10.逃逸分析、栈上分配、同步消除、标量替换的理解:

逃逸分析并不是直接优化代码,而是为其他优化手段提供依据的分析技术。
①逃逸分析:基本行为是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸;甚至可能被外部线程访问到
譬如赋值给类变量或可以在其他线程中访问到的实例变量,称为线程逃逸

public static StringBuffer craeteStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb;
}

上述方法中sb变量直接返回,这样就有可能被其他方法改变,虽然它是一个局部变量,但它的作用域就不只在方法内部,称其逃逸到方法外部,若想sb不逃出方法,如下:

public static String createStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}

②栈上分配:JVM中在堆上分配创建对象的内存空间,堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用就可以访问对象。当经过逃逸分析发现对象不会
发生逃逸,那么将该对象在栈上分配内存,对象占用的内存空间随栈帧的出栈而销毁。在一般应用中,不会逃逸的局部变量对象占比很大,如果能栈上分配,那大量的对象会
随着方法结束而自动销毁,垃圾回收压力会小很多

③同步消除:线程同步是一个耗时的过程,如果逃逸分析能确定一个变量不会逃出线程,那这个变量的读写就不会有竞争,对这个变量的同步策略可以消除

④标量替换:标量是无法再分解成更小的数据的数据,Java中的原始数据类型就是标量,那些还可以分解的数据叫做聚合量,Java中的对象就是聚合量,如果经过逃逸分析发现

一个对象不会被外界访问,经过JIT优化,把这个对象拆解成成员变量替代,这些成员变量可以在栈上分配而不占用堆空间,这就是标量替换。
综上,我们可以得出一个结论,对象不一定在堆上分配空间。

11.内存泄漏和内存溢出:

内存泄漏:程序中已经动态地分配内存,由于某些原因未释放或无法释放,造成内存的浪费
例如链接没有释放(IO、网络、数据库等)、不合理的变量作用域(一个变量的定义的作用范围大于其使用范围)、静态集合类(将HashMap、List等集合定义为static,导致其和容器相同的生命周期)

内存溢出:程序在申请内存时,没有足够的空间提供使用,导致OOM
例如内存中加载数据庞大(一次数据库请求取出过多数据)、设置内存过小
避免内存泄漏的方法:
①尽量不要是同static成员变量,减少对象声明周期
②及时关闭无用的资源
③不用的对象,手动设置为null

12.设置 -Xms和-Xmx的值相同:

-Xms:分配初始堆内存,默认物理内存的1/64
-Xmx:分配最大堆内存,默认物理内存的1/4
默认空余堆内存小于40%时,堆内存增大直到最大限制;空余内存大于70%时,堆内存减小直到最小限制
为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间消耗,通常设置为相等值

13.类的加载过程:

加载:通过类的全限定名获取定义此类的二进制字节流,将这个字节流代表的静态存储结构转化为方法区的运行时数据结构,在内存生成一个代表此类的java.lang.Class对象
链接:分为验证、准备和解析
验证:检查Class文件的字节流信息符合当前虚拟机的要求,并不会危害虚拟机自身的安全
准备:为类的静态变量分配内存并设置静态变量初始值,这里的初始值是零值
解析:将常量池中的符号引用替换为直接引用
初始化:执行类构造器()方法的过程,初始化变量,进行具体的赋值操作

14.双亲委派模型:

当一个类加载器收到加载某个类的请求时,会先检查当前类加载器是否加载过该类,如果没有加载过,会先向上委托父加载器去尝试加载,直至引导类加载器,若父加载器无法加载,逐层向下,直至本类加载器自己加载。
在这里插入图片描述

15.破坏双亲委派模型:

打破双亲委派机制的有Tomcat、JDBC等
Tomcat容器中可能要部署多个应用,不同的应用之间可能依赖同一个第三方类库的不同版本,因此要保证每个应用程序的类库都是独立的、相互隔离的,同时
容器的类库不能和应用程序类库混淆,因此,如果使用默认的类加载器,由于只关注类的全限定类名,所以导致上述问题无法解决。因此Tomcat有多个自定义
类加载器,例如每个web应用程序都有一个对应的WebappClassLoader,只对当前应用可见,这里不同的WebappClassLoader类加载器就打破了双亲委派机制,不
再去让父类加载器去加载类,而是自己加载

16.类在什么时候初始化/类的初始化时机:

只有对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
①创建类的实例,也就是new的方式
②访问某个类或接口的静态变量,或者对该静态变量赋值
③调用类的静态方法
④反射
⑤初始化某个类的子类,则其父类也会被初始化
⑥JVM启动时被标为启动类的类,直接使用java.exe命令运行主类

17.Java中的引用类型:

强引用:最常见的引用类型,Object o = new Object()新建对象方式,只要引用关系存在,就不会被垃圾回收
软引用:通过SoftReference类实现,用来描述存在非必要的对象,在系统将要发生内存溢出时回收
弱引用:通过Weakreference类实现,用来描述存在非必要的对象,比软引用更弱,只要发生垃圾回收就必被回收
虚引用:通过PhantomReference类实现,不会对对象生存时间产生影响,唯一目的只是为了能在这个对象被回收时收到系统通知

18.判断一个类不再使用:

①该类的所有实例都已经被回收
②加载该类的类加载器已经被回收
③该类对应的java.lang.Class对象没有在任何地方被引用且无法通过反射访问该类的方法

19.垃圾收集器:

年轻代:
①Serial收集器:单线程收集器,指的是只有一条线程进行垃圾回收,需要STW;简单高效,对于限定在单CPU的环境下,有最高的垃圾收集效率
②ParNew收集器:多线程版本的Serial收集器,有多个垃圾回收线程,需要STW;负责回收年轻代,与CMS配合
③Parallel Scavenge收集器:也是年轻代的垃圾收集器,作用和ParNew类似,只不过不同于CMS等垃圾收集器,它关注的是可控制吞吐量(吞吐量=执行用户代码时间/执行用户代码时间+执行垃圾回收时间)
而CMS等是关注降低STW的时间
老年代:
①Serial Old收集器:Serial收集器的老年代版本,使用标记-整理算法
②Parallel Old收集器:Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法;如果注重吞吐量,可以选择这个吞吐量优先的垃圾回收器组合
③CMS(Concurrent Mark Sweep)收集器:以最短回收停顿时间为目标的收集器;分为初始标记(STW)、并发标记、重新标记(STW)、并发清除四个阶段
初始标记(STW):仅仅标记GC Roots能直接关联的对象
并发标记:和用户线程一起运行,进行GC Roots Tracing的过程,即寻找引用链的过程
重新标记(STW):修正并发标记阶段由于用户线程运行而导致的引用变化的对象
并发清除:和用户线程一起运行,清除垃圾对象
基于标记-清除算法,会产生大量的空间碎片
什么是浮动垃圾:
浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次GC才能回收,由于浮动垃圾的存在,因此需要预留一部分内存,意味着CMS不能像其他垃圾收集器一样等待
老年代快慢的时候再回收,如果预留内存不足,会出现Concurrent Mode Failure
JDK8默认的垃圾收集器是ParallelGC,并行垃圾收集器,年轻代是Parallel Scavenge,老年代是Parallel Old
G1垃圾收集器:
并行垃圾回收器,避免对整个堆空间进行垃圾回收,堆内存空间被分成大小相等的区域region,虽然保留了新生代和老年代的概念,但新生代和老年代不再是邬丽隔离的,它们是一部分Region的集合(不需要连续)
一个Region有可能属于Eden、Survivor或者Old区,只能属于一个角色。G1还增加了一个新的内存区域叫做Humongous区,用于存储大对象,如果超过0.5Region,就放到H区

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雅俗共赏zyyyyyy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值