二.类加载
- 类加载的基本流程(熟练背诵)
java代码会被编译成.class文件(包含成一些字节码),Java程序要想运行起来就需要让jvm读取到这些.class文件,并把这里面的内容构造成类对象,保存到内存的方法区中。
类加载的过程分以下步骤
加载:找到.class文件,打开文件,读取文件内容 。
//jvm会根据java.lang.String,java,util.ArrayList这种类名在一些指定的目录范围进行查找验证:.class文件是一个二进制的格式(某个字节都是有某些特定含义的)
就需要验证你当前的这个格式是否符合要求的,如图为.class文件要遵守的格式准备:给类对象分配内存空间(最终的目标就是构造出类对象)
//public static int value=123;
//这里只是分配内存空间,还没有初始化呢,此时这个空间上的内存的数值就是全0的解析:针对类对象中的包含的字符串常量进行处理,进行一些初始化操作
// final Sring s=“test”
也就是把“符号引用”(文件偏移量)替换成“直接引用 ”(内存地址),因为在文件中不涉及到内存地址,因此初始化的语句会被设置为一个”文件的偏移量“,通过偏移量就能找到这个字符串所在的位置初始化: 针对类对象进行初始化,把类对象中需要的各个属性都设置好~~
还需要 初始化static成员,需要执行静态代码块,以及还可能需要加载一下父类
2 . 双亲委派模型
属于类加载中,第一个步骤加载中的一个环节,根据全限定类名找到.class文件
- 从Application ClassLoader作为入口,开始执行查找的逻辑,它不会立即去扫描自己负责的
( 负责的是搜索项目当前的目录和第三方库对应的目录), 而是把它的查找的任务交给他的父亲 - Extension ClassLoader,也不会立即扫描自己的负责的目录,(负责的是JDK中的一些扩展库对应的目录),把它的查找的任务交给他的父亲
- BootStrap ClassLoader,也不想立即扫描自己的负责的目录,(负责的是标准库的目录),但是它没有父亲,只能亲自负责扫描标准库的目录
如果给的类不是标准库的类,任务仍然会交给它的孩子来执行,如果还没有扫描到就会抛出一个异常。
搞这个流程是为了确保标准库的类被加载的优先级最高,其次是扩展库,然后是自己写的和第三方库
三.垃圾回收
GC(垃圾回收机制):让JVM自行判定,某个内存是否不再使用,然后就会自动回收了
1.引用计数(python和php)
单独安排一块空间来保存一个计数器。这个计数器描述了有多少个引用指向它。
缺点是: 比较浪费内存,如果对象小并且很多计数器,占据的空间就难以忽视,存在循环引用问题
2.Java采取的是可行性分析
可达性分析,本质上时间换空间这样的手段~~
有一组线程周期性的扫描我们代码中所有的对象出发,尽可能的进行访问的遍历,把所有能够访问道德对象,都标记成“可达”,反之经过扫描之后未被标记的对象就是垃圾了。
而这里的可达性分析都是周期性进行的,当前某个对象是否是垃圾,是随着代码的执行会发生改变的
回收垃圾的三种方式:
标记消除:把标记的对象直接删除掉。
缺点:- 效率问题:标记和清除这两个过程的效率都不⾼
- 空间问题:标记清除后会产⽣⼤量不连续的内存碎⽚,空间碎⽚太多可能会导致以后在程序运⾏中需要分配较⼤对象时,⽆法找到⾜够连续内存⽽不得不提前触发另⼀次垃圾收集
复制算法:通过复制的方式,把有效的对象归类在一起,再统一释放剩下的空间
2,4复制到右边,其他统一释放
缺点: ① 内存要浪费一半,利用率不高② 如果有效对象多,拷贝的开销大
标记整理:既能解决内存碎片问题,又能处理复制算法中利用率
处理后
缺点:搬用的开销仍然很大
实际上JVM采取的释放思路是上述基础思路结合,通过区域划分实现不同区域和不同的垃圾回收策略
分代回收:
新生代:刚new的新对象放到这,从诞生到第一轮可达性分析扫描,这个虽然不长,但大部分对象都会成为垃圾
- 伊甸区–>幸存区复制算法,每一轮GC扫描之后,都把有效对象复制到幸存区,然后伊甸区就可以释放了
- GC扫描线程也会扫描幸存区,就会把活过GC扫描的对象拷贝到幸存区的另一个部分。幸存区之间的拷贝每一轮会拷贝多个对象,也会淘汰一批(随着时间的推移,有的对象就成了垃圾)
- 当对象在幸存区经历多轮 GC 扫描后,JVM 会判定该对象短时间内难以被释放,进而将其拷贝至老年代。
- 进入老年代的对象仍会被 GC 扫描,但老年代 GC 扫描频率远低于新生代;主要用标记整理。
按经验规律:新生代对象更容易被回收,老年代对象更容易持续存活,这样设计是为了降低 GC 扫描的开销。