JVM
- JVM内存区域划分
- JVM类加载机制
- JVM垃圾回收机制
- 【哪些内存需要被JVM中垃圾回收机制回收】
- 【JVM中垃圾回收机制的基本单位】
- 【JVM中垃圾回收机制是如何判断对象是否是垃圾】
- 【如何回收垃圾】
JVM 是 Java Virtual Machine 的简称,意为 Java虚拟机。
虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。
常见的虚拟机:JVM、VMwave、Virtual Box。
JVM 和其他两个虚拟机的区别:
-
VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
-
JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪。
JVM 是一台被定制过的现实当中不存在的计算机。
JVM内存区域划分
JVM内存区域主要有四部分:
- 堆
- 栈
- 程序计数器
- 方法区
【堆】
堆的作用:程序所创建的对象都是保存在堆里面的。
也就是说,堆的内存空间是最大的
【栈】
栈细分,还可以分为:
- Java虚拟机栈:Java代码使用的栈
- 本地方法栈:给JVM内部C++代码使用的栈,也就是给JVM中的本地方法使用的栈
栈中存放最关键的信息是方法之间的调用关系和局部变量。
【方法区】
方法区里面存放的是类对象
类对象里面有什么?类对象里面有代码和静态变量
方法区里面还有个运行时常量池
运行时常量池是方法区的一部分,存放字面量与符号引用。
字面量 : 字符串**(JDK 8** 移动到堆中**)** 、final常量、基本数据类型的值。
符号引用 : 类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符
【程序计数器】
程序计数器的作用:程序计数器只是用来记录程序执行到哪里
程序计数器是占地最小的区域
总结:
堆和方法区是在整个Java进程只有一份,也就是说堆和方法区是线程共享的。
栈和程序计数器在每一个线程都有一份,也就是说栈和程序计数器是线程私有的。
简单的效果图:
JVM类加载机制
Java进程启动(JVM启动)将.class文件从磁盘读取到内存,并且创建类对象的过程叫做类加载。
类加载主要分为三个步骤:
- 加载
- 连接
- 验证
- 准备
- 解析
- 初始化
加载:找到.class文件,打开文件,读文件,创建空的类对象
连接
验证:检查.class文件格式是否符合规范要求
准备:给静态变量分配内存空间,空间里面填充0值
解析:把字符串常量进行初始化,“把符号引用替换成直接引用”
初始化:针对类中的静态变量进行初始化,执行静态代码块,加载父类
【双亲委派模型】描述的是类加载中“加载阶段”
说到双亲委派模型就必须说一说类加载器,类加载器的作用就是负责把类给加载起来。
JVM中自带了三个类加载器:BootStrapClassLoader,ExtensionClassLoader,ApplicationClassLoader.
BootStrapClassLoader:负责加载标准库中的类
ExtensionClassLoader:负责加载一些扩展类
ApplicationClassLoader:负责加载应用程序里程序员自己写的类
在JVM中,给这三个类约定了一个父子关系:
BootStrapClassLoader是ExtensionClassLoader的父类
ExtensionClassLoader是ApplicationClassLoader的父类
双亲委派模型就是在上述的体系中,展开的规则:
要加载的类都先调用ApplicationClassLoader类加载器进行加载,但是ApplicationClassLoader类加载器并不做任何操作,把要加载的类交给其父类进行加载,以此类推,直到最后一个类加载器的时候,在来判断要加载的类是否是该加载器负责加载的,不是则往回走,直到加载完这个类。
JVM垃圾回收机制
在Java中,程序员不需要考虑回收内存的事情,我们想要开辟内存就开辟内存,JVM中有垃圾回收机制会帮助程序员回收没用的内存空间。
【哪些内存需要被JVM中垃圾回收机制回收】
JVM中的垃圾回收机制主要回收的是堆里面的内存空间。
【JVM中垃圾回收机制的基本单位】
JVM中垃圾回收机制是以对象为单位进行回收的,而不是字节
【JVM中垃圾回收机制是如何判断对象是否是垃圾】
<1>引用计数法
引用计数法:给对象增加一个计数器,计数器用来记录该对象被引用的次数,每当有一个地方引用了该对象,则计数器加一,当某个地方引用失效,计数器减一。当计数器里面的值为0时,对象就会被当作垃圾清除。
引用计数法的优点:
- 引用计数法能够很好地解决“如何判断对象是否是垃圾”这个问题
- 引用计数法实现简单,判定效率比较高
引用计数法的缺点:
- 在多线程的环境下,需要修改同一个计数器,必须考虑到线程安全的问题
- 可能导致循环引用的问题(重大缺陷)
- 如果对象比较少,并且比较多,引用计数就会造成不小的空间开销
<2>可达性分析
以代码中的一些特殊的变量作为起点,然后以起点出发,看看哪些对象能被访问到,只要能被访问到,则被标记为”可达“,当完成一遍后,剩下的对象就是”不可达“,会被标记为垃圾。
起点叫做”GC Root“
哪些变量可以称作”起点“(GC Root):
- 局部变量表中的引用(栈里面的局部变量)
- 方法区中,静态引用成员
- 常量池中对应的对象
JVM中采用”可达性分析“来判断对象是否是垃圾
【如何回收垃圾】
经典的垃圾回收算法
<1>标记-清除算法
首先,通过”可达性分析“标记出要回收的对象,然后统一回收就行了
标记-清除 算法的不足:
- 使用标记-清除 算法会引入 内存碎片 的问题,释放了对象,产生了很多有用的空间但是空间并不是连续的,要分配大一些的空间就不行了
- 标记和清除这两个过程的效率都不是很高
<2>复制算法
复制算法解决了标记-清除算法中的内存碎片的问题
复制算法将内存分为两部分,每次只能使用一部分,当要回收垃圾的时候,将还存活的对象复制到另外一部分,然后在将已使用用的部分统一释放了。
复制算法最大的缺点就是可利用的空间减少了(空间利用率不是很高)
<3>标记-整理算法
标记-整理算法 很像顺序表的删除操作,将存活的对象放在空间的前面,统一将后面的空间释放。
标记-整理算法的缺点:比较耗时,效率比较低
标记-整理算法的优点:解决了内存碎片的问题和空间利用率的问题
<4>迭代回收算法
迭代回收算法结合了复制算法和标记-整理算法
迭代回收算法引入”年龄“的概念。
年龄的大小判断:经历垃圾回收(GC)的次数
迭代回收算法将对象分为新生代和老年代,将内存分为两部分,一部分放新生代,一部分放老年代。
一般的新生代的对象在第一次垃圾回收的时候就清除了,在新生代的区域使用复制算法
一般的老年代中的对象存活率比较高,所以采用标记-整理算法。
迭代回收算法大致过程描述
1.新对象都保存在伊甸区
2.新对象一般是活不过一轮GC(垃圾回收),存活下来的对象保存到幸存区中。
从伊甸区到幸存区这个过程相当于复制算法。由于存活的对象比较少,复制的开销不是很大
3.幸存区的对象,又会经历多轮GC,每一轮GC都会淘汰一些对象,存活下来的对象,通过复制算法保存到另一个幸存区
4.在幸存区中经历多轮GC还在的对象,会被认为该对象一时半会儿销毁不掉,会被保存到老年代区域
5.老年代里面的对象的存活率比较高,要销毁对象使用的是标记-整理算法
注意:如果创建的对象太大的话,对象会直接保存到老年带区域
垃圾回收算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。