JVM面试总结高频考点

JVM概述

作用

构成部分

类加载子系统

作用:

类加载器子系统负责从文件系统或者网络中加载 class 文件。

主负责加载类, 有执行引擎执行,存放在方法区(元空间)

扮演者的是一个快递员的角色

类加载过程

1.加载

根据类的地址,从硬盘上读取类的信息,

将信息读入到方法区,生成Class类的对象

2.链接

验证: 验证字节码文件格式是否是当前虚拟机所支持的文件格式,语法格式

准备: 为静态成员分配默认值(int 默认值0) 注意static final在编译期间赋值

解析: 将字节码中符号引用 替换 成 直接引用

例如: 编写代码 方法1 中调用 方法2 (符号引用)

类加载到内存后把符号的引用地址 换成 内存的地址引用

3.初始化

类什么时候初始化?

1 )创建类的实例,也就是 new 一个对象

2)访问某个类或接口的静态变量,或者对该静态变量赋值

3)调用类的静态方法

4)反射(Class.forName(“”))

5)初始化一个类的子类(会首先初始化子类的父类)

类的初始化顺序

先初始化静态的,多个静态的按照从上向下的顺序执行,

如果类有父类,则先初始化父类的静态,然后是子类.

如果是创建对象,先调用父类的构造方法,然后是子类自己的构造方法

类加载器

站在JVM的角度划分: 启动类加载器(不是java语言写的),

其他类加载(都是java语言写的)

站在开发者的角度:

启动类加载器(引导类加加载器)

这个类加载器使用 C/C++语言实现,嵌套在 JVM 内部.它用来加载 java 核心类

库.

负责加载扩展类加载器和应用类加载器

加载<JAVA_HOME>\lib

扩展类加载类器

是由java语言实现的 继承自ClassLoader

负责加载 D:\ProgramFiles\Java\jdk1.8.0_261\jre\lib\ext

应用程序类加载器(系统类加载器)

Java 语言编写的,由 sun.misc.Launcher$AppClassLoader 实现.

派生于 ClassLoader 类.

负责加载用户类

用户自定义类加载器(例如tomcat)

双亲委派机制

类的加载时按需加载,使用时才会加载.

类加载时,加载器都会将类交给父级类加载器加载.

如果所有的父级加载没有找到类,

则一级一级的向下委派查找.

如果都找不到,那么就会抛出异常.

目的: 为了安全考虑 避免了用户自己写的类覆盖了系统中的类.

类的主动使用和被动使用

主动使用会触发类的初始化

new

使用静态变量 静态方法

反射加载类

执行main方法

子类被初始化 父类也会触发初始化

被动使用不会触发了类的初始化

仅仅使用类的静态常量 而且是直接赋字面量的那种

将类作为数组的类型声明使用时不会触发初始化

JVM 运行时数据区

堆,方法区(元空间) 主要用来存放数据 是线程共享的.

程序计数器,本地方法栈,虚拟机栈 是运行程序的,是线程私有的.

程序计数器

jvm中的程序计数器不是cpu中的寄存器, 可以理解为计数器.

是一块非常小的内存空间,运行速度是最快的,不会出现内存溢出情况.

作用:记录当前线程中的方法执行的位置. 以便于cpu在切换执行时,记录程序执行的为位置.

在运行时数据区中唯一一个不会出现内存溢出的区域.

本地方法栈

当我们在程序中调用本地方法时,会将本地方法加载到 本地方法栈中执行.

也是线程私有的, 如果空间不够,也会出现栈溢出错误. hashCode();

虚拟机栈

背景: java为了移植性好(跨平台) 所以将运行程序的设计架构为栈结构运行, 而不是依赖于cpu的寄存器架构.

栈是运行时的单位(加载方法运行),

而堆是存储的单位(存储对象的).

作用运行方法 一个方法就是一个栈帧. 栈帧中包含( 局部变量(基本类型,引用地址) 方法地址,返回地址)

栈中的操作 入栈,出栈 栈中异常 StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。 递归调用方法次数过多\

栈中存储方法运行时需要的数据

栈的运行原理 第一个方法被加载 入栈 在方法中调用了其他方法, 另一个方法入栈 方法运行结束后出栈.

栈帧的结构: 局部变量表: 方法参数,定义的局部变量, 基本值类型直接存值, 引用类型存地址.

操作数栈

动态链接

方法返回地址

Java 堆

概述

堆是JVM内存中核心的区域,用来存储创建出来的对象,是线程共享的.

堆空间在jvm启动时被创建,大小可以设置.

物理上不是连续的,逻辑上是连续的空间

堆中会发生垃圾回收.

堆内存区域划分

堆内存划分为:

新生代(新生区)

新生代分为: 伊甸园区(新生成的对象存储)

幸存者0 (from)

幸存者1(to)

老年代(老年区)

为什么要分区?

把不同的生命周期的对象存储在不同区域,这样不同的区域可以使用不同的垃圾回收算法,

可以提高垃圾回收的效率.

对象在堆内存中的的过程:

新建的对象 存放在伊甸园区, 第一次垃圾回收时,垃圾对象直接被回收掉, 存活下来的对象,会把他存放到幸存者0/幸存者1.

再次垃圾回收时,把在幸存者0区 存活的对象 移动到幸存者1区,然后将幸存者0区清空,依次交替执行.

每次保证有一个幸存者区域是空的,内存是完整的.

当对象经过15次垃圾回收后,依然存活的.将被移动到老年区(老年区,垃圾回收的频率就比较低)

堆各区域的占比

新生代 占整堆的三分之一

新生代中的 伊甸园区 幸存者0 幸存者1 占比是 8:1:1

对象经过15次垃圾回收后,依然存活的.将被移动到老年区

为什么最大是15次 , 因为在对象头中只有4个bit位的空间,只能表示最大值15.

堆空间的参数设置

一般所说JVM优化 就是调整jvm相关各区的参数

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

分代收集思想 Minor GC、Major GC、Full GC

一般情况下收集新生代 Minor GC/Yong GC

老年代 会触发Major GC / Old GC

整堆收集 Full GC

整堆收集触发的条件:

System.gc();时

老年区空间不足

方法区空间不足

开发期间尽量避免整堆收集 ( 在垃圾回收时 会STW stop the world 回收时停止其他线程运行 )

TLAB 机制

TLAB 线程本地分配缓存区

在多线程情况下,可以在堆空间中通过-XX:UseTLAB 设置. 在堆空间中为线程开辟一块空间,用来存储线程中产生的一些对象, 避免空间竞争,提高分配效率

字符串常量池位置

jdk7之前,将字符串常量池位置在 方法区(永久代)中存储. jdk8之后 方法区又称为元空间

jdk8之后,将字符串常量池的位置 放到了堆空间. 因为方法区只有触发FUll GC时才会回收.

因为程序中大量的需要使用字符串,所以将字符串常量池的位置改变到了堆中,可以及时回收无效的字符串常量.

方法区

概述

方法区也是一块内存空间,逻辑上属于堆,为了区分,称为元空间(jdk8之后)

主要用来存储类的信息

在jvm启动时创建 大小可以分配

如果加载的类太多,也会报内存溢出错误

是线程共享的.

方法区的大小可以通过 -XX:MetaspaceSize 设置

方法区在windows中默认大小是21MB

如果到达21MB会触发FULL GC

可以将其值设置的大一些,减少FULL GC的触发

方法区中主要回收运行时常量池,类的信息

类的信息卸载(回收) 条件是比较苛刻的.

满足3个条件:

1.该类以及子类的对象没有被引用

2.该类的类加载器被卸载

3.该类的LClass对象也没有被引用

本地方法接口

什么是本地方法

一个 Native Method 就是一个 java 调用非 java 代码的接口

非java语言实现的,例如 C C++

为什么要使用 Native Method

我们的java程序 需要与外部(计算机硬件)进行数据交互( 例如hashCode read() start() )

可以直接调用外部 的本地方法实现.

JVM解释是用C写的,可以更好的与本地方法交互.

执行引擎

前端编译( .java ---> .class)

字节码 不等于 机器码

需要jvm将字节码加载到内存中.

需要通过执行引擎将字节码 解释/编译成机器码 后端编译( .class ---> 机器码)

执行引擎机制:

解释器: 将字节码逐行解释执行

JIT编译器(即时编译器): 将字节码整体编译为机器码执行

为什么JVM执行引擎设计为半解释型,半编译型?

逐行解释执行效率低.

JVM会针对使用频率较高的热点代码进行编译,并缓存起来. 执行效率提高.

但是编译是需要消耗时间的.

所以jvm刚刚启动后,可以先通过解释器 解释执行代码.

之后再使用编译器编译执行. 两种结合在一起.

垃圾回收

概述

垃圾收集机制并不是java语言首创的.

但是又是java的招牌, java可以自动垃圾回收.

垃圾回收:

回收哪些区域 频繁回收内存,较少回收方法区 栈,本地方法栈,程序计数器没有垃圾回收

什么时候回收

怎么回收

什么是垃圾

垃圾是指在运行程序中没有任何引用指向的对象,这个对象就是需要被回收的垃

圾。

为什么需要 GC?

垃圾如果不及时清理,越积越多,可能会导致内存溢出.

垃圾多了,内存碎片较多 例如数组 需要连续空间

早期是手动回收不被使用的对象 例如 C++

java语言是自动垃圾收集的.

垃圾回收机制

自动内存管理

无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和 内存溢出的风险

更专心地专注 于业务开发

自动收集的担忧

自动回收方便了程序员的开发,但是降低处理内存问题的能力.

自动虽好,但是还是应该了解并掌握一些相关内存管理的知识.

Java 堆是垃圾收集器的工作重点

从次数上讲:

频繁收集 Young 区

较少收集 Old 区

基本不收集元空间(方法区)

内存溢出与内存泄漏

溢出: 内存不够用了

泄漏: 有些对象已经在程序不被使用了,但是垃圾回收机制并不能判定其为垃圾对象,不能将其回收掉,

这样的对象越积越多, 长久也会导致内存不够用.

例如: 与数据库连接完之后,需要关闭连接通道,但是没有关闭.

io 读写完成后没有关闭

垃圾收集算法分为两大类:

1.垃圾标记阶段算法

主要是来判定哪些对象已经不再被使用,标记为垃圾对象.

判定对象为垃圾的标准: 不被任何引用所指向的对象. Object obj = new Object();

垃圾回收阶段算法:

引用计数算法(在jvm中不被使用)

如果有一个引用指向此对象,那么计数器加1. 如果没有引用指向,计数器为0, 此时就判定为垃圾.

优点:方便使用,设计简洁.

缺点: 增加了计数器的存储空间,计数需要消耗时间.

导致一个循环引用问题. 好几个对象之间相互引用,但是没有其他引用指向他们,此时垃圾回收不能回收他们,但是也没有引用指向. 这就造成了内存泄漏

Object obj = new Object();

obj=null;

可达性分析算法/根搜素算法(这是java目前所使用的垃圾标记算法)

解决 循环引用问题, 设计简单 ,运行高效,防止内存泄漏

思路:

从一些活跃引用(GCRoots 根)开始, 如果对象被根直接或间接引用,那么此对象不是垃圾, 否则标记为垃圾对象.

哪些引用被用来当做根:

虚拟机栈中引用的对象 (方法中引用的对象)

本地方法栈中引用的对象

静态变量所引用的对象

常量引用指向的对象

被synchronized当做锁的对象

Java 虚拟机内部的引用

总结: 栈中引用的(正在使用的) 方法区,常量池中(生命周期较长的),被synchronized当做锁的对象

final(关键字) finally(代码块) finalize()(方法) 是Object类中的一个方法,在对象被最终回收之前调用,只调用一次.

八股文--->背 理解了背一遍就记住了 加入想法,上升到一个全局思想. 不理解 天天背也不理解

finalize() 方法机制

java允许对象在销毁前去调用finalize(),去处理一些逻辑. 一般不用(不建议用)

不要自己显示的去调用finalize()方法,在里面写代码一定要慎重

在 finalize()时可能会导致对象复活。

finalize()由垃圾回收器调用,没有固定的时间.

一个糟糕的 finalize()会严重影响 GC 的性能。比如 finalize 是个死循环。

对象状态:

可触及的:从根节点开始,可以到达这个对象 。 (没有被标记为垃圾)

可复活的:对象的所有引用都被释放,但是对象有可能在 finalize()中复活。 确定为垃圾了,但没有调用finalize()方法.

不可触及的:对象的 finalize()被调用,并且没有复活,那么就会进入不可触及 状态。不可触及的对象不可能被复活,因为 finalize()只会被调用一次.

2.垃圾回收阶段算法

1.标记-清除算法

分为两个阶段:

标记: 标记出从根可达的对象,标记的是被引用的对象.

清除: 此清除并非直接将垃圾对象清除掉.

而是将垃圾对象的地址维护到一个空闲列表中,

之后如果有新的对象产生,判断空闲列表中的对象空间能否存放得下新的对象,如果能放得下,那么就覆盖垃 圾对象.

优点: 简单,容易理解

缺点: 效率低, 会产生STW(在回收时,停止整个应用程序), 会产生内存碎片.

2.复制算法

将内存分为大小相等的两块,每次只使用其中的一块儿区域即可.

当回收时,将不是垃圾的对象,复制到另一块内存中,排放整齐.

然后将原来的内存块清空.

减少内存碎片.

优点: 运行高效,减少内存碎片

缺点: 用到两倍的内存空间 ,对于G1垃圾回收器,将每个区域又可以拆分成更多的小区域,需要维护各区之间的关系.

在新生代中的幸存者0和幸存者1这两个区域使用复制算法.

3.标记压缩算法

背景:

复制算法需要移动对象位置,移动的数量如果多的情况下,效率低. 对于年轻代来讲还是不错的.

对于老年代,大量的对象是存活的. 如果需要移动就比较麻烦效率低.

实现:

将存活对象标记出来,重新在本内存空间中排放位置.

清除其他空间的垃圾对象.

标记-清除 和 标记-压缩对比

标记清除是不移动对象, 不会把垃圾对象清除掉(维护在一个空闲列表中)

标记-压缩是要移动对象的. 要清除掉垃圾对象.

优点: 不会像标记-清除算法那样会产生内存碎片

不会像复制算法那样需要两块内存空间

缺点:

效率相对低, 对象位置移动后需要重新设置对象地址, 也会有STW

分代/分区收集

由于对象的生命周期长短不同,将不同的对象存储在不同的区域.

针对不同的区域进行分区收集,提高收集效率.

垃圾回收中的相关概念

System.gc() 的理解

调用System.gc()方法,会触发FULL GC(整堆收集),

但是不一定调用后会立刻生效,因为垃圾回收是自动的.

一般情况下,不要在项目中显示的去调用.

Stop the World

Stop the World -->STW 在垃圾回收时,会导致整个应用程序停止.

在标记垃圾对象时,需要以某个时间节点上内存中的情况进行分析(拍照 快照)

因为不进行停顿的话,内存中的对象不停的变化,导致分析结果不准确.

停顿是不可避免的,优秀的垃圾回收器尽可能减少停顿的时间.

对象引用

Object obj = new Object();

就是将对象分等级: 强引用(有引用指向的对象) 软引用 弱引用 虚引用(都是垃圾了)

强引用: Object obj = new Object(); obj引用创建的对象 那么此对象就是被强引用的.

这种情况下,即使内存不够用了,报内存溢出,也不会回收.

软引用: 当内存足够使用时,先不回收这类对象,当虚拟机内存不够用时,要回收此类对象.

弱引用: 此类对象只能生存到下次垃圾回收时, 只要发生垃圾回收,就会回收此类对象.

虚引用: 发现即回收.

内存溢出与内存泄漏

垃圾回收器

比较底层,了解垃圾回收器的一些种类及实现.

垃圾回收器(具体实现垃圾回收的收集器名称)

垃圾回收器分类

按线程数分,可以分为串行垃圾回收器和并行垃圾回收器

按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器

按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器

垃圾回收器的性能指标

吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运

行时间+内存回收的时间)

垃圾收集开销:垃圾收集所用时间与总运行时间的比例。

暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。

收集频率:相对于应用程序的执行,收集操作发生的频率。

内存占用:Java 堆区所占的内存大小。

快速:一个对象从诞生到被回收所经历的时间。

常见的垃圾收集器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值