目录:1.JVM内存区域划分
2.JVM类加载机制
1)类加载的过程
2)类啥时候被加载
3)双亲委派模型
3.JVM垃圾回收机制
1)垃圾回收机制(GC)含义、优点、缺点
2)GC实际工作过程(1)垃圾如何判定
(2)垃圾如何回收
3)常用的垃圾回收器
一.JVM内存区域划分
JVM在启动的时候,会申请到一整个很大的内存区域,JVM是一个应用程序,要从操作系统里申请到一整个很大的区域划分
JVM根据需要,把整个空间,分成几个部分,每个部分各有不同的功能作用。
JVM根据需要,把整个空间,分成几个部分,每个部分各有不同的功能作用
这也就说明了即使每个线程都有一个栈,线程之间栈上的内容是可以使用和访问的。
二.类加载机制
类加载就是java通过javac得到.class文件,从文件(硬盘)被加载到内存中(元数据区)这样的过程。
1)加载:把.class文件找到,打开文件,读文件,把文件内容读到内存中
2)验证:检查下.class文件格式对不对,.class是一个二进制文件,官方提供了JVM虚拟机规范,规范文档上详细描述了.class的格式
3)准备:给类对象分配上内存空间(先在元数据区占个位置),也会使静态成员变量被设置成0值
4)解析:初始化字符串常量,把符号引用转为直接引用
符号引用:字符串常量得有一块内存空间,存这个字符的实际内容还得有一个引用,来保存这个内存空间的起始地址,在类加载之前,字符串常量,此时处在.class文件中的,此时这个"引用"记录的并非是字符串常量的真正的地址,而是它在文件中的"偏移量"(占位符)
直接引用:类加载之后,才会把真正的字符串常量值给放到内存中,此时才有"内存地址",这个引用才能真正赋值成指定内存地址。
5)初始化:调用构造方法,进行成员初始化,执行代码块,静态代码块,加载父类。
一个类啥时候被加载?
1.构造类的实例的时候
2.调用这个类的静态方法/使用静态属性
3.加载子类,就会先加载其父类
双亲委派模型:描述的是加载过程,也就是找.class文件的过程,
JVM默认提供了三个类加载器
首先加载一个类的时候,先从 ApplicationClassLoader开始,但是ApplicationClassLoader不会进行加载,它会先把这个加载任务交给父亲(ExtensionClassLoader),但是ExtensionClassLoader也不会进行加载,而是把这个任务进一步交给父亲(BootstrapClassLoader),BootstrapClassLoader
想把这个任务进一步交给父亲发现父亲为null,此时它不得不加载,它会搜索自己负责的标准库目录相关的类,如果找到就加载,如果没找到就由子类加载器,也就是ExtensionClassLoader它会再去搜索扩展库相关的目录,如果找到就加载,没找到就交给子类加载器进行加载也就是ApplicationClassLoader 它去搜索用户项目相关的目录,如果找到就加载,没找到,此时它没有子类加载器了,它就会抛出类找不到的异常。
为啥会有上述逻辑:上述这套顺序其实是出自于jvm实现代码的逻辑,这段代码大概是类似于递归的方式进行的,这个顺序最主要的目的是为了保证Bootstrap能够先加载,Application能够后加载,这样就可以避免因为用户创建了一些奇怪的类比如用户写了一个java.util.HashMap这个类,按照上述流程,此时JVM加载的还是标准库的类,不会加载到用户自己写的类,这样能保证即使出现上述问题,不会让jvm已有代码混乱,最多只是让用户自己写的类不生效罢了。另一方面,类加载器可以用户自定义,上述三个类加载器,其实也是可以用户自定义的,上述三个类加载器是jvm自带的,用户自定义的类加载器,也可以加入到上述流程中,就可以和现有的加载器配合使用了。
三.垃圾回收机制(GC)
垃圾回收:就是把不用的内存帮我们启动释放
GC好处:比较省事,可以让程序员少写代码
GC坏处:需要消耗额外的系统资源,也有额外的性能开销,另外GC还存在另外一个问题,STW问题,如果有时候,内存中的垃圾已经很多了,此时触发一次GC操作,开销可能非常大,大到可能把系统资源吃了很多,另一方面,GC回收垃圾的时候可能涉及到一些锁操作,导致业务代码无法正常执行,这样的卡顿,极端情况下,可能是出现几十毫秒甚至上百毫秒。
JVM里有很多内存区域,GC主要是针对堆进行释放的,GC是以对象为基本单位,进行回收的(不是字节)。
GC实际工作过程
1.找到垃圾、判定垃圾
2.再进行对象的释放
1.找到垃圾/判定垃圾
1)引用计数(不是Java的做法,python、php)
给每个对象分配一个计数器,每次创建引用指向该对象时,计数器就加+1,每次该引用销毁了,计数器就减1,
这个方法简单有效,但是Java没有使用
1.内存空间浪费的多(利用率低)
每个对象都分配一个计数器,代码中如果对象非常少,这倒无所谓,如果对象多了,占用的额外空间就比较多
2.存在循环引用的问题
2)可达性分析:
java中的对象,都是通过引用来指向并访问的,整个java中的对象,都通过类似链式/树形结构,整体给串起来的,可达性分析,就是把所有这些对象被组织的结构视为树,就从根节点出发,遍历树,所有能被访问到的对象,标记成"可达",(不能被访问到的,就是不可达),JVM里有所有对象的名单,(每次new对象,JVM都会记录下来)通过上述遍历,把可达的标记出来了,剩下的不可达的就作为垃圾回收了。
如何清理垃圾:
1).标记清除:
这种方法比较简单,但是产生了内存碎片化的问题,被释放的空间是零散的,不是连续的,申请
内存空间要求是连续的,有可能会申请失败
2)复制算法
优点:解决了内存碎片化问题,缺点:空间利用率低,如果垃圾少、有效对象多,赋值成本极大。
3)标记整理
优点:保证了空间利用率的问题,解决了内存碎片化的问题,很明显,这种算法的缺点,效率也不高,如果要搬运的空间比较大,此时开销也比较大。
我们可以看到上述三种做法都不完美,基于上述基本策略,我搞了一个复合策略“分代回收”,把垃圾回收分不同的场景,根据情境用不同的算法
刚new出来的对象,我们把它放到伊甸区,经过一轮GC,对象就被放到幸存区了从伊甸区到幸存区,我们用的是复制算法,到幸存区之后,我们也要周期性的接受GC的检验,如果变成垃圾之后,就要被释放,如果不是垃圾就要拷贝到另一个幸存区去,两幸存区同一时刻只能用一个,在两者之间来回拷贝,这里采用的也是复制算法,由于幸存区体积不大,此处空间的浪费也能接受,如果这个对象在幸存区来回拷贝了多次,依然存在 ,此时就要进入老年代了,老年代都是年纪大的对象,生命周期普遍更长,针对老年代也要周期性GC扫描,但是频率更低了,如果老年代的对象是垃圾,使用标记整理的方式进行释放。
常见的垃圾回收器
1.CMS
2.G1
3.ZGC