JVM部分内容
1.JVM内存区域划分
为什么要划分内存区域,JAVA虚拟机是仿照真实的操作系统进行设计的,JVM也就仿照了它的情况,进行了区域划分的设计。
JAVA进程也就是JAVA虚拟机会从操作系统申请内存空间给进程使用,JVM内存空间划分,就相当于JVM进程自身从操作系统申请到内存空间,再把内存空间按照不同的功能进行分配。
1.具体划分
1.程序计数器
这是一个很小的区域,只是用来记录当前指令执行到哪个地址了。
2.元数据区
保存当前类被加载好的对象
3.栈
保存方法的调用关系。
每次调用方法,就会进入方法内部执行,当执行完毕就会返回调用位置,继续往后走。
栈这个空间不大,一般是几十几百MB,大部分情况下是够用的,少数情况下可能会出现栈溢出。
4.堆
保存new的对象
Test t = new Test();
左边的t如果是一个局部变量,t就是在栈上。如果是一个成员变量,t就是在堆上。如果是一个静态成员变量,t就是在元数据区。
右边的new Tests()一定是在堆上。
堆是JVM中最大的空间区域,往集合类里添加元素也是保存在堆里,如果堆上的对象不再使用的话,就会被释放掉(垃圾回收)。
方法元信息和类元信息都是类对象提供的,指的是一些属性,比如类叫什么名字,是不是public,继承自哪些类,实现了哪些接口......方法叫什么名字,参数有几个,返回值是什么类型......
元数据区和堆整个JAVA进程共用一份,
程序计数器和栈一个进程中可能有多份(一个线程一份)
2.类加载机制
1.类加载的步骤
类加载一共有三个阶段,其中第二个阶段又被分为3个步骤,所以一共有5个步骤。
1).加载:找到.class文件
根据类的全限定名(包名+类名,形如java.lang.String),打开文件,读取内容到内存中。
2).验证:解析,校验.class文件读到的内容是否是合法的,并且把这里的数据转成结构化的数据。.class文件这个二进制文件格式是由明确要求的。JAVA写的代码都会转到下面这个.class中,只不过会换成二进制的表现形式。
magic表示魔数,区分不同的二进制文件类型,这是一个固定值,不同的二进制文件有不同的取值
u4表示4个字节的无符号位整数,u2表示2个字节的无符号位整数。
cp_info,method_info......表示其他的结构体。
3).准备:给类对象申请内存空间,此处申请的内存空间相当于是"全0"空间。
4).解析:针对字符串常量进行初始化。
字符串常量本身就包含在.class文件中,需要把文件中的字符串常量解析出来放到内存空间里(元数据区,常量池)
5).初始化
针对刚才谈到的类对象进行最终的初始化,针对类对象的各种属性进行填充,包括类中的静态成员。如果这个类有父类,而且这个父类没有被加载,那么这个父类也会触发类加载。
6).类加载触发的时机
这里使用懒汉模式,用到哪个类就加载哪个类
1.构造某个类的实例
2.调用/使用类静态属性,静态方法
3.使用某个类的时候,如果这个类的父类还没有加载,那么就会给这个父类触发类加载
2.双亲委派模型
这个模型描述了类加载中,根据全限定类名找到.class文件的过程。
JVM中有专门的模块负责类加载,叫做类加载器。JVM提供了三种类加载器,分别是BootstrapClassLoader,ExtentionClassLoader,ApplicationClassLoader。
它们三个并不是父类子类关系,而是使用parent引用指向。这三个类加载器首当其冲的就是找.class文件
寻找过程
先从ApplicationClassLoader作为入口开始,然后把加载类的过程委托给父类完成
父类ExtentionClassLoader不会立即开始查找,而是把任务委托给它的父类完成
BootstrapClassLoader也想委托给父类,可是它没有父类,只能自己进行类加载,根据类名找标准库范围,是否存在匹配的.class文件
BootstrapClassLoader没有找到就会把任务归还给子类ExtentionClassLoader,接下来ExtentionClassLoader就会进行查找。
ExtentionClassLoader没有找到就会把任务归还给子类ApplicationClassLoader,接下来ApplicaitonClassLoader就会进行查找,没找到就抛出异常。
程序员是可以自定义类加载器的,自定义的时候可以把类加载器放到双亲委派模型当中也可以不放在里面。
3.垃圾回收机制
就是指JAVA释放内存的手段
1.GC如何回收各个区域
1.程序计数器,线程销毁,自然就释放
2.栈,方法执行结束,栈帧就结束,自然就释放
3.元数据区,类对象一般不会释放
4.堆,创建很多对象,会有旧的对象消亡。
说是“回收内存”,本质上是“回收对象”
2.找垃圾
1.引用计数
这个方案是python,php在使用
每个对象在new的时候都会搭配一个小的内存空间,这个空间保存一个整数用来计数,这个整数表示当前有多少个引用指向它。如果引用计数为0,就表示这是个垃圾。
缺点
1.内存消耗得更多,尤其是对象本身比较小时,引用计数消耗的比例就更大
2.可能出现“循环引用”的问题
此时虽然这两个对象引用计数不为0,但是它们没法使用。
2.可达性分析
这个方案是java在使用
1.以代码中的特定对象作为遍历的起点"GCROOT"
这个对象可以是,栈上的局部变量,常量池引用指向的对象,静态成员
2.尽可能进行遍历
判定某个对象是否能遍历到
3.每次访问到一个对象都会把这个对象标记为“可达”,当完成所有对象的遍历之后,没有被标记成“可达”的对象就是“不可达”。一共有多少个对象JVM是知道的,知道哪些是可达的,那么剩下的就是不可达。
可达性分析的过程是周期性的。
3.回收垃圾
1.标记-清除
把垃圾对象的内存直接进行释放,这样做会产生内存碎片问题
这样会导致空闲的内存空间不是连续的,这样是无法申请一个大一些的内存的
2.复制算法
只使用一半的内存空间,当清理垃圾时,把不是垃圾的对象拷贝到另一半,然后整体回收。
这样使用内存的利用率比较低,同时当不是垃圾的对象比较多时,复制的开销大。
3.标记-整理
解决了空间碎片和内存利用率的问题。但是内存搬运的操作开销也比较大。
4.分代回收
这是JAVA使用的方案
当某个对象经过一轮GC之后,它的年龄就会加1
针对不同年龄的对象采取不同的策略
如果某个对象年龄比较大,那么它大概率还会继续存在很久(要死早死了,之所以没死,是因为有特殊之处)
新创建的对象就放到伊甸区,绝大部分伊甸区的对象是活不过第一轮GC的。伊甸区到幸存区使用的是复制算法,因为复制的规模小,开销可控
幸存区的对象也要经历GC的扫描,每一轮GC都会消灭一大部分对象,剩余的对象再经过复制算法复制到另一个幸存区
如果这个对象在幸存区经历了多次复制都存活了下来,就会晋升到老生代,老生代使用的就是标记-整理算法了。