三、垃圾回收GC
3.1 如何判断对象已死?
-
引用计数法
- 在对象中添加一个引用计数器
- 每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一
- 回收的时候对计数为0的对象进行清除,即清除未使用的对象
-
可达性分析
-
通过可达性分析算法来判定对象是否存活
-
该算法基本思路是通过一系列称为
GC Roots
的根对象作为起始节点集根据引用关系向下搜索 -
搜索过的路径叫做
引用链
-
GC Root对象包括一下几种
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象,常量引用的对象
- 本地方法栈中Native方法引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的Class对象
- 所有被同步锁(synchronized关键字)持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
3.2 垃圾回收算法
-
标记清除算法
- 算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象
-
标记复制算法
- 标记复制算法涉及到From Survivor区和To Survivor区
- 该算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
-
标记整理算法
- 标记过程和标记清除算法一样,但是后续步骤不是直接对对象进行清理,
- 让所有存活的对象向内存一端移动,然后直接清理掉边界以外的内存
标记整理算法相比前两种算法需要移动对象,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,这种操作必须全程暂停用户应用程序才能进行
对于前两种不移动整理的算法,在内存分配的时候会稍微复杂一点
四、类加载器
4.1 类的生命周期
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)
4.2 类加载的过程
4.2.1 加载
类加载(Loading)阶段虚拟机需要完成以下三件事情:
- 通过类全限定名获取类的二进制字节流
- 将这个类的字节流转化为方法区的运行时数据结构
- 在内存中生成一个代表该类的java.lang.Class对象
4.2.2 验证
验证是连接的第一步,这一阶段目的是确保Class文件字节流中包含的信息是否符合规范
例如:文件格式校验,字节码校验,符号引用校验
4.2.3 准备
准备阶段是正式为类中定义的变量(即静态变量 ,被static修饰的变量)分配内存并设置类变量初始值的阶段。这些变量使用的内存都应该在方法区分配
4.2.4 解析
解析阶段是Java虚拟机将常量池内的符号引用 替换为直接引用 的过程
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行
4.2.5 初始化
初始化是类加载过程的最后一个步骤,初始化阶段就是执行类构造器<clinit>()方法的过程
<clinit>()方法与类的构造函数<init>不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行 完毕。
4.3 类加载器
类加载器: 类加载器只用于实现类的加载动作
类与类加载器: 需要注意的一点,即使这两个类来源于同一个 Class文件,被同一个Java虚拟机加载,只要加载它们的**类加载器不同,那这两个类就必定不相等
**
4.3.1 双亲委派机制
双亲委派机制是JDK1.2的时候才出现的
双亲委派模型:绝大多数Java程序都会使用以下3个系统提供的加载器进行加载
- 启动类加载器(Bootstrap Class Loader): 负责加载
<JAVA_HOME>\lib目录
的类,或者被-Xbootclasspath
参数所指定的路径中存放的 - 拓展类加载器(Extension Class Loader): 负责加载
<JAVA_HOME>\lib\ext目录
的类,或者被java.ext.dirs
系统变量所指定的路径中所有类库 - 应用程序类加载器(Application Class Loader): 负责加载用户类路径(ClassPath)上所有的类库。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器
双亲委派机制: 双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器 去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
为什么要有双亲委派机制 ?
因为类加载器不同,加载的类一定不同。
有了双亲委派机制,加载一个类最终都是要委托给顶端能加载的加载器来加载,这就
保证了加载同一个类的加载器一直都一个
4.3.2 破坏双亲委派机制
在Java发展过程中是出现了几次破坏双亲委派机制的
- 第一次破坏:
- 第一次破坏其实发生在双亲委派模型出现之前,在JDK1.2以前类加载器的概念和java.lang.ClassLoader抽象类已经出现了。当时加载方法是按照用户的意愿来加载类的
- 为了兼容原来的代码,避免
loadClass() 双亲委派的实现方法
被子类覆盖,只能在JDK1.2的ClassLoader中添加一个findClass()
方法由用户重写这个方法 - 这样如果loadClass方法调用父类加载失败就会自动调用findClass来执行用户的加载逻辑
- 第二次破坏:
- 第二次“被破坏”是由这个模型
自身的缺陷导致的
,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题,即越基础的类由越上层的加载器加载,因为基础类一般总是要被用户继承和调用API - 如果有基础类型又要调用回用户的代码,那该怎么办呢?
- 例如JNDI服务,JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(SPI)的代码。现在问题来了,启动类加载器不可能认识ClassPath下类,怎么办呢
- 解决方法: 引入了一个不太优雅的设计:
线程上下文类加载器
这个类加载器默认就是应用程序类加载器 ,有了这个就可以加载SPI服务代码了,正因为有了这个就破坏了双亲委派机制
- 第二次“被破坏”是由这个模型