《JavaEE初阶》JVM基础知识
文章目录
- 《JavaEE初阶》JVM基础知识
- JVM内存区域划分
- 堆:
- 栈:
- 方法区:
- 程序计数器:
- 划分细节:
- 类加载机制:
- 双亲委派模型:
- 垃圾回收机制(GC)
- 什么样的内存需要GC来回收
- 引用计数来判断对象是否是垃圾:
- 使用可达性分析来判断对象是否为垃圾:
- 垃圾回收策略:
- 有缺陷的垃圾回收策略:
- JVM使用的分代回收策略:
本章主要介绍JVM中比较重要的三个内容:
-
JVM内存区域划分
-
JVM类加载机制
-
JVM垃圾回收机制
JVM内存区域划分
当我们创建一个java进程时,启动时会向操作系统申请一块内存,JVM会将这块内存划分为几个区域: 堆 栈 程序计数器 方法区
堆:
堆上主要存放的是new的对象,是最重要的区域,也是内存划分最多的区域.
栈:
栈上主要存放 方法之间的调用关系(不记录方法的主要内容,只记录方法之间的调用关系),局部变量
JVM中的栈分为java虚拟机栈和本地方法栈,java虚拟机栈是java代码使用的栈,而本地方法栈主要存放JVM内部使用的C++代码,(我们在阅读源码时,可以看到native方法,这类方法就是由JVM内部通过C++来实现的).但是在java1.8中,将这两块区域进行合并.
方法区:
方法区中主要存放 类对象
类对象: 类对象是我们在编写代码中,类的代码以及类中的静态变量与静态方法,这都属于类对象中的内容.
class student{
int a; //对象内容 每个对象都有一份a
static int b; // 类对象内容 所有对象共用一份b
}
程序计数器:
这个区域主要存放 下一条指令 的地址,这块区域内存划分最小的区域.
划分细节:
方法区和堆,在整个java进程中,只有一份,而栈和程序计数器是每一个进程都具有的.
类加载机制:
对于一个java程序,JVM通过类加载机制了类的生命周期:
分为以下三个大的步骤:
-
加载:
java程序打开.class文件,将.class文件转化为二进制的字节流,将这个二进制字节流的静态存储结构转化为方法区运行时的数据结构.在内存中生成一个代表这个类java.lang.class对象
-
链接:
-
验证:
检查.class文件格式是否符合规范要求
-
准备:
给静态变量分配内存空间,并将其值填充为 0 值
-
解析:
将字符串常量进行初始化, “符号引用替换为直接引用”
在编译过程中,编译器会通过一些特殊的符号来代表这些字符串常量,在进行类加载时,将真正的字符串常量存放到对应内存中.
-
-
初始化:
针对类的静态成员进行初始化,执行静态代码块.
双亲委派模型:
描述的是类加载阶段,JVM去那些目录下寻找.class文件.
JVM自带的三个类加载器:
-
BootStrapClassLoader 负责加载标准库中的类
-
ExtensionClassLoader 负责加载一些扩展的类
-
ApplicationClassLoader 负责加载本地程序中的类
双亲委派模型就是在上述体系中实现的:
双亲委派模型能够巧妙地避免我们在本地程序中写出一个特殊的类,导致将方法库中的类给覆盖掉.例如我们写了个java.lang.String…
垃圾回收机制(GC)
在我们学习C语言的时候,我们知道C语言中申请内存是需要手动通过free来释放的.对于需要程序员手动释放内存,是容易造成内存泄露的.
JVM为java实现了自动化进行内存释放,也就是垃圾回收机制(GC)
什么样的内存需要GC来回收
在了解GC之前,我们需要了解什么样的内存需要被回收:
对于程序计数器而言,每一个进程中都具有一个程序计数器,所以我们不需要进行GC,直接跟随进程销毁即可
对于栈而言,栈中主要存储的是局部变量,我们已经约定了局部变量在出了其作用域就可以会被回收, 所以也不需要GC.
对于方法区而言,其主要任务是进行类加载,很少设计到"类卸载",所以对于GC的使用并不是很急迫.
对于堆而言,堆存储了许多new 对象, 这也意味着当很多对象使用完之后,是需要GC来清理内存的,故而堆是GC工作的主战场.
引用计数来判断对象是否是垃圾:
引用计数方案并不是java使用的方法.但我们也需要了解.
引用计数就是在一个对象中引入额外的计数器,这个计数器记录了有多少个引用指向了这个对象.如果计数器的值为0,说明该对象没有引用指向,则认为是垃圾,需要释放.
这个方案是具有很多缺陷的:
-
在多线程场景下需要考虑线程安全问题,(如果了解过多线程会很容易理解为什么有线程安全问题)
-
如果对象占用的内容较小,并且有多个时,会带来空间资源的浪费.(如果对象是2kb,引入计数器后变成了4kb,负担就翻倍了)
-
循环引用问题:
class Test{ Test ref = null; } Test a = new Test(); Test b = new Test(); a.ref = b; b.ref = a;
如果我们把a,b的引用销毁,我们想要的效果是 两个Test被销毁,但是事与愿违:
此时计数器为1,仍然不能销毁这个对象.
使用可达性分析来判断对象是否为垃圾:
可达性分析是java使用的方案.
可达性分析是通过一些特殊的变量为起点.从起点出发,看看哪些对象可以被访问到,如果可以呗访问到,则认为这个对象不是垃圾,如果不能访问到,则认为是垃圾.
可以树的构造思想来理解:
以A结点为起点,则认为对象BDEG不是垃圾,CF是垃圾.
可达性分析相比于引用计数,就不会占用额外的内存空间,也不会涉及到循环引用问题.
垃圾回收策略:
有缺陷的垃圾回收策略:
-
标记-清除策略:
使用可达性分析,找到垃圾对象,直接将垃圾对象释放掉.
将图中的垃圾对象进行回收
这个方案的确将内存释放了.但是也引入了一个很严重的问题,释放后的内存空间并不是连续的.即内存碎片问题.
-
标记-复制策略:
使用可达性分析,找到垃圾对象,将要保留的对象复制到另一侧的内存空间.
这样就不会造成内存碎片问题,但是也导致了内存空间的不合理利用.使得内存空间的利用率大大降低.
-
标记-整理策略:
使用可达性分析,找到垃圾对象,使用顺序表或者链表的思想将垃圾对象调整内存位置.解决内存碎片问题.
这个方案可以提高内存的利用率,也可以解决内存碎片的情况,但是缺点也十分明显,搬运操作是O(n)时间复杂度,比较耗时.
上述操作都有各自的缺陷,但是JVM将上述方案进行了结合,使用分代回收方案来解决他们各自的缺陷:
JVM使用的分代回收策略:
将内存空间划分为以下三种区域:
采用以下方案来操作对象:
-
新new的对象放在伊甸区
-
伊甸区的对象是大概率撑不过第一轮GC的,如果伊甸区的对象撑过了第一轮GC,则将这个对象通过复制算法放在幸存区.
-
幸存区的对象经过又一轮GC,如果幸存区的对象撑过了这一轮GC,则将这个对象通过复制算法放入另一个幸存区.
-
当对象在幸存区经过多轮GC,依然没有被垃圾回收,则认为这个对象一时半会是不会被销毁的,所以将这个对象通过复制算法放到老年代.
-
对象在老年代空间中,也是需要经过GC,但是GC的频率大大降低,如果老年代中的对象被标记为垃圾对象,则使用标记整理方案来进行回收.(由于老年代被垃圾回收的频率不高,所以可以接受标记整理方案带来的时间开销.)
-
当有一个很大的对象时,是直接存入到老年代的.(JVM特殊规定)