全网最菜教程 jvm粗略总结 吧

179 阅读10分钟

jvm面试常客,今天就对jvm做一点小小的总结,

春招秋招知识点背不过来就写下来吧,就算复制也会为了凑文章而仔细看一眼,文章只为自己写知识的总结吧,没有参考意义


目录

  1. JVM内存模型
  2. JVM垃圾回收
  3. JVM垃圾回收器
  4. JVM参数详解
  5. JVM性能调优
  6. 文章内部的一些补充

1. JVM内存模型

图是盗的一位大佬的,大佬联系方式在图片右下角有保留,如有侵权即刻就删

我们看到1.8作为1.6的升级版,也是java长期支持的一个版本还是做了一些升级的,那就是删除了方法区加入了元空间点击这里看原因

线程私有 作用 补充
程序计数器 流程控制,记录位置 生命周期同线程
虚拟机栈 主要存放了编译器可知的各种数据类型,对象 生命周期同线程
本地方法栈 为虚拟机使用到的 Native 方法服务 HotSpot 虚拟机中和 Java 虚拟机栈合二为一
线程共享 作用 补充
存放对象实例,GC堆 会在后面详细介绍gc堆
方法区 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 方法区可以理解为永久代,但稍有不同
直接内存 并非虚拟机内存但是常常被调用 可能出现OutOfMemoryError

计数器简单介绍

1.字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。

2.在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

虚拟机栈简单介绍

StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。 OutOfMemoryError: 若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。

本地方法栈简单介绍

和虚拟机栈所发挥的作用非常相似,区别是: 机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。


2. JVM垃圾回收

标记-清除算法这个算法不应用在jvm中

原理:标记所有需要回收的对象,然后进行统一回收,最基础的算法,后面的算法都是对它的填坑。

优点:。。。。。。容易理解

缺点:会产出大量散碎空间,效率低下,如果有循环引用无法回收

复制算法

原理:解决效率问题,如果有需要回收的,就直接分一块一样大小的空间,把需要留下的放进去,然后直接把原来的删掉

优点:速度快,不会产生碎片

缺点:每次清空都会建立空间,删除空间有可能移除

标记-整理算法

原理:将所有存活的对象放在一起,剩下的直接消掉。

分代收集算法

原理: 当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。


3. JVM垃圾回收器垃圾回收算法的具体实现

新生代采用复制算法,老年代采用标记-整理算法。 Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器,它在进行垃圾收集工作的时候必须暂停其他所有的工作线程 ParNew收集器其实就是Serial收集器的多线程版本

Parallel Scavenge收集器关注点是吞吐量,其他收集器关注的是用户线程的停顿时间,很多参数供用户找到最合适的停顿时间或最大吞吐量

Serial Old收集器 Serial收集器的老年代版本

Parallel Old收集器 Parallel Scavenge收集器的老年代版本

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。

CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作

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


4. JVM性能调优

JVM调优目标:使用较小的内存占用来获得较高的吞吐量或者较低的延迟 这里有几个比较重要的指标:

内存占用:程序正常运行需要的内存大小。

延迟:由于垃圾收集而引起的程序停顿时间。

吞吐量:用户程序运行时间占用户程序和垃圾收集占用总时间的比值。

就像CAP原则(分布式系统中一致性、可用性、分区容错性,三个要素最多只能同时实现两点,不可能三者兼顾)一样,上面三个特点不可能同时兼顾,要根据实际情况进行调节

一般步骤:
1.监控GC的状态
2.生成堆的dump文件
3.分析dump文件
4.分析结果,判断是否需要优化
5.调整GC类型和内存分配
6.不断分析和调整


5. 补充

删除方法区加入元空间的理由
整个永久代有一个 JVM 本身设置固定大小上线,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,可以对一些参数进行调整
如何判断一个对象是否应该死亡

  1. 引用计数法 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。

  2. 可达性分析算法 这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

类加载的过程

1、类的加载指的是将类的class文件加载到内存中,并且为之创建一个java.lang.Class对象。 2、JVM提供类加载器去加载类。JVM提供的类加载器通常称为系统类加载器,可以继承ClassLoader基类去创建自己的类加载器。 3、程序中所有的类实际上也是实例,他们都是java.lang.Class实例。 4、使用不同的类加载器可以从不同来源来加载类的二进制数据(本地文件系统、JAR包、网络、java源文件动态编译进行加载)。

Java对象的创建过程

①类加载检查: 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

②分配内存: 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

内存分配的两种方式:(补充内容,需要掌握)

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的

内存分配并发问题(补充内容,需要掌握)

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。 TLAB: 为每一个线程预先在Eden区分配一块儿内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配

③初始化零值: 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

④设置对象头: 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

⑤执行 init 方法: 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。