目录
[1.堆 (共享)](#1.堆 (共享))
[2.栈 (私有)](#2.栈 (私有))
[JVM 类加载](#JVM 类加载)
什么是JVM?
JVM(Java Virtual Machine,Java虚拟机) 是一种虚拟化的计算机,允许在其上执行 Java 程序(以及其他由 Java 编译器编译成字节码的程序)。JVM 提供了一种跨平台的执行环境,使得开发者可以编写一次代码,并在任何安装了 JVM 的设备或操作系统上运行。这种跨平台能力通常被称为"一次编写,到处运行"的特性。
内存划分:
JVM就是Java进程,当进程一旦跑起来之后,就会从操作系统里面申请一大块内存空间
JVM就是要将这块空间进行划分成不同的区域,并且每个区域都有不同的功能作用
如下图所示:分为五块不同区间
1.堆 (共享)
堆是整个内存区域中最大的一块,放的内容就是代码new出来的对象
堆里面分为两个区域:新生代和老生代,在讲到后面GC垃圾回收机制会提到....
2.栈 (私有)
分为Java虚拟机栈和本地方法栈
Java虚拟机栈存储的是保存了方法调用关系
本地方法栈存储的是本地方法的调用关系
3.元数据区(共享)
元数据区放的是"类数据"
java
class Test {
.....
}
Test.class就是类对象
还存放了一些方法相关的信息
例如:类有一些方法中,每个方法都代表了一系列的"指令集合"(JVM字节码指令)
还有常量池也存放在元数据区中,
4.程序计数器(私有)
程序计数器是内存区域里面最小的一块区域,它只需要保存当前要执行的下一条指令(JVM字节码)的地址
在上述四块区域中,堆和元数据区的数据是在整个进程只有一份,每个线程都可以访问到,是大家共同可以使用的;而栈和程序计数器是每个线程中有一份,是只能每个线程跟每个线程独立的,不共同使用。
示例:
javaclass Test { int a; Test2 t2 = new Test2(); String s = "hi"; static int b; } public static void main() { Test t = new Test(); }
a,t2,s,b t 都存放在哪个区域中呢?
a存放在堆
t2存储在堆 new Test2()存在在堆 并且t2指向它
s 存放在堆 但"hi"存放在元数据上 s执行它
b 是静态变量 存放在元数据上
t 是局部变量存放在栈上 但它保存了对象的首地址(在堆上)
基本判断方法:
1)局部变量在栈上2)成员变量在堆上
3)静态变量或方法都在元数据上
JVM 类加载
一.类加载过程
一个java进程启动,要将.java文件转化成.class文件,加载到内存中,才能得到'类对象'
类加载过程有以下几个环节:
1.加载
在硬盘中,找对对应的.class文件,读取文件里面的内容
2.验证
检查.class文件里的内容,看看是否符合要求JVM的规范要求,保证这些信息运行后不会危害到JVM的自身的安全;
3.准备
给类对象分配内存(元数据区),并设置类变量的初始值,如果类里有静态变量,那么值为0 /false /null 。。。
4.解析
针对字符串常量进行初始化,把刚才.class文件里面的常量的内容放到元数据区里
5.初始化
针对类对象进行初始化(不是对对象初始化,和构造方法无关),给静态成员进行初始化,执行静态代码块
这样类对象就加载完成,后续代码可以使用这个类对象,创建实例,或者使用里面的静态成员了...
二.双亲委派模型
这个模型描述了JVM加载.class文件过程中,找文件的过程;
这个类加载中的 "双亲委派模型 "出现在"加载"那个环节,根据代码中写的"全限定类名"找到对应的.class文件;
全限定类名:包名+类名 比如String -> Java.lang.String List -> java.util.List;
如何寻找文件?
JVM内置了三个类加载器 负责加载不同的类
1.启动类加载器 BookstrapClassLoader 爷爷
负责加载标准库的类
2.扩展类加载器 ExtensionClassLoader 爸爸
负责加载JVM扩展库的类
3. 应用类加载器 ApplicationClassLoader 儿子
负责加载第三方库的类 和自己代码写的类
这里标注了它们之间的关系并不是类的继承之间的关系,而是通过类加载器中存在一个parent字段 可以指向他们 类似树 所有它也可以称做"父亲委派模型"/"单亲委派模型"
当子类加载器需要加载类时,它会首先将请求委托给父类加载器,直到请求最终被顶级加载器(即 BookstrapClassLoader)处理。只有在父类加载器无法处理该请求时,子类加载器才会尝试加载该类。此时,如果儿子类加载器也没有找到,最后就会抛出ClassNotFoundException
双亲委派模型主要是为了应付以下场景:
当你自己的代码中写的类,类的名称和标准库/扩展库的类发生了冲突,JVM会确保加载的类是标准库上的类,就不加载自己写的类了,如果标准库的类不能加载,那么可能整个Java进程都没法加载了;
是否 可以打破双亲委派模型呢?
答案当然是可以
自己写个类加载就行。。。
JVM垃圾回收机制
什么是垃圾回收(GC)?
垃圾回收机制,是Java提供对于内存自动回收的机制;
JVM(Java虚拟机)的垃圾回收(Garbage Collection,GC)机制是 Java 运行时管理内存的重要部分。其主要目的是自动化地回收那些不再被使用的对象,从而释放内存,避免内存泄漏,提高系统的稳定性和效率。JVM 的垃圾回收机制有一套复杂的算法和流程,但也让 Java 开发者无需手动管理内存。
在GC中,最主要回收的堆上的内存
1)程序计数器:不需要额外回收,线程销毁,自然就没了
2)栈:同理,线程销毁,也就没了
3)元数据区:一般也不需要,都是加载类,不需要卸载类
4)堆: 存放的都是对象实例 GC的主力部分
GC的主要流程,找到谁是垃圾(不再使用的对象),并且给它回收。
谁是垃圾?
在一个对象中,创建的时间往往可以确定,但什么时候不再使用,却是很模糊的。在编程中,一定要确保每个对象都是有效的,可不敢提前释放;
因此,判断一个对象是否需要回收,采取的策略都是比较保守的
Test t = new Test();
使用对象,是通过引用的方式来使用的,如果没有引用这个对象,那么这个对象将在代码中肯定不能使用。
如果 t = null;
new Test() 这个对象没有引用指向了,那么它就可以是作为垃圾;
具体是怎么判断对象是否有无引用,以下会介绍俩种方法。
1.引用计数
这个不是JVM使用的方案,而是Python/PHP等语言使用的方案
javaTest a = new Test(); b = a;
javab = null; a = null;
给对象加个引用计数器,当有地方指向它,那么计数器+1,如果引用失效时,那么计数器-1;
如果计数器为0,那么这个对象将会被当做垃圾被回收
但是有以下几个问题:
1. 消耗额外的空间,如果对象非常大还好,浪费的空间可以接受,但是你对象很小,还要额外开销内存,就会浪费很多空间;
2.存在"循环引用"的问题;
javaclass Test { Test t; } Test a = new Test(); Test b = new Test(); a.t = b; b.t = a;
第一步:
第二步:
javaa = null; b = null;
此时,这俩对象互相执行对方,导致这俩个计数器都为1(但不为0,所有不是垃圾),但是外部代码也访问不到这对象;
Python/PHP它们如果检测到了循环引用就会提供了报错信息;
2.可达性分析
以一系列被称为 "GC Roots" 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到任何一个 GC Roots 没有引用链相连时,则证明该对象是不可达的,是可以被回收的对象。
GC Roots 对象:包括虚拟机栈中引用的对象、本地方法栈中引用的对象、元数据区中类静态属性引用的对象、元数据区中常量引用的对象等。
举例:类似树形状
如果将 a.right = null;
那么 c 将会断开 ,外面将找不到c了 那个就可视为 c 和 f 都是垃圾了
由于可达性分析,需要消耗一定的时间,因此Java垃圾回收,没法做到"实时性",周期性进行扫描(JVM提供了一组专门的负责GC的线程,不停的进行扫描工作);
释放垃圾策略:
1.标记-清除算法
这种算法存在的问题是:
- 碎片化:回收后的内存空间可能会分散,导致内存碎片化,降低内存使用效率。很难使用到连续大的内存。
- 效率问题:标记和清理过程需要扫描所有对象,性能可能较差。
2.复制算法
将内存分为大小相等的两块,每次只使用其中的一块。
释放1 3 5 保留 2 4
当这一块的内存用完了,就将还存活的对象复制到另一块内存中,然后把使用过的这块内存空间一次清除掉。
这种算法的优点是避免了内存碎片化,但缺点是需要两倍于 Eden 区的内存空间来存储对象。
3.标记-整理算法
首先标记出所有需要回收的对象,然后让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
类似顺序表删除中间元素
- 优点:不会产生内存碎片,提高了内存的利用率;标记和压缩的过程相对来说效率较高。
- 缺点:相对于标记 - 清除算法,增加了对象移动的开销,需要更新对象的引用地址等
4.分代回收
上述三个方案,只是铺垫,JVM中的实际方案,是综合上述的方案,更复杂的策略,分代回收(即分情况讨论,根据不同的场景/特点选择合适的方案)
新生代中的对象存活率较低,通常采用复制算法进行垃圾回收;老年代中的对象存活率较高,一般采用标记 - 清除算法或标记 - 整理算法进行垃圾回收。
新生代:又分为 Eden 区和两个 Survivor 区,比例一般为 8:1:1。
新创建的对象都在Eden区,当 Eden 区满了之后,会触发一次 GC,只有少部分对象可以活过第一轮GC
Eden区 -> Survivor区 通过复制算法(由于存活数量少,复制算法的开销也比较低,Survivor区的空间也不是很大)
Survivor区 ->另一个Survivor区 通过复制算法 每一轮GC下来,Survivor区都会淘汰一部分对象,剩下的通过复制算法进入下一个Survivor区,存活下来的对象年龄+1
Survivor区 -> 老年代 某些对象 经过多次GC 还没有变成垃圾 那么将会复制到老年代区
老年代区的对象也是需要进行GC的,但是老年代中的对象都是生命周期比较久的,因此可以减小GC的频率