Java类的加载
加载
加载,是指查找字节流,并且据此创建类的过程。即查找和导入class文件
双亲委派模型:每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载
在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。
类加载器
除了启动类加载器之外,其他的类加载器都是 java.lang.ClassLoader 的子类
启动类加载器(bootstrap class loader):C++实现,java无法访问。
扩展类加载器(ExtClassLoader):java核心类库提供,启动类加载器的子类。只加载jar包
应用类加载器(AppClassLoader):java核心类库提供,扩展类加载器的子类
自定义类加载器:自定义加载器
jdk1.9前
启动类加载器负责加载最为基础、最为重要的类,比如存放在 JRE 的 lib 目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类)。
扩展类加载器加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)。
应用类加载器加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。
jdk1.9后
Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。
链接
链接,是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。
验证:确保被加载的类能够满足Java虚拟机的约束条件
准备:为被加载类的静态字段分配内存。会进行默认值的设置
解析(可选):在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用。这个阶段可以被推迟到初始化之后,当程序运行的过程中真正使用某个符号引用的时候,再去解析它。如果此时引用指向了一个未被加载的类、字段或方法,解析将触发这个类的加载
初始化
常量值:被直接赋值的基本类型或字符串类型的final静态变量,由java虚拟机完成初始化
< clinit >:除常量值外的直接赋值的变量,静态代码块中的代码
对类的静态变量、静态代码块进行初始化。
初始化=为常量值赋值+执行< clinit >方法
类的初始化仅会被执行一次。
初始化时机
- 当虚拟机启动时,初始化用户指定的主类
- 创建类的实例(new),初始化目标类
- 调用静态方法,初始化静态方法所在类
- 访问静态字段,初始化静态字段所在类
- 子类初始化触发父类的初始化
- 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化
- 使用反射 API 对某个类进行反射调用时,初始化这个类
- 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类
Java对象内存布局
对象头=标记字段(64位虚拟机中占64位)+类型指针(64位虚拟机中占64位)
标记字段:存储java虚拟机有关该对象的运行数据,如哈希码、GC信息、锁信息
类型指针:指向该对象的类
压缩指针:对应虚拟机选项 -XX:+UseCompressedOops,默认开启。解决64位虚拟机对象头过大问题,将堆中原本 64 位的 Java 对象指针压缩成 32 位的。
垃圾回收
堆内存回收
堆结构
image.pngJava 虚拟机将堆划分为新生代和老年代。其中,新生代又被划分为 Eden 区,以及两个大小相同的 Survivor 区。
默认情况下,Java 虚拟机采取的是一种动态分配的策略(对应 Java 虚拟机参数 -XX:+UsePSAdaptiveSurvivorSizePolicy),根据生成对象的速率,以及 Survivor 区的使用情况动态调整 Eden 区和 Survivor 区的比例。
Eden区内存分配:每个线程预先分配一段连续的内存,作为线程私有的TLAB。线程内维护两个指针,一个指向TLAB内空余的起始,一个指向TLAB尾部。线程内的new指令,直接使用指针加法(把指向剩余内存位置的指针加上请求的字节数)实现,累加后指针超出TLAB尾部,当前线程申请新的TLAB。
Survivor区:两个空间相同的区域,一个是from,一个是to。只有from可以被分配,to区永远为空。
新生代垃圾回收:Eden区+Survivor区 from区空间耗尽,触发一次Minor GC,存活下来的对象被送到Survivor to区,同时交换to和from。
JVM会记录Survivor区的对象被来回复制的次数,达到阈值(15)会被晋升到老年代。另外,单个Survivor区被使用了50%,较高复制次数的对象也会晋升。
卡表
为了解决老年代对象持有新生代的引用,垃圾回收是需要扫描老年代的问题。把堆划分为多个卡,每个卡512字节,并且维护一个卡表,存储每张卡的标志(对应的卡是否可能存在指向新生代对象的引用,有则脏卡)。Minor GC的时候,从卡表中找到脏卡,加入Minor GC的GC Roots里,完成脏卡扫描后,把脏卡标志清零。
引用计数法和可达性分析
引用计数法:为每个对象添加引用计数器,统计指向该对象的引用。一旦计数器变成0,说明对象死亡,可以回收。致命缺陷:循环依赖无法处理,然后导致内存泄漏。
可达性分析:将一系列 GC Roots 作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的
GC Roots:
- Java 方法栈桢中的局部变量
- 已加载类的静态变量
- JNI handles
- 已启动且未停止的 Java 线程
多线程情况下,如果引用对象被修改,会导致
Stop-the-world 以及安全点
Stop-the-world:停止所有非垃圾回收的线程的工作,直到完成垃圾回收
安全点:垃圾回收前保证虚拟机堆栈的稳定,以便安全的执行可达性分析
垃圾回收算法
标记清除,标记整理,标记复制
垃圾回收器
新生代
Serial:单线程的标记-复制算法
Parallel New:多线程的标记-复制算法
Parallel Scavenge:多线程的标记-复制算法。吞吐量更高,不可用和CMS共用
老年代
Serial Old:单线程的标记-整理算法
Parallel Old:多线程的标记-整理算法
CMS:并发的标记0清除算法。除了少数几个操作需要 Stop-the-world 之外,它可以在应用程序运行过程中进行垃圾回收。在并发收集失败的情况下,Java 虚拟机会使用其他两个压缩型垃圾回收器进行一次垃圾回收
G1
https://www.jianshu.com/p/548c67aa1bc0
横跨新生代和老年代的垃圾回收器。直接将堆分成极其多个区域。每个区域都可以充当 Eden 区、Survivor 区或者老年代中的一个。采用的是标记 - 压缩算法,而且和 CMS 一样都能够在应用程序运行过程中并发地进行垃圾回收。
G1内存结构
G1把内存划分成多个(默认2048)大小相等的Region(默认最小1M 最大32M),Region逻辑上连续,物理内存地址不连续。每个Region被标记成E(Eden)/S(Survivor)/O(Old)/H(Humongous)
G1内存结构
H=巨型对象>=1/2 Region。直接在一个或连续多个Region中分配,并标记为H
RSet实现
https://www.jianshu.com/p/870abddaba41
每个Region初始化的时候,会初始化一个remebered set,用于记录并跟踪其他Region指向该Region中对象的引用,每个Region默认按照512k分成多个card。
image.png
Region2的RSet记录了Region1和Region3有对象引用了Region2的对象。
G1中采用post-write barrier和concurrent refinement threads实现了RSet的更新。赋值前后,jvm会插入一个pre-write barrier和post-write barrier,post-write barrier会进行dirty-card的设置。(1.字段所在card设置为dirty-card;2.如果为应用线程,每个线程有一个dirty card queue,把card插入队列;3.不是应用线程,把card放到全局共享queue)
全局队列集合超过一定阈值后,ConcurrentG1RefineThread会取出若干个队列,遍历每个队列中记录的card,并进行处理(1. 根据card的地址计算出card所在的Region;2.Region不存在、Region是young区、Region在回收集合中,不处理;3. 更新RSet)
风险:应用线程插入速度过快,ConcurrentG1RefineThread来不及处理,会由应用线程接管RSet的更新操作。refinement threads线程数量可以通过-XX:G1ConcRefinementThreads或-XX:ParallelGCThreads参数设置
SATB
GC前存活对象的快照,保证在并发标记阶段的正确性。
三色标记算法
黑色:根对象,或者该对象和它的子对象都被扫描
灰色:对象本身被扫描,但还没扫描完它的子对象
白色:未被扫描的对象。扫描完成后,白的就是不可达对象,即垃圾对象。
使用pre-write barrier把每次引用关系变化时的旧值记录到stab_mark_queue,remark阶段进行扫描判断。
G1的GC模式
Young GC
当E区不能再分配新的对象时触发,E区的对象会移动到S区,当S区空间不够的时候,E区对象直接晋升到O区,同时S区的对象移动到新的S区。过去S区对象达到一定年龄,晋升到O区。
Mixed GC
回收所有年轻和部分老年代的Region。-XX:MaxGCPauseMillis指定一个目标停顿时间,G1的预测停顿模型会挑选满足停顿时间的Region进行垃圾回收。
Full GC
G1的垃圾回收过程是和应用程序并发执行的,当Mixed GC的速度赶不上应用程序申请内存的速度的时候,Mixed G1就会降级到Full GC,使用的是Serial GC
Synchronized
声明synchronized代码块时,编译成的字节码包含monitorenter和monitorexit指令,这两种指令都会消耗操作数栈上的一个引用类型的元素,作为加锁对象。
monitorenter、monitorexit
可以理解为每个锁对象拥有一个锁计数器和指向持有该锁线程的指针。
执行monitorenter时,计数器为0或者计数器为1但是持有锁的对象为当前前程(可冲入),则计数器+1,否则等待,直到持有线程释放锁。
执行monitorexit时,计数器-1,直到减为0,表示锁被释放。
网友评论