本文内容:
- 一次编译到处运行
- Java对象
- Java内存模型(JMM)
- GC
- JVM参数
一次编译到处运行
JVM屏蔽了底层的区别
java如何编译成字节码?
- 解析与填充符号表
- 词法分析、语法分析
- 填充符号表(KV对,标识符与它的声明类型、作用域、地址等)
- 注解处理
- 语义分析与字节码生成
- 标注检查:
- 变量声明
- 变量与赋值的类型匹配
- 数据及控制流分析:
- 局部变量使用前是否赋值
- 每条路径是否都有返回值
- 受检异常是否都处理
- 解语法糖
- 字节码生成
Class文件结构
先明确一点,Class是由java源码编译而来,所以它必定包含了类中的所有信息,包含
- 类名、类修饰符、继承的类、实现的接口
- 字段名、字段类型、字段修饰符
- 方法名、方法的修饰符、方法引用的类型(形参、返回类型)、方法内部的具体代码
- 字面量
所以我们要做的就是把它们以一种结构化的方式组织起来,Class的结构概览:
- 魔数(简单判别是否是Class文件),版本号
- 常量池:字面量、符号引用(类名、接口名、字段名、方法名、变量名)
- 标志:类修饰符
- 类自身的引用,父类的引用,接口的引用
- 字段表,方法表,属性表
需要注意的是常量池会被其他部分频繁引用,因为它存储了所有的名,字段、方法、类都需要一个名,所以包含一个指向池中常量的指针
类加载过程
输入:class的二进制字节流,输出:方法区的Class对象,整个过程的几个阶段以及阶段任务如下图
- 加载:将字节码文件的二进制字节流加载到JVM中,开发者可自定义类加载器(重写loadClass())来决定从哪加载
- 主要做什么:
- 将二进制字节流加载到JVM中
- 将其代表的静态结构转化为方法区的运行时数据结构
- 实例化一个Class对象,作为访问这些数据的外部接口
- 几种情况对类主动引用的情况必须要立刻加载class文件:
- 实例化对象、引用类变量、类方法、内部静态类
- main的类
- 加载子类时,父类要先被加载
- 反射调用类时
- 验证:检验字节码的安全性,防止对JVM造成危害
- 文件格式验证
- 是否以魔数开头
- 版本号是否可以处理
- UTF8常量中是否有不符合UTF8编码的数据
- ...
- 元数据验证(类级别的语法)
- 是否继承了类
- 是否实现了抽象父类中的所有方法
- 是否继承了final类型的类
- ...
- 字节码验证(方法体)
- 是否有不合法的强制类型转化(父类转子类)
- 跳转指令不会跳到方法体以外的指令上
- 数据类型和指令配合无误,不会用加载long的指令来加载int
- ...
- 符号引用验证
- 符号引用中的可访问性是否能被当前类访问
- 类符号引用能否找到类
- 指定类中是否存在 指定名和描述符的 方法和字段
- ...
- 准备:对类变量分配内存、赋零值(数据类型的默认值)
- 解析:符号引用(字面量,如类的全限定名、方法描述符) -> 直接引用(内存地址、偏移量)
- 类/接口的解析
- 假定当前代码位于类D处,要将一个符合引用N解析成类C
- 非数组,交给D的类加载器去加载
- 数组,其元素类型交给D的类加载器加载,JVM生成一个代表数组维度和元素的对象
- 符号引用验证,确定D可以访问C
- 字段解析(子类和父类的同名字段,拒绝编译)
- 假定字段属于类C,先加载C
- 在C中查找是否有符号 该字段的简单名称、描述,有就返回其直接引用
- 递归在实现的接口中(自下向上)查找
- 递归在继承的类中查找
- 报异常
- 类方法解析
- 加载类C,如果符号引用的是接口,则报异常
- 在类C中查找是否符合 方法名、描述符的,有则返回直接引用
- 在继承体系上递归向上查找
- 在实现接口中递归向上查找
- 报错,找到成功后要进行权限验证,看是否可访问
- 接口方法解析
- 加载接口I,如果符合引用的是类,则报异常
- 在I中查找是否符合 方法名、描述符的,有则返回直接引用
- 在继承体系上递归向上查找
- 报错,接口方法一定是public的,不用进行权限验证
初始化:执行类构造函数cinit函数
Java对象
new一个对象的过程
- new一个对象,分配内存
- 指针碰撞:空间整理型回收器,移动指针
- 空闲列表:复制型,分配一个空闲的内存块
- 并发情况:CAS 或 TLAB本地线程分配缓冲(堆是线程共享的,分配对象空间时需要进行同步,给每个线程建立一个独享的空间,在上面分配)
- 执行构造函数,进行初始化
- 使用
- 回收资源
- GC
注:Java创建子类对象时,不会创建父类对象,只是调用父类对象的构造来进行初始化(继承了父类的成员)
栈上分配
- 线程私有的对象,减小GC
- 逃逸分析
- 如果对象仅在方法体内使用
- -XX:DoEscapeAnalysis
- 标量替换
- 作用:如果未用到对象的全部成员,可以节约内存
- 做法:用一些基本类型的局部变量替换对象
- -XX:EliminateAllocations
对象内存布局
对象头:指向类型的指针、运行时信息(hashcode,GC年龄,锁信息),数组还有长度信息
实例数据
填充对齐:HotSpot要求对象必须是8byte的整数倍
访问定位
句柄:
直接指针:
对比
句柄 直接引用 优点 GC对象移动时不需要修改栈中引用(对栈操作比较麻烦,对线程空间进行修改--需要再写回内存) 1次内存访问 缺点 2次内存访问 对象变化时要需要修改栈中引用
Java内存模型(JMM)
GC
判断对象是否需要回收:引用计数、可达性分析,从GCRoot(当前栈中对象、全局对象、常量)出发,形成引用链,未经过的即为需要回收的对象
GCROOT:栈中引用的对象、方法区类静态成员引用的对象、方法区常量引用的对象
G1对比CMS
G1 CMS 优先收集,空间分为Region,维护每个Region回收时间、价值,以便实现可预测的停顿 Concurrent Mark Sweep
并发、标记清理
并行,可充分利用多核CPU优势
空间碎片 局部是基于复制,整体是基于标记-整理(复制时往某一边复制),无碎片
有碎片
缺点 收集阶段用户线程继续工作,产生浮动垃圾,预留空间不够时导致Concurrent Mode Failure,引起Full GC
- 初始标记(STD)
- 并发标记
- 最终标记(STD)
- 筛选回收(STD)
- 初始标记(STD)
- 并发标记
- 重写标记(STD)
- 并发清除
选择 可预测的停顿时间、并行 吞吐量
GC调优
- 目标:保证最大停顿时间,尽可能提高吞吐量
- 做法:
- 分析GC日志,计算 吞吐量、最大停顿时间
- 查找FullGC出现原因,避免FullGC,减小MinorGC
- 根据场景选择GC回收器、设定合适的GC器参数 + JVM参数
Minor和FullGC的区别:
- Minor只针对young,当引用到old时就停止扫描
- FullGC针对全局,扫描量大
- Minor基于复制
JVM参数:
- -Xmx: maximun heap size
- -Xms:最小堆大小
- -Xmn: new 新生代
- -xss: 内存栈初始大小
- -XX:NewRatio 新手代:老年代
- -XX:SurvivorRatio=4: Eden:survivor
- 持久代(类信息)
内存泄露:
- 静态集合内的对象,长期对象持有短期对象的引用
- 各种连接没close