1. 类的加载过程
加载:
1)通过一个类的全限定名获取定义此类的二进制字节流
2)将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
补充:加载 .class 文件的方式:
1)从本地系统中直接加载;
2)通过网络获取,如:web applet
3)从 zip 压缩包中读取,日后 jar、war 格式的基础
4)运行时计算生成,如:动态代理技术
5)由其他文件生成,如:jsp 应用
6)从专有数据库中提取 .class 文件
7)从加密文件中获取,典型的防 class 文件被反编译的保护措施
链接:
验证:
-- 确保 class 文件的字节流中包含信息符合当前虚拟机规范,保证被加载类的正确性,不会危害虚拟机自身安全
-- 主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证
准备:
-- 为类变量分配内存并且设置该类变量的默认初始值
-- 这里不包含用 final 修饰的 static ,因为 final 在编译的时候就会分配了,准备阶段会显示初始化;
-- 这里不会为实例变量分配初始化,类变量会分配在方法区,而实例变量是会随着对象一起分配到 Java 堆中
解析:
-- 将常量池内的符号引用转换为直接引用的过程
-- 事实上,解析操作往往会伴随着 jvm 在执行完初始化之后再执行
-- 符号引用就是一组符号来描述所引用的目标,符号引用的字面量形式明确定义在 class 文件格式中,直接引用就是 直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
-- 解析动作主要针对类或接口、字段、类方法 、接口方法、方法类型等,对应常量池中的 CONSTATN_CLASS_INFO、CONSTATN_FIELDREF_INFO、CONSTATN_METHODREF_INFO等
初始化:
1)初始化阶段就是执行类构造器方法 <clinit>() 的过程
2)此方法不需定义,是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
3)构造方式中的指令按语句在源文件中出现的顺序执行
4)<clinit>() 不同于类的构造器,构造器是虚拟机视角下的 <init>()
5)若该类有父类,JVM会保证子类的 <clinit>() 执行前,父类的 <clinit>() 已经执行完毕
6)虚拟机必须保证一个类的 <clinit>() 方法在多线程下被同步加锁
2. 类加载器的分类,双亲委派机制
引导类加载器(Bootstrap classloader):嵌套在 jvm 内部的,用来加载 Java 核心类库,用于提供 jvm 自身需要的类;并不继承自 java.lang.Classloader,没有父加载器;加载扩展类和应用程序类加载器,并指定为他们的父类加载器;出于安全考虑,Bootstrap classloader 启动类加载器只加载报名为 java、javax、sun等开头的类
扩展类加载器(Extension Classloader):Java 语言编写,由 sun.misc.Launcher$ExtClassLoader 实现;派生于 ClassLoader 类;从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 jdk 的安装目录的 jre/lib/ext 子目录下加载类库,如果用户创建的 jar 放在此目录下,也会自动由扩展类加载器加载
应用程序类加载器(AppClassLoader):Java 语言编写,由 sun.misc.Launcher$AppClassLoader 实现;派生于 CLassLoader 类,负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库;该类加载是程序中默认的类加载器,一般来说,Java 应用的类都是由它来完成加载;通过 Classloader#getSystemClassLoader() 方法可以获取到该类加载器
用户自定义类加载器:隔离加载类;修改类加载的方式;扩展加载源;防止源码泄露
双亲委派机制:Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要使用该类是才会将它的 class 文件加载到内存生成 class 对象,而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理
工作原理:
1)如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
2)如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
3)如果父类加载器可以完成类加载任务,就成功返回;倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载
优势:避免类的重复加载;保护程序的安全,防止核心 API 被随意篡改
JVM 主要组成部分及其作用
类加载子系统:
运行时数据区:
执行引擎:
本地库接口
本地方法库
作用:首先编译器把 Java 代码转换成字节码,通过类加载子系统再把字节码加载到内存中,将其放在运行时数据区的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎,将字节码指令翻译成底层系统指令,再交由 CPU 去执行,而这个过程需要调用其他语言的本地方法库来实现整个程序的功能
JVM 运行时数据区:
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这个区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁
程序计数器:当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选去下一条需要执行的字节码指令的,线程独享的,也是JVM规定不会发生OOM的区域
本地方法栈:和虚拟机站作用一样,只不过本地方法栈是为虚拟机栈调用 native 方法服务的,线程独享的
虚拟机栈:用于存储局部变量表、操作数栈、动态链接、方法返回值等信息,现场独享的
方法区:用于存储虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据,线程共享的
堆区:Java 虚拟机中内存最大的一块区域,是被所有线程共享的,几乎所有的对象实例都是在这里分配内存
为对象分配内存:
当类加载完成后,接着会在 Java 堆中划分 一块内存分配给对象,内存分配根据 Java 堆是否规整,有两种方式:
指针碰撞:如果Java堆的内存是规整的,即所有用过的内存放在一边,而空闲的放在另一边,分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作;
空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一张列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录
内存分配保证现场安全的两种方式:
1)采用 CAS + 失败重试来保障更新操作的原子性;
2)本地线程分配缓冲(Thread Local Allocation Buffer,TLAB):把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,哪个线程需要分配内存,就在哪个现场的 TLAB 上分配,只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁
JVM 有哪些垃圾回收算法
标记-清除算法:标记无用对象,然后进行清楚回收;效率不高,会产生内存碎片
复制算法:按照容量划分两个大小相等的内存区域,当一块用完的时候,将继续存活的对象复制到另一块内存上,然后 再把已使用的内存空降一次清理掉;内存使用率不高,只有原来的一半
标记-整理算法:标记无用对象 ,让所有存活的对象都向一端移动,然后直接清楚掉另一端的内存
分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代 ,新生代采用复制算法,老年代采用标记整理算法
JVM 有哪些垃圾收集器:
serial 收集器:新生代单线程收集器,采用复制算法,标记和清理都是单线程,简单高效
ParNew 收集器:新生代并行收集器,采用复制算法,可以看作是 serial 收集器的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现;
Parallel Scavenge 收集器:新生代并行收集器,采用复制算法,追求高吞吐量,高效利用 CPU。吞吐量 = 用线程时间 /(用户线程时间 + GC线程时间),高吞吐量可以高效的利用 CPU 的时间,尽快完成运算任务
Serial Old收集器:老年代单线程收集器,采用标记-整理算法,搭配 serial 年轻代收集器
Parallel Old收集器:老年代并行收集器,采用标记-整理算法吞吐量优先,搭配 Parallel Scavenge 年轻代收集器
CMS。收集器:老年代并发收集器,采用标记-清除算法,以获取最短回收停顿时间为目标的收集器,具有高并发 、低停顿的特点,搭配 ParNew 年轻代收集器
G1 :Java堆并行收集器,逻辑分代,物理不分代
GC ROOTS 对象:
虚拟机栈中引用的对象(方法参数、局部变量表等);
本地方法所引用的对象;
方法区中静态属性、常量所引用的对象;
虚拟机内部的引用(基本数据类型CLass对象、系统类加载器、常驻的异常对象等)
所有被 synchronized 持有的对象;
反应 Java 虚拟机内部情况的JMXBean、JVNTI中注册的回调、本地代码缓存等