JVM——Java虚拟机

一. 内存区域划分

JVM本身也是一个进程,会向系统申请内存,然后根据实际的用途划分出不同的空间。
在这里插入图片描述
JVM申请到的内存主要分为四个部分:堆、栈(此处的堆、栈并非是数据结构中的堆和栈)、程序计数器、元数据区。


  1. 代码中new出来的对象,位于堆中;对象中持有的非静态成员变量,也在这里。

  2. 分为本地方法栈和虚拟机栈,包含了方法调用关系和局部变量。
    本地方法栈是指:JVM内部,通过C++实现的调用关系和局部变量;
    虚拟机栈是指:Java代码的调用关系以及Java中的局部变量。
  3. 程序计数器
    比较小的空间,存储下一条要执行的Java指令的地址。x86的cpu也有一个类似的寄存器——eip。
  4. 元数据区(方法区)
    所谓“元数据”,是指一些具有辅助性质的、描述性质的属性。
    比如类的信息、方法的信息、一个程序中有哪些类、每个类中有哪些方法、每个方法中包含哪些指令都会记录在元数据区。

此外还要注意:在JVM中,堆和元数据区只有一份,而栈和程序计数器有多份,每个线程都有一个独立的执行流,有一份独立的栈和程序计数器。

了解了内存区域划分的方法之后,我们来看一个例子加深理解:

class Test{
	private int n;
	private static int m;
}
public class Solo{
	public static void main(String[] args){
		Test t = new Test();
	}
}

试问:上述代码中的n,m,t 都存在于内存分区中的哪些区域?
t是一个局部变量,因此位于栈上;
n是一个非静态成员变量,位于堆上,实际上new出来的Test() 对象也位于此处,t中存放的是Test()对象的地址。
m是一个静态成员变量,位于元数据区/方法区,带有static修饰的成员变量也叫做类属性,属于类的信息,因此位于元数据区。

二. 类加载机制

类加载指的是,java进程运行的时候,将.class文件从硬盘读取到内存,并进行一系列的校验解析的过程。
类加载大致可以分为5步:

  1. 加载
    找到硬盘上的.class文件、打开并读取文件内容。
  2. 验证
    确保当前读取到的内容是合法的.class文件格式,包括文件格式验证、字节码验证、符号引用验证等等。
  3. 准备
    给类对象,申请内存空间,此时申请到的内存空间,里面的值全为0(相当于仅执行了申请这一步骤)。
  4. 解析
    主要是针对类中的字符串常量进行处理,将常量池中的符号引用替换为直接引用(初始化常量)。

什么叫做将符号引用替换为直接引用?
由于文件中不存在地址这个概念,地址是“内存”的地址。虽然不存在地址,但是可以通过一些其他的描述来替代地址,比如:偏移量。

如何理解偏移量呢,举个形象的例子:
以前学校组织看电影的时候,要求我们在电影院门口排队,进去影院后按列依次坐下。为了跟我的好朋友坤坤坐在一起,我就需要计算一下:跟坤坤站位隔开几个同学。
在这里插入图片描述
同样的,此处的隔开几个同学就是偏移量,是我们进去影院前位置的描述方式。而进去影院后,我们就有了座位,就相当于内存中有了地址。这就是将符号引用替换为直接引用

  1. 初始化
    针对类对象完成后续的初始化,还要执行静态代码的逻辑,可能会触发父类的加载。
2.1 双亲委派模型(类加载环节)

描述了如何查找.class文件的策略。JVM中进行类加载的操作,有一个专门的模块,称为“类加载器”(ClassLoader),JVM中的类加载器默认有三个,也可以自定义。
在这里插入图片描述

  • 下层的Application ClassLoader负责查找当前项目的代码目录以及第三方的目录;
  • 中层的Extension ClassLoader负责查询扩展库的目录;
  • 上层的Bootstrap ClassLoader负责查询标准库的目录。

双亲委派模型的工作过程,是一个递归的过程:

  1. 从Application ClassLoader进入,开始工作,但是Application ClassLoader不会立即搜索自己负责的目录,会把搜索任务向上传递给Extentsion ClassLoader;
  2. Extension ClassLoader接到任务后,也不会立即搜索自己负责的目录,同样将任务向上传递;
  3. Bootstrap ClassLoader接到任务后,不能继续向上传递,于是开始搜索自己的目录,如果没搜索到,就将任务向下传递;
  4. 此时Extension ClassLoader开始查询自己的目录,若没有查询到,将任务下传,Application ClassLoader进行查询,若仍然没有查询到,就抛出异常:ClassNotFoundException。

那么为什么要这么设定呢?
确保类加载器的优先级。上述执行顺序始终为先查询标准库、再查询扩展库、最后查询自定义库。这样的顺序可以避免由于自己引入的类与标准库中的类重名而引起的问题。就好比我们国家的任何法律都要服从宪法,任何地方行政条例都要服从法律。

当然,如果自定义类加载器,也就不遵守上述双亲委派的模型。

三. 垃圾回收机制(GC)

首先来说,垃圾回收是回收内存。那JVM申请到的内存又分为好几块,具体要回收哪些呢?

  • 栈中存放的是局部变量,由于局部变量再代码执行完毕后会自动销毁,因此不需要回收;
  • 程序计数器也不需要回收,只需要在每个指令周期内更新即可;
  • 元数据区一般也不需要回收,因为很少涉及到类卸载;

既然上面三个都不需要GC,那么一定是需要的了。

3.1 识别垃圾

判定这个对象是否为垃圾,主要是看有没有引用指向他,如果没有,则视为垃圾(除了匿名对象)。

//对于这样的匿名对象,在该条代码执行完,就会被视为垃圾.
new Mythread().start();

如果只有一层引用,那么很简单,当栈上的局部变量被销毁后,堆上的对象就被视为垃圾。
在这里插入图片描述
而如果是多层调用,这里就不得不引入新的机制:

  1. 引入计数(PHP,Python采用)
    引入计数,就是给每个对象安排一个额外的空间,空间中保存当前对象有几个引用。只有引用数为0时,才将该对象视为垃圾。
    之所以未采用这个机制,一是考虑到引入额外的空间会消耗内存(特别时对象比较小的时候,这个空间就显得格外累赘);二是考虑到循环引用的问题(类似于多线程中的死锁),此时这个机制就无法正常工作。
class Test{
	Test t;
}
Test a=new Test();
Test b=new Test();
a.t=b;
b.t=a;
a=null;
b=null;

比如这段代码,在a、b未被赋值为null之前,引用计数为2;而在a、b被赋值为null之后,引用计数为1,此时也无法访问到a.t和b.t,那么这两个对象就处于既不能用,也没有被视为垃圾的境地。(os:当然你要是能先给a.t和b.t赋值为null,再给a、b赋值null,也不会出现这样的问题,就像死锁是可以人为避免的、但是是容易忽略的)
在这里插入图片描述

  1. 可达性分析(Java采用)
    不会产生循环引用的问题、也不会消耗额外的空间,属于以时间换空间的策略。
    基本思路就是选择一些变量作为GC Roots(主要是栈和方法区中的对象),反复出发遍历,如果该对象能遍历到,就不是垃圾,如果遍历不到,就视为垃圾。

详细的可以参考这篇

3.2 释放内存空间

当对象被标记为垃圾之后,释放内存空间还有多种方式。

  1. 标记-清除
    把标记为垃圾的对象,直接释放掉(最朴素的方法),缺点是会引起内存碎片化。
    在这里插入图片描述
  2. 复制算法
    复制算法是将内存空间划分为等大小的两块,当识别出垃圾后,不直接释放,而是把不是垃圾的对象复制到另一边,然后统一释放这一半的内存空间。缺点是浪费了一半的内存空间,且在某些条件下效率低(垃圾极少,几乎复制所有内存)
    在这里插入图片描述
  3. 标记-整理
    类似于顺序表,删除中间元素的过程,会将后面的元素依次向前搬运。缺点是不论识别出多少垃圾,都会造成大量搬运,效率低。
    在这里插入图片描述
  4. 分代回收(Java真正采用)
    这是综合了上述的复制算法和标记-整理算法得出的方法,各取所长。
    这里引入了一个概念,对象的年龄。Java中有专门的线程负责周期性扫描/释放内存,一个对象,如果被扫描了一次,可达(不是垃圾),年龄+1。
    在这里插入图片描述

JVM根据对象年龄的差异,将堆内存划分为两个部分,新生代(年龄小)/老年代(年龄大)。整个过程大致如下:

  1. 当创建一个对象的时候,它位于伊甸区,由于Java中的大部分对象都有朝生夕死的特性,大部分活不过第一轮GC。少数幸存的对象通过复制算法进入生存区,年龄+1。继续扫描新生代,生存区中的对象如果存活,就会在两个生存区之间来回复制。
  2. 当生存区中的对象经历了若干轮GC之后仍然存在,JVM就认为这个对象的生命周期很长,就会将它拷贝到老年代。
  3. 老年代的对象,也会被GC扫描,但是扫描的频率会大大降低。当老年代的对象死亡,就会按照标记-整理的方式进行回收。

这个方法综合了复制算法和标记-整理算法。在新生代很大程度上避免了复制算法浪费空间和极端情况下效率低的缺点;在老年代,由于对象不易死亡,不容易引起大量的搬运,也尽量避免了效率低的特点。

os:其实这个过程还真有现实例子可举,比如以后找工作的过程中:
在这里插入图片描述

一图胜万言,兄弟们自己体会吧~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值