JVM(内存划分,类加载,垃圾回收)

JVM


Java程序,是一个名字为Java 的进程,这个进程就是所说的“JVM”

1.内存区域划分


JVM会先从操作系统这里申请一块内存空间,在这个基础上再把这个内存空间划分为几个小的区域

在一个JVM进程中,堆和方法区只有一份;栈和程序计数器,每个线程有一份

JVM(内存划分,类加载,垃圾回收)_第1张图片

1.堆:存放new的对象

堆里面分为两个区域:新生代和老年代,新生代放新建的对象,当经过一定GC次数之后还存活的对象会放入老年代。新生代还有三个区域:一个 Endn + 两个 Survivor(S0/S1)

JVM(内存划分,类加载,垃圾回收)_第2张图片

垃圾回收的时候会将 Endn 中存货的对象放到一个未使用的Survivor 中,并把当前的 Endn 和正在使用的 Survivior 清除掉

2.栈:存放方法之间的调度关系

  • 虚拟机栈:java里面用来保存调用关系的内存空间

  • 本地方法栈:本地方法,就是JVM内部 C++ 写的代码,调用关系的内存空间

3.程序计数器:存放下一个要执行的指令的地址

程序计数器是一块比较小的内存空间,可以看作释放前线程所执行的字节码的行号指示器

4.方法区:存放类对象(加载好的)

方法区的作用:用来存储被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据的

变量在哪个部分和变量类型无关,和变量的形态有关(成员,静态,局部)

  • 代码里的局部变量:栈

  • 代码里的成员变量:堆

  • 代码里的静态变量:方法区

2.类加载


java程序在运行之前,需要先编译 .java => .class(二进制字节码文件)运行的时候,Java 进程(JVM)就会读取对应的 .class 文件,并且解析内容,在内存中构造出类对象并进行初始化

1.类加载过程

JVM(内存划分,类加载,垃圾回收)_第3张图片

其中前5步时固定的顺序,并且也是类加载的过程,其中中间的3步都属于连接,所以类加载总共分为现在几个步骤

  1. 加载:找到.class文件,读取文件内容,并且按照.class规范格式来解析

  1. 验证:检查看当前的.class里的内容格式是否符合要求

  1. 准备:给类里的静态变量分配内存空间

  1. 解析:初始化字符串常量,把符号引用替换成直接引用(在类加载之前,字符串常量是没有分配内存空间的,变量名称无法保存字符串常量的真实地址,只能先使用一个占位符,等类加载完成,字符串常量分配过内存之后,就可以用真正的地址替换占位符)

  1. 初始化:针对类进行初始化,初始化静态成员,执行静态代码块,并且加载父类

2.何时触发类加载

使用到一个类的时候,就触发加载(类并不一定是程序一启动就加载了,而是第一次使用才加载)

  • 创建这个类的实例

  • 使用了这个类的静态方法/静态属性

  • 使用类的子类(加载子类就会触发加载父类)

3.双亲委派模型

如果一个类加载器收到了类加载的请求,它首先不会自己尝试去加载,而是将这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载

类加载器:JVM加载类,是由类加载器(class loader)模块来负责的

JVM 自带了多个类加载器:

  1. Bootstrap ClassLoader 负责加载标准库中的类

  1. Extension ClassLoader 负责加载 JVM 扩展的库的类

  1. Application ClassLoader 负责加载自己的项目里的自定义类

双亲委派模型的工作过程:

JVM(内存划分,类加载,垃圾回收)_第4张图片

  1. 上述三个类加载器存在父子关系

  1. 进行类加载的时候,输入的内容全限定类名,形如:java.lang.Thread

  1. 加载的时候,从Application ClassLoader开始

  1. 某个类加载器开始加载到时候,不会立即扫描自己的路径,而是先把任务委派给父“类加载器”

  1. 找到最上面的 Bootstrap ClassLoader在往上,没有父“类加载器”了,就会自己加载

  1. 如果父”类加载器“没有找到类,就会交给自己的子”类加载器“,继续加载

  1. 如果一直找到最下面的Application ClassLoader 也没有找到类,就会抛出一个“类没找到”异常,类加载就失败了

3.垃圾回收(GC)


Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还被引用着,哪些已经没有引用了。判断对象是否被引用有如下几种算法:

内存VS对象

在Java中,所有的对象都是要存在内存中的,因此将内存回收也叫做死亡对象的回收。

1.GC回收的部分

JVM主要内存分成这几个部分:

  • 堆:GC主要针对堆来回收

  • 方法区:类对象,加载之后也不太会卸载

  • 栈:释放时机确定,不必回收

  • 程序计数器:固定内存空间,不必回收

JVM(内存划分,类加载,垃圾回收)_第5张图片

2.回收对象的判断算法

1.引用计数算法

给每个对象都加上一个计数器,这个计数器就表示“当前的对象几个引用”

但是,主流的 JVM 中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象循环引用问题

循环引用:

当两个对象互相引用时

classTest{

Testx;

}

Testa=newTest();

Testb=newTest();

a.x=b;

b.x=a;

a=null;

b=null;

当前这两个对象的计数器都为1,所以无法进行释放,此时外界的代码仍然时无法访问和使用对象的

2.可达性分析【JVM采取的方法】

核心思想:通过一系列称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为 “引用链”,当一个对象到GC Roots没有任何的引用链时,证明此对象是不可用的

JVM(内存划分,类加载,垃圾回收)_第6张图片

对象 Object 5-Object 7 之间虽然还有关联,但是它们到达不了GC Roots 是不可达的,因此他们会被判定为可回收对象

在Java语言中,课作为GC Roots的对象包含下面几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象

  1. 方法区中类静态属性引用的对象

  1. 方法区中常量引用的对象

  1. 本地方法栈中JNI(Native)引用的对象

3.垃圾回收算法

1.标记-清除算法

首先标记出垃圾,然后直接把对象对应的内存空间进行释放

JVM(内存划分,类加载,垃圾回收)_第7张图片

标记-清楚算法的不足:

  1. 效率问题:标记和清楚这两个过程的效率都不高

  1. 空间问题:标记清楚后会产生大量不连续的内存碎片,内存碎片较多会导致在分配内存空间较大的对象时,无法找到足够连续的内存

2.复制算法

复制算法是为了解决“标记-清楚”算法带来的问题描述:将一个内存空间分为两部分,首先先使用一边,清理时不再是原地释放,而是把“非垃圾”拷贝了另一边,然后再把之前这一边给释放掉

JVM(内存划分,类加载,垃圾回收)_第8张图片

复制算法的不足:

  1. 空间利用率更低了(一次只能使用一半的内存空间)

  1. 如果一轮GC下来,大部分对象仍然需要保留,只有少数对象要回收,这个时候拷贝开销就很大

3.标记-整理算法

前面与“标记-清理”算法过程一致,打包后续不是直接堆可回收对象进行清理,而是让所有存活对象都想一段移动,然后直接清理掉边界以外的内存(类似于顺序表的删除元素操作)

JVM(内存划分,类加载,垃圾回收)_第9张图片

这种方式,相对于上述的复制算法来说,空间利用率上来了,同时能够解决内存碎片问题,但是搬运操作也是比较耗时的

4.分代回收算法

分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略,从而实现更好的垃圾回收(当前 JVM 垃圾收集采用此算法)一般是将Java堆分为新生代(GC 扫描的频率更高)和老年代(GC 扫描的频率降低),在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高,没有额外空间对它进行分配担保,就必须采用“标记-清除”或者“标记-整理”算法

根据对象的不同特点(根据对象的年龄(依据 GC 的轮次来算的,有一组线程,周期性的扫描代码里所有的对象,如果一个对象,经历了一次GC ,就认为年龄+1)来划分)。

一个基本的经验规律:如果一个对象的寿命比较长,大概率还会活很久

JVM(内存划分,类加载,垃圾回收)_第10张图片

上述规则还有个特殊情况,如果对象是一个非常大的对象,将会直接进入老年代!

你可能感兴趣的:(jvm)