JVM的主要职责管理内存
JVM基础
JVM的内部结构

- JAVA 堆(heap):是java 虚拟机所管理的内存中最大的一块,存放对象实例,即new的对象和数组(可以被所有的线程共享,不会存放别的对象引用)
- 方法区:用于存储已经被虚拟机加载的类、常量和静态变量 (可以被所有的线程共享,包含了所有的class和static变量)
- 程序计数器:是一块较小的内存空间,作用是当前线程所执行的字节码行号指示器
- JVM栈:是虚拟机描述的JAVA 方法执行的内存模型,每个方法被执行的时候都会创建一个栈帧,用于存放局部变量表、操作栈、动态链接、方法出口等信息(存放基本变量类型(含具体数值),引用对象的变量(存放引用在堆里的具体地址))
- 本地方法栈:与JVM栈作用类似,区别是本地方法栈是为Native方法服务的,JVM栈是为java方法服务
JVM中那些是共享区,那些可以作为GcRoot
- 堆和方法区是所有线程共享的,栈、本地方法栈、程序计数器是每个线程独有的
- 栈中的本地变量,本地方法栈中的变量、方法区的静态变量、正在运行的线程可以作为GcRoot
内存回收机制
内存回收的几种算法
标记清除算法 :标记清除算法主要分为有两个阶段,首先标记出需要回收的对象,然后咋标记完成后统一回收所有标记的对象; 缺点:
效率问题:标记和清除两个过程效率都不高。
空间问题:标记清除之后会导致很多不连续的内存碎片,会导致需要分配大对象时无法找到足够的连续空间而
不得不触发GC的问题。复制算法 :将可用内存按空间分为大小相同的两小块,每次只使用其中的一块,等这块内存使用完了将还存活的对象复制到另一块内存上,然后将这块内存区域对象整体清除掉。每次对整个半区进行内存回收,不会导致碎片问题,实现简单且效率高效。 缺点: 需要将内存缩小为原来的一半,空间代价太高。
标记整理算法:标记整理算法标记过程和标记清除算法一样,但清除过程并不是对可回收对象直接清理,而是将所有存活对象像一端移动,然后集中清理到端边界以外的内存。
-
分代回收算法 :当代虚拟机垃圾回收算法都采用分代收集算法来收集,根据对象存活周期不同将内存划分为新生代和老年代(新生代经过多次垃圾回收机制后都没有被回收就会被放入老年代),再根据每个年代的特点采用最合适的回收算法。
新生代存活对象较少,每次垃圾回收都有大量对象死去,一般采用复制算法,只需要付出复制少量存活对象的
成本就可以实现垃圾回收;
老年代存活对象较多,没有额外空间进行分配担保,就必须采用标记清除算法和标记整理算法进行回收; 效率对比
效率: 复制算法 > 整理算法 > 清除算法
内存整齐度:复制算法 = 整理算法 > 清除算法
内存使用率:整理算法 = 清除算法 > 复制算法
分代机制
- 年轻代:用来存放新建的对象,死亡快存在朝生夕死的情况(使用复制算法)
- 老年代:老年代中存放的对象是存活了很久的,如年轻代经历多次垃圾回收仍存活就会被放入老年代(清除和整理算法)
GC如何判断一个对象可以被回收
- 引用计数法:每个对象都有一个引用技术属性,新增一个引用计数加一,引用释放的时候计数减一,计数为零的时候表示可以回收
- 可达性分析法:从GcRoots 开始向下搜索,搜索所有走过的路劲称之为引用链,当一个对象到GcRoots没有任何引用链时,则证明引用对象是不可用的,那么虚拟机就判断是可回收对象
- 引用计数法可能会出现A引用了B,B又引用了A,这个时候就会出现他们都不再使用了,但因为计数器为1永远无法被回收
- 可达性算法中不可达对象并不是立即死亡的,对象被宣告死亡至少要经历两次标记过程,第一次是可达性分析算法发现没有引用链,第二次是由虚拟机建立的finalizer队列中判断是否需要执行finalizer()方法。如果对象在进行可达性分析后发现没有与任何的 “GC Roots” 引用链相连接,则会被第一次标记,随后会进行一次筛选过程,筛选的条件是 “该对象是否有必要执行 finalize() 方法”。只有在该对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过 时,才认为是 “没有必要执行”。
GcRoot 的对象有哪些
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)中引用的对象
- JVM的内部引用(Class 对象、异常对象NullPointException 。。。。、系统类加载器)
- 所有被同步锁(Synchronized关键字)持有的对象
- JVM内部的JMXBean 、JVMTI中注册的回调,本地缓存等
- JVM实现中的临时性对象,跨代引用的对象(在分代回收只回收部分代时)
- 正在运行的变量在当前时刻都可以称之为GcRoot,存在虚拟机栈中
native 层也是用户空间,用户空间和内核空间唯一的通讯方式就是系统调用
GC的几种引用方式
- 强引用:默认的引用方式
- 软引用:内存不足时回收
- 弱引用: GC 到来时回收 (除了强引用用的最多的引用)
- 虚引用:GC回收后可以得到一个通知
垃圾回收器
- CMS:核心思想就是将STW打散,让一部分GC线程与用户线程并发执行(concurrent mark sweep)
初始标记→并发标记 → 重新标记 → 并发清除
①、STW只标记出根对象直接引用的对象
②、继续标记其他对象与应用程序并发执行
③、STW对并发执行阶段的对象重新标记
④、并行将产生的垃圾清除,清除过程中优惠产生付浮动垃圾下次清除 - G1垃圾优先:他的内存模型中对于堆内存不再分代,而是分成一个个Region的小块,每个Region可以隶属不同的年代(实际不分代,逻辑分代)
初始标记→ 标记Region → 并发标记 → 重新标记 → 回收
①、标记出GCRoot 直接引用的对象 STW
②、通过rest 标记出上个阶段标记的Region引用到的old区Region
③、跟CMS差不多,但是遍历范围不在是整个old区,而只需要遍历第二步标记出来的Region
④、和CMS差不多
⑤、和CMS不一样,G1采用拷贝算法,G1只选择垃圾多的清理 - CMS 核心算法:三色标记:是一种逻辑上的抽象,将每个内存对象分成三种颜色
黑色:自己和成员变量都标记完
灰色:自己标记完了
白色:自己未标记
通过增量标记解决漏标问题
类加载
类的生命周期

- 加载:查找并加载类的class 文件,将class文件字节码内容加载到内存中,并将这些数据转换成方法区的运行时数据结构,然后生成代表这个类的java.lang.class 对象
- 链接:包括验证、准备、和解析三个阶段,将java类的二进制代码合并到JVM的运行状态之中的过程
- 验证:确保被导入的类型的正确性
- 准备:为类的静态字段分配字段并默认初始化这些字段
- 解析:虚拟机常量池内的符号引用替换成直接引用
- 初始化:将类变量初始化为正确的初始值
类加载器
- 根加载器 :用来加载核心库,JVM自带的类加载器
- 扩展类加载器:负责jre/lib/ext目录下的jar包或者java.ext.dirs指定目录下的jar包装入工作库
- 系统类加载器:咱自己的类加载器
根加载器和扩展类加载器 负责加载javaHome 下的所有jar包和class文件
双亲委托机制
- 向上委派:实际上就是查找缓存,是否加载了该类,有则返回,没有则继续向上委派到顶层后缓存中还是没有,则到加载路径中查找,有则返回,没有则向下查找
- 向下查找:查找加载路径,有则返回没有则向下查找
向上委派到类加载器为止,向下查找到发起类加载的加载器为止
双亲委托机制的意义:
安全性:避免用户些的类替换了java 核心类
同时也避免了类的重复加载,因为jvm中区分类的不同,不仅仅是根据类名,相同class文件被不同加载器加载也会是两个不同类
类加载器的作用
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区运行时数据结构,然后再堆中生成一个代表这个类的class对象,作为方法区中类数据的访问入口
类缓存
标准的JavaSE 类加载器可以按照要求查找类,但一旦某个类被加载到类加载中,他将维持加载一段时间,不过JVM垃圾回收机制可以回收这些class对象
对象的创建
- 判断对象对应的类是否创建
- 为对象分配内存空间,两种方式
a、指针碰撞:找到空闲指针,然后从开始位置往后移n个指针(内存空间规整)
b、空闲列表:从空闲列表中找到满足条件的内存空间 - 处理并发安全问题
a、 CAS算法配上失败重试机制
b、本地线程分配缓冲区 - 初始化分配到的内存空间
将分配到的内存空间出对象头之外都初始化为零 - 设置对象的对象头
将对象的所属类,对象的HashCode 和对象的GC分代数据存储在对象头中 - 执行init 方法进行初始化