JVM知识点总结

什么是JVM

定义:Java Virtual Machine - java 程序的运行环境(java 二进制字节码的运行环境)

好处:

一次编写,到处运行

自动内存管理,垃圾回收功能

数组下标越界检查

多态

比较:

JVM内存结构

程序计数器

Program Counter Register 程序计数器(寄存器)

作用:记住下一条jvm指令的执行地址

特点:是线程私有的;不会存在内存溢出

虚拟机栈

Java Virtual Machine Stacks (Java 虚拟机栈)

参数:-Xss

定义:

每个线程运行时所需要的内存,称为虚拟机栈

每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

问题辨析

1. 垃圾回收是否涉及栈内存?不涉及

2. 栈内存分配越大越好吗?不是

3. 方法内的局部变量是否线程安全?

如果方法内局部变量没有逃离方法的作用范围,它是线程安全的

如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

栈内存溢出(Stack Overflow)

栈帧过多导致栈内存溢出

栈帧过大导致栈内存溢出

线程运行诊断

案例1: cpu 占用过多

定位

用top定位哪个进程对cpu的占用过高

ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)

jstack 进程id:可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号

本地方法栈

Java虚拟机栈用于管理Java方法的调用,本地方法栈用于管理本地方法(C或C++)的调用

Heap 堆 = 新生代(伊甸园+幸存区from+幸存区to) + 老年代

通过 new 关键字,创建对象都会使用堆内存

参数:-Xmx

特点

它是线程共享的,堆中对象都需要考虑线程安全的问题

有垃圾回收机制

堆内存溢出(OutOfMemory:Java heap space)

对象一直使用并占用越来越多内存

堆内存诊断

1. jps 工具 查看当前系统中有哪些 java 进程

2. jmap 工具 查看堆内存占用情况 jmap - heap 进程id

3. jconsole/jvisualvm 工具 图形界面的,多功能的监测工具,可以连续监测

方法区

定义

线程共享

存储每个类的结构,如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括类和实例初始化以及接口初始化中使用的特殊方法

也就是说存储了类的基本信息和已装载类的详细信息

方法区域是在虚拟机启动时创建的

方法区域在逻辑上是堆的一部分

在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里进行的GC主要是对方法区里的常量池和对类型的卸载

参数:-XX:MaxPermSize=8m;-XX:MaxMetaspaceSize=8m

版本 内容
JDK1.6及以前 方法区的具体实现为永久代,静态变量存储在永久代上,永久代为虚拟机内的一块内存
JDK1.7 依然存在永久代,但是已经开始去除永久代,将字符串常量池以及静态变量存储在堆中
JDK1.8及以后 方法区的具体实现变为元空间,元空间使用的是本地内存,存储类型信息,常量,方法,字段。字符串常量池和静态变量依然在堆中

 

1.6

1.8

方法区内存溢出

永久代内存溢出 java.lang.OutOfMemoryError: PermGen space

元空间内存溢出 java.lang.OutOfMemoryError: Metaspace *

运行时常量池

常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量 等信息

运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

StringTable

有垃圾回收

常量池中的字符串仅是符号,第一次用到时才变为对象

利用串池的机制,来避免重复创建字符串对象

字符串变量拼接的原理是 StringBuilder (1.8)

字符串常量拼接的原理是编译期优化

可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池

1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回

1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份, 放入串池, 会把串池中的对象返回

StringTable 性能调优

调整 -XX:StringTableSize=桶个数

直接内存

Direct Memory

直接内存不是JVM里的内存,而是操作系统里的内存。

常见于 NIO 操作时,用于数据缓冲区

分配回收成本较高,但读写性能高

不受 JVM 内存回收管理

分配和回收原理

使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法

ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调 用 freeMemory 来释放直接内存

垃圾回收

垃圾回收只针对方法区和堆内存(都是线程共享)进行垃圾回收。

如何判断对象可以回收

引用计数法

当计时器的数值为0的时候,这个对象就可以被回收了。

可达性分析算法

Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象

扫描堆中的对象,看是否能够沿着 GC Root对象为起点的引用链找到该对象,找不到,表示可以回收

哪些对象可以作为 GC Root ?

System Class、Native Stack、Thread、Busy Monitor等

四种引用

1. 强引用

只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收

2. 软引用(SoftReference)

仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用 对象

可以配合引用队列来释放软引用自身

3. 弱引用(WeakReference)

仅有弱引用引用该对象时,在垃圾回收(fullGC)时,无论内存是否充足,都会回收弱引用对象

可以配合引用队列来释放弱引用自身

4. 虚引用(PhantomReference)

必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存

5. 终结器引用(FinalReference)

无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象

垃圾回收算法

标记清除:Mark Sweep

速度较快

会造成内存碎片

标记整理:Mark Compact

速度慢

没有内存碎片

复制:Copy

不会有内存碎片

需要占用双倍内存空间

分代垃圾回收

对象首先分配在伊甸园区域

新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的 对象年龄加 1并且交换 from to

minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行

当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)

新生代内存不足以放入某个对象时,该对象直接晋升老年代,不会引起minor gc

当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长

相关 VM 参数

垃圾回收器

1. 串行/SerialGC

单线程

堆内存较小,适合个人电脑

新生代:复制 

老年代:标记+整理

2. 吞吐量优先/并行/ParallelGC(JDK 8 默认)

多线程

堆内存较大,多核 cpu

让单位时间内,STW 的时间最短,垃圾回收时间占比最低,这样就称吞吐量高

新生代:复制 

老年代:标记+整理

3. 响应时间优先/CMS

多线程

堆内存较大,多核 cpu

尽可能让单次 STW 的时间最短

标记+清除

4.Garbage First/G1(JDK 9 默认)

同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms

超大堆内存,会将堆划分为多个大小相等的 Region

整体上是 标记+整理 算法,两个区域之间是 复制 算法

G1 垃圾回收阶段:

 

(1)Young Collection

会 STW

(2)Young Collection + CM

在 Young GC 时会进行 GC Root 的初始标记

老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW)

(3)Mixed Collection

会对 E、S、O 进行全面垃圾回收

最终标记(Remark)会 STW

拷贝存活(Evacuation)会 STW

Full GC

SerialGC

新生代内存不足发生的垃圾收集 - minor gc

老年代内存不足发生的垃圾收集 - full gc

ParallelGC

新生代内存不足发生的垃圾收集 - minor gc

老年代内存不足发生的垃圾收集 - full gc

CMS

新生代内存不足发生的垃圾收集 - minor gc

老年代内存不足 + 并发失败- full gc

G1 新生代内存不足发生的垃圾收集 - minor gc

老年代内存不足 + 并发失败- full gc

类文件结构

常量池主要存放两大常量

字面量:类似java语言层面的常量概念

文本字符串

声明为 final 的常量值

基本数据类型的值

其他

符号引用:属于编译原理方面的概念

类和接口的全限定名

字段的名称和描述符

方法的名称和描述符

类加载器

实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。

主要有一下四种类加载器:

1) 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。

2)扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

3)系统类加载器(system class loader)也叫应用类加载器:它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

4)用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。

类加载过程

1)加载

加载时类加载的第一个过程,在这个阶段,将完成一下三件事情:

       a.  通过一个类的全限定名获取该类的二进制流。

       b.  将该二进制流中的静态存储结构转化为方法去运行时数据结构。 

       c.  在内存中生成该类的Class对象,作为该类的数据访问入口。

2)验证

验证的目的是为了确保Class文件的字节流中的信息不回危害到虚拟机.在该阶段主要完成以下四钟验证:

       a.  文件格式验证:验证字节流是否符合Class文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.

       b.  元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。

       c.  字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。

       d.  符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。

       e.  准备

准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

3)解析

该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。

4)初始化

初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

 

posted @ 2022-06-05 22:22  1243741754  阅读(92)  评论(0编辑  收藏  举报