学习笔记-Jvm
JVM体系
栈空间以及程序计数器中不会有垃圾,垃圾回收、JVM调优主要是对堆进行操作(方法区是特殊的堆)
类加载器
- 虚拟机自带加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序加载器
public class Aa {
public static void main(String[] args) {
Aa aa1 = new Aa();
Aa aa2 = new Aa();
Aa aa3 = new Aa();
System.out.println(aa1.hashCode()); //460141958
System.out.println(aa2.hashCode()); //1163157884
System.out.println(aa3.hashCode()); //1956725890
System.out.println("===================");
Class<? extends Aa> aClass1 = aa1.getClass();
Class<? extends Aa> aClass2 = aa2.getClass();
Class<? extends Aa> aClass3 = aa3.getClass();
System.out.println(aClass1); //class Aa
System.out.println(aClass2); //class Aa
System.out.println(aClass3); //class Aa
System.out.println("===================");
System.out.println(aClass1.getClassLoader());//AppClassLoader
//ExtClassLoader 扩展类加载器
System.out.println(aClass1.getClassLoader().getParent());
//null获取不到 rt.jar 根加载器
System.out.println(aClass1.getClassLoader().getParent().getParent());
}
}
双亲委派机制
1.类加载器收到类加载的请求!
2.将这个请求向上委托给父类加载器去完成,一直向上委托,知道启动类加载器
3、启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,通知子加载器进行加载
4、重复步骤3
5、如果没有任何加载器能加载,就会抛出ClassNotFoundException
优点:这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
沙箱安全机制
什么是沙箱安全机制:
沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络
。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的Java程序运行都可以指定沙箱,可以定制安全策略。
Native
带有native关键字,说明Java作用范围无法触及,会去调用低层C语言的库
1、进入本地方法栈
2、调用本地方法接口 JNI
使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。这些函数的实现体在DLL中,JDK的源代码中并不包含,是看不到的。
Java不是完美的,Java的不足除了体现在运行速度上要比传统的C++慢许多之外,Java无法直接访问到操作系统底层(如系统硬件等),为此Java使用native方法来扩展Java程序的功能。
可以将native方法比作Java程序同C程序的接口,其实现步骤:
1、在Java中声明native()方法,然后编译;
2、用javah产生一个.h文件;
3、写一个.cpp文件实现native导出方法,其中需要包含第二步产生的.h文件(注意其中又包含了JDK带的jni.h文件);
4、将第三步的.cpp文件编译成动态链接库文件;
5、在Java中用System.loadLibrary()方法加载第四步产生的动态链接库文件,这个native()方法就可以在Java中被访问了。
PC寄存器
JVM中的程序寄存器(Program Counter Register),或翻译为PC计数器,也称为程序钩子。寄存器存储指令相关的现场信息,CPU只有把数据装在到寄存器才能运行。每个线程都有一个独立的PC寄存器。
作用:用来存储指向下一条指令的地址,由执行引擎来读取下一条指令。它是程序控制流的指示器,分支、跳转、循环、异常处理、线程恢复、等基础功能都需要以来这个计数器来完成。
1、使用PC寄存器存储字节码指令地址有什么用呢?(为什么使用PC寄存器记录当前线程的执行地址呢?)
因为CPU需要不停的切换各个线程,这时候切换回来以后就得知道接着从哪开始继续执行。JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
2、为什么PC寄存器会被设定为线程私有?
多线程在一个特定时间段只会执行其中某一个线程的方法,CPU会不停的进行任务切换(看似并行实则并发)这必然导致经常终端或恢复,如何保证不出错?最好的办法是每个线程分配一个PC寄存器,准确记录各线程正在执行的当前字节码指令地址。
方法区
方法区(Method Area),与java堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类型信息,常量,静态变量,及时编译后的代码缓存数据
元空间不在虚拟机中设置内存,而是直接使用本地内存
jdk8之后变成元空间,放到native heap本地堆内存中,而方法区中永久代的字符串常量放到堆中
方法区内部结构
类型信息
对每个加载的类型(类Class,接口interface,枚举enum,注解annotation),jvm必须在方法区存储以下类型信息
- 类型的完整有效名称,全限定名
- 类型直接父类的全限定名
- 类型的修饰符(public,abstract,final的某个子集)
- 类型直接接口的一个有序列表
域(Field)信息 (属性,字段)
- JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序
- 域的相关信息包括:域名称,域类型,域修饰符(public,private,protected,static,final,volatile,transient的某个子集)
方法(Method)信息
JVM必须保存所有方法的一下信息,同域信息一样包括声明顺序
- 方法名称
- 方法的返回类型(或void)
- 方法参数的属性和类型,按照顺序
- 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的子集)
non-final的类变量
静态类变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分,类变量被类所有实例共享,即使没有类实例时也可以访问
全局常量(static final)
被声明为final的类变量处理方式则不同,每个全局变量在编译的时候就会被分配了
运行时常量池
- 运行时常量池(Runtime Constant Pool) 是方法区的一部分
- 常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期间生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
- 当类和接口加载到虚拟机后,就会创建对应的运行时常量池
- JVM为每个已加载的类型(类或接口)都维护一个常量池,池中的数据项像数组项一样,是通过索引访问的
- 运行时常量池中包含多种不同的常量,包括编译期间就已经明确的数值字面量,也包括到运行期解析后才能获得的方法或字段引用,此时不再是常量池中的符号地址,这里转换为真实地址
- 运行时常量池对比Class文件的常量池的另一重要特性是具备动态性
- 运行时常量池类似于传统编程语言的符号表,但是它所包含的数据却比符号表要更加丰富一些
- 当创建接口或类的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区能提供的最大空间,则JVM会抛出OutOfMemoryError异常
栈
用来存放基本数据类型和引用数据类型的实例的。堆栈是线程独享的,每一个线程都有自己的线程栈。
堆 heap

垃圾回收主要在伊甸区和养老区
当伊甸园内存满后会触发轻GC,并将未被清理的部分放在幸存区,
当伊甸园和幸存区内存都满后会触发重GC,并将未被清理的部分放在老年区
堆全都内存满后会报OOM异常:java.lang.OutOfMemoryError: Java heap space]
修改内存大小:
Xms字面意思是最小内存,这里可以理解成初始化时的内存分配,Xmx也就是允许分配的最大内存空间。
-Xms1024m -Xmx1024m-XX :+PrintGCDetails
获取内存大小:
public class Test {
public static void main(String[] args) {
long l = Runtime.getRuntime().maxMemory();
long l1 = Runtime.getRuntime().totalMemory();
System.out.println((double)l/1024/1024);
System.out.println((double)l1/1024/1024);
}
}
OOM异常
1、修改扩大内存
2、使用工具检测:MAT、Jprofiler
- 分析Dump内存文件,快速定位内存泄漏
- 获得堆中的数据
- 获得大的对象
- ....
先在idea中安装 Jprofiler 插件,再下载安装 Jprofiler 客户端,
异常程序:
public class Test3 {
//byte[] a=new byte[1*1024*1024]; 1m
public static void main(String[] args) {
ArrayList<Object> objects = new ArrayList<>();
int count=0;
try {
while (true){
objects.add(new Test3());
count++;
}
}catch (Error error){
System.out.println(count);
error.printStackTrace();
}
}
}
-Xms8m -Xmx8m -XX : +HeapDumponoutofMemoryError
修改启动参数:
生成文件:
打开文件查看信息:
GC
作用区域只有堆
一般应用:分代收集算法
-
年轻代:存活率低,使用复制算法
-
老年代:标记清除压缩混合
引用计数法
对对象的引用次数进行计数,gc掉次数较低的对象!
- 缺点:计数器本身存在消耗
复制算法
两个幸存区:from、to (空的是to)
每次gc把伊甸区和from区的幸存者一起放在to中,之后from和to身份转换!
当一个对象经历15次(默认)gc后还存在就移动到老年区!
-XX:MaxTenuringThreshold=8 #修改‘任期’
- 优点:没有内存碎片
- 缺点:浪费了内存空间,有一半内存空间(to)永远是空的!
标记算法
扫描并标记对象,在扫描对没有标记的对象进行清理
-
缺点:两次扫描浪费时间,会产生内存碎片
-
优点:不需要额外的空间
标记压缩:
在 标记算法 基础上再扫描一遍,并将存活对象进行移动,防止产生内存碎片
改进:先进行几次标记清除再进行压缩
JMM
JMM就是Java内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。
Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。线程不能直接读写主内存中的变量。
不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。
原子性:
原子性指的是一个操作是不可分割,不可中断的,一个线程在执行时不会被其他线程干扰。
int i = 2;//基本类型赋值操作,必定是原子性操作
int j = i;//先读取i的值,再赋值到j,两步操作,不能保证原子性
i++; //先读取i的值,再+1,最后赋值到i,三步操作了,不能保证原子性
i = i + 1;
可见性:
可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了。Java是利用 volatile 关键字来提供可见性的。 当变量被volatile修饰时,这个变量被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值。
除了volatile关键字之外,final和synchronized也能实现可见性。
有序性:
在Java中,可以使用synchronized或者volatile保证多线程之间操作的有序性。实现原理有些区别:
volatile关键字是使用内存屏障达到禁止指令重排序,以保证有序性。
synchronized的原理是,一个线程lock之后,必须unlock后,其他线程才可以重新lock,使得被synchronized包住的代码块在多线程之间是串行执行的
八种内存交互操作:
- lock(锁定),作用于主内存中的变量,把变量标识为线程独占的状态。
- read(读取),作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用。(read应该是读到cpu-cache里,然后load,读取cpu-cache到内存里是)
- load(加载),作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。
- use(使用),作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
- assign(赋值),作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
- store(存储),作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
- write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
JMM对8种内存交互操作制定的规则:
- 不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
- 不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
- 不允许线程将没有assign的数据从工作内存同步到主内存。
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
- 一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
- 一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!