关于 JVM

内存区域划分

像办公楼一样,有办公区休息区吃饭区啥的,JVM 这个应用程序就会在启动的时候就会向操作系统申请一块内存区域,然后把这个区域分成几个部分,每个部分有不同的功能作用。一个 Java 进程就对应一个 JVM。

(虽然说这里的栈也是"先进后出"的,但数据结构的栈是个更大的概念,这里的特指 JVM 的一块内存空间)

**本地方法栈(线程私有):**native 表示 JVM 内部的 C++ 代码,这里就是给调用 native 方法(JVM 内部的方法)准备的栈空间。这里存储的是 native 方法之间的调用关系。

程序计数器(线程私有): 记录当前线程执行到哪个指令了。每个线程一份,很小一块存一个地址的。

虚拟机栈(线程私有): 给 Java 代码使用的栈,这里面拥有很多个栈,每个线程就对应一个。这里存储的是 方法 之间的调用关系。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。调用一个方法就创建一个栈帧。

**堆区(线程共享):**整个 JVM 空间最大的区域,new 出来的对象(类的成员变量)都在堆上。一个进程只有一个堆。

元数据区 / 方法区(线程共享): 用来存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的。运行时常量池是方法区的一部分,存放字面量与符号引用。字面量 : 字符串(JDK 8 移动到堆中) 、final常量、基本数据类型的值。 符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。

小结:1. 局部变量在 栈 上、2. 普通成员变量在 堆 上、3. 静态成员变量在 方法区。

类加载

类加载就是将 .class 文件 从文件(硬盘)被加载到内存中(元数据区)的过程。

**加载:**就是把 .class文件先找到(找的过程)然后打开文件读文件,再把文件内容读到内存中。最终加载完成得到了一个类对象。

**验证:**根据 JVM 虚拟机规范来检查 .class 文件格式(是一个二进制文件)是否正确。

**准备:**给类对象分配内存空间(先在元数据区占个位置),同时也会使静态成员被设置成 0 值。

解析: 初始化字符串,把符号引用转为直接引用。字符串常量得有一块内存空间来存,这块内存空间还得有一个引用来保存内存空间的起始地址,当类加载的时候,字符串常量处在 .class文件 中,此时这个引用记录的并非字符串常量真实的地址(占位符),此时就是符号引用;当类加载之后,字符串常量真正被放到内存中了,此时才有内存地址,这个引用才能被真正赋值成内存地址,此时就是直接引用了。

**初始化:**调用构造方法,进行成员初始化,执行代码块,静态代码块,加载父类......

一个类,什么时候会被加载?要用到的时候才加载,加载过一次之后就不用重复加载了。

  1. 构造 类 的实例大的时候

  2. 调用这个类的 静态方法/使用静态属性

  3. 加载子类的时候会先加载其父类

双亲委派模型

双亲委派模型描述的是在加载中,找 .class 文件的基本过程。

**类加载器工作流程:**首先加载一个类的时候,是从 ApplicationClassLoader开始的,但是它会把加载人物交给父亲去加载,到了ExtensionClassLoader这里它也会交给父亲去加载,到了BootstrapClassLoader之后,再想交给父亲发现没有上级了,就只能自己加载了,搜索自己负责的标准库目录相关的类,找到就加载,没找到就继续传回去由子类来加载。如果最下面的还没找到这个类那么就会抛异常了。

为啥有这顺序: 主要是出自于 JVM 实现代码的逻辑,因为 JVM 的代码是按照类似于递归的方式来实现的。同时,这个顺序也保证了 Bootstrap 能先加载,Application 能后加载,这就能避免程序猿定义了一个类似于 java.lang.String 这样的类,就会先加载的是标准库的类而不会加载到自己写的这个类了。再者,也可以自定义类加载器,自定义的能随便加入上述流程的任意位置。

垃圾回收机制

所谓的"垃圾",指的就是不再使用的内存,垃圾回收就是把不用的内存帮我们自动释放了。(C++是要自己手动释放的,否则可能会出现"内存泄漏问题")

GC

GC 是当下最主流的一种垃圾回收机制。上面已经知道,JVM 里是有很多的内存区域的,GC 主要就是针对 堆 进行释放的,因为大,多。同时,GC 是以"对象"为基本单位进行回收的,为不是字节,比如一个对象有很多属性,其中7个不用了,3个还在用,那么此时就不会回收那不用的7个,而是都不用了才把这个对象一起回收。

GC 实际工作过程

1. 找到垃圾 / 判定垃圾

关键思路,就是看这个对象有没有"引用"在指向它。这里有两种方式判断:

a)引用计数【python、php......】

会给每个对象分配一个计数器,每当创建一个引用的时候计数器就+1,当这个引用被销毁了计数器就-1。虽然这个方法简单有效,但是存在两个问题:第一个内存空间浪费多,因为计数器也是需要一定内存来存数据的,当代码中存在多个对象的时候就会额外产生很多内存占用。第二个就是存在循环引用问题:

b)可达性分析【Java的做法】

Java 的对象都是通过引用来指向并访问的,大多数情况下,一个引用指向一个对象,这个对象里面的成员又指向别的对象,比如二叉树。然后可达性分析,就把所有的这些对象组织的结构视为是树,然后就从树结点出发,遍历这棵树,把可以访问的视为"可达",不能访问的视为"不可达"。JVM 就会将这些不可达的视为垃圾进行回收。很明显,这种遍历相比上述会比较慢,因此就会隔一段时间遍历一遍。

2. 再以对象为单位进行释放
垃圾回收算法
a)标记清除
b)复制算法(解决了内存碎片问题)
c)标记整理(解决了复制算法空间利用率低的缺点)

这种类似于顺序表有搬运元素的操作,效率也不高,特别是当搬运空间比较大的时候开销也会更大。

分代回收

基于上述这些基本策略,JVM 就采用了复合策略,也就是把垃圾回收分成不同的场景,根据场景的不同决定使用哪些算法。

这里就会给对象引入一个 "年龄" 这样的概念,其中的 "一岁" 就代表经历了一轮可达性分析之后,发现这个对象不是垃圾(也可以理解成是 熬过GC的轮次)。

相关推荐
AAA 建材批发王哥(天道酬勤)1 小时前
JVM 由多个模块组成,每个模块负责特定的功能
jvm
JavaNice哥8 小时前
1初识别jvm
jvm
涛粒子8 小时前
JVM垃圾回收详解
jvm
YUJIANYUE8 小时前
PHP将指定文件夹下多csv文件[即多表]导入到sqlite单文件
jvm·sqlite·php
逊嘘8 小时前
【Java语言】抽象类与接口
java·开发语言·jvm
鱼跃鹰飞18 小时前
大厂面试真题-简单说说线程池接到新任务之后的操作流程
java·jvm·面试
王佑辉18 小时前
【jvm】Major GC
jvm
阿维的博客日记18 小时前
jvm学习笔记-轻量级锁内存模型
jvm·cas·轻量级锁
曹申阳1 天前
2. JVM的架构模型和生命周期
jvm·架构