Why
-
深化技术能力,要有技术追求
-
解决线上问题
-
了解程序运行原理,优化代码,编写高质量代码
What
JVM是Java虚拟机,是用来执行Java字节码(二进制的形式)的虚拟机计算机, JVM作用在操作系统之上,而Java程序作用在jvm之上
How
Java内存模型
模块 | 存储内容 | 异常 |
堆 | 唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。 | GC的主要管理区域,是JVM中最大一个块区域。 堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。 |
Java虚拟机栈 | 每个方法被执行的时候都会同时创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口。 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,引用指针或句柄 )和returnAddress 类型。 | 线程私有, 生命周期与线程相同 如果线程请求的栈深度大 于虚拟机所允许的深度,将抛出StackOverflowError 异常; (递归无收敛) |
本地方法栈 | 保存的是本地方法要执行所需的必要参数 | 线程私有, 生命周期与线程相同 本地方法栈区域可能会抛出StackOverflowError 和OutOfMemoryError异常。其他参见Java虚拟机栈 |
方法区 (在jdk1.8之前叫方法区,1.8之后修改成元空间,不同点在于,方法区属于堆空间一部分,是有默认大小的,元空间利用的是宿主机的内存,是动态扩展的) | 被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据。 包含运行时常量池,编译期生成的各种字面量和符号引用,在类加载后存放到方法区的运行时常量池中。运行期间也可能将新的常量放入池中,比如String 类的intern() 方法。 | 当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常 |
程序计数器 | 每条线程的程序计数器,相互独立,线程私有的。 若线程正在执行Java 方法,则计数器记录正在执行的虚拟机字节 码指令的地址;若正在执行Natvie 方法,则计数器值(Undefined) | 是唯一一个没有任何OutOfMemoryError 的区域 |
程序编译与加载
字节码
举个例子
java代码
编译后的字节码 javap -v
oker@okerdeMBP Documents % javap -v ByteCodeTest.class
Classfile /Users/oker/Documents/ByteCodeTest.class
Last modified 2022-10-5; size 405 bytes
MD5 checksum 522f1107f4fe2c07f2052e4b3387ff24
Compiled from "ByteCodeTest.java"
public class ByteCodeTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#18 // ByteCodeTest.i:I
#3 = Fieldref #19.#20 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Methodref #21.#22 // java/io/PrintStream.println:(I)V
#5 = Class #23 // ByteCodeTest
#6 = Class #24 // java/lang/Object
#7 = Utf8 i
#8 = Utf8 I
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 code
#14 = Utf8 (I)V
#15 = Utf8 SourceFile
#16 = Utf8 ByteCodeTest.java
#17 = NameAndType #9:#10 // "<init>":()V
#18 = NameAndType #7:#8 // i:I
#19 = Class #25 // java/lang/System
#20 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#21 = Class #28 // java/io/PrintStream
#22 = NameAndType #29:#14 // println:(I)V
#23 = Utf8 ByteCodeTest
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/System
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Utf8 java/io/PrintStream
#29 = Utf8 println
{
int i;
descriptor: I
flags:
public ByteCodeTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 11
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 5: 0
line 7: 4
public void code(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iload_1
4: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
7: return
LineNumberTable:
line 9: 0
line 10: 7
}
SourceFile: "ByteCodeTest.java"
类加载
双亲委派
双亲委派机制定义:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
Bootstrap ClassLoader(启动类加载器) :主要负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
Extention ClassLoader(扩展类加载器):主要负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
Application ClassLoader(应用程序类加载器) :主要负责加载当前应用的classpath下的所有类
User ClassLoader(用户自定义类加载器) : 用户自定义的类加载器,可加载指定路径的class文件
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
自定义类加载器
-
这个自定义的类加载器继承自ClassLoader
-
这个类加载器要重写ClassLoader类中的findClass()方法
Groovy + classloader
加载过程
类加载过程
加载(加载IO流文件,并存储类信息在方法区)->
验证(格式验证,语义分析,操作验证)->
准备(为类中的所有静态变量分配内存空间,并为其设置一个初始值)->
解析(将常量池中的符号引用转为直接引用)->
初始化(将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作)
static的代码(包括静态代码块)是在类加载的初始化阶段执行的。
final修饰的变量是在类加载的加载阶段执行的,比static靠前
https://blog.csdn.net/qq_29857681/article/details/85323603
小结:
内存分配与垃圾回收
内存分配
垃圾回收
怎么判定是垃圾?
可达性分析:“GC Roots”根对象集作为起始点集合,从这些节点开始,根据引用关系向下搜索,搜索过程路径称为“引用链”。如果,某对象到GC Root没有引用链相关联,那么,就是GC Root到对象不可达,则证明这个对象不可能再被使用。
固定作为GC Root的对象:
虚拟机栈中引用的对象,如:线程中被调用的方法堆栈中使用的参数、局部变量、临时变量。
方法区中类静态属性引用的对象。
方法区中常量引用的对象,比如:字符串常量池里的引用。
本地方法栈中JNI引用的对象。
Java虚拟机内部的引用,比如:基本数据类型对应的Class对象、常驻异常对象、系统类加载器,如:String、NullPointExcepition等。
所有被同步锁持有的对象(synchronized关键字)。
反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
怎么回收?
常见算法
-
标记-清除算法(Mark-Sweep)
-
首先标记出所有需要回收的对象,
-
然后回收所有需要回收的对象。
-
-
标记-整理(压缩)算法(Mark-Compact)
-
标记过程仍然一样,但后续步骤不是进行直接清理,而是令所有存活的对象向一端移动,然后直接清理掉这端边界以外的内存。
-
-
复制算法(Copying)
-
将可用内存划分为两块,每次只是用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来整块内存空间一次性清理掉。
-
G1垃圾回收器(Garbage first)
G1主要面向的是服务端的垃圾回收器。在G1之前,JVM的主要垃圾回收器采用的是物理分代的思想,将内存区域严格的划分成年轻代(young GC)和老年代(major GC),然后针对于年轻代和老年代使用不同的垃圾回收器进行GC操作,直到G1,G1采用的是对整个堆进行回收,并且G1使用的分区region思想将内存划分成了许多的分区。
主要特点
1. 按照region分块,每个块标记S,E,O,H
2. 按照性价比进行回收,分别统计每个块的回收空间和时间,按照性价比进行排序
3. 可以指定用户停顿时间,动态回收垃圾大小。默认200ms
https://blog.csdn.net/qq_29857681/article/details/125571888
JVM问题解决
OOM
减少Full GC
-
首先考虑内存泄漏 dump线上内存 查看大对象/数量较多的对象 进行代码排查
-
调大年轻代 让对象尽量在年轻代进行回收 少进入老年代
-
同时要注意年轻代过大导致young GC时间过长,因此要在年轻代大小和GC时间之间平衡(不断调试)。
Full gc 优化:https://blog.csdn.net/cml_blog/article/details/81057966