JVM
一、虚拟机
虚拟机:模拟某种计算机体系结构,执行特定指令集的软件
- 系统虚拟机(Virtual Box,VMware)
- 程序虚拟机(JVM,.NET CLR,P-Code)
Java虚拟机:通过Java TCK(Technology Compatibility Kit)的兼容测试的Java语言虚拟机(可以执行Java语言的高级语言虚拟机)。
三大商用JVM
- Oracle HotSpot(本文所讲内容)
- JDK1.2 开始加入Sun JDK,JDK 1.3开始成为Sun JDK默认实现,JDK 1.4成为唯一的虚拟机
- 2006年底开始开源,由此建立的OpenJDK项目
- Oracle JRockit VM
- IBM J9 VM
1.1 Java虚拟机架构
- Class Loader
- 类加载器
- Runtime Data Area
- 运行时数据区
- Execution Engine
- 执行引擎
- Native Interface
- 本地接口
二、类加载器(Class Loader)
2.1 类加载过程
class文件加载到虚拟机的内存,这个过程称为类加载,如下:
- 加载
- 类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象
- 验证
- 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。
- 主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证
- 准备
- 为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i = 5;这里只是将i初始化为0,至于5的值将在初始化时赋值),这里不包含final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
- 解析
- 主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以时任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
- 初始化
- 类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值)
2.2 双亲委托机制
如果一个类加载器收到类加载的请求,它首先不会自己区尝试加载这个,而是把这个请求委派给父类加载器区完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载
2.3 应用(加载属性文件)
public class Demo2 {
public static void main(String[] args) throws Exception{
Properties properties=new Properties();
//FileInputStream fis=new FileInputStream("user.properties");
//使用类加载器加载 编译后目的配置文件
//(1)Demo2.class.getClassLoader() 默认目录 day27
InputStream is=Demo2.class.getClassLoader().getResourceAsStream("user.properties");
//(2)Demo2.class.getResourceAsStream()// 默认当前类的位置
InputStream is1=Demo2.class.getResourceAsStream("/user.properties","utf-8");
properties.load(is);
is.close();
properties.list(System.out);
}
}
三、运行时数据区(Runtime Data Area)
3.1 运行时数据区划分
- 线程私有
- 程序计数器
- Java虚拟机栈
- 本地方法栈
- 线程共享
- Java堆
- 方法区
3.1.1 程序计数器(私有)
也称PC寄存器(Program Counter Register)
- 一块较小的内存空间,它的作用可以看作时当前线程所执行的字节码的行号指示器
- 如果线程正在执行的是一个Java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址
- 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
3.1.2 虚拟机栈(私有)
特征
- 线程私有
- 后进先出(LIFO)栈
- 存储栈帧,支撑Java方法的调用,执行和退出
- 可能出现OutOfMemoryError错误和StackOverflowError错误
3.1.2.1 栈帧
每一个线程的启动,在内存中创建一个对应的JVM栈,用于存储“栈帧”
执行机制
- 一个方法对应一个栈帧结构
- 一个方法从调用起到执行完毕的过程,就对应一个栈帧在JVM中从入栈到出栈的过程
栈帧的概念和特征
- Java虚拟机栈中存储的内容,它被用于存储数据和执行过程结果的数据结果,同时也被用来处理动态连接,方法返回地址和异常完成信息
- 一个完整的栈帧包含:局部变量表,操作数栈,动态连接,方法返回地址等附加信息
3.1.2.1.1 局部变量表
概念和特征
- 由若干个Slot(槽)组成,长度由编译器决定,Code属性的locals决定
- 单个Slot可以存储一个类型为boolean,byte,char,short,int,float,reference和returnAddress(已过时)的数据,两个Slot可以存储一个类型为long或double的数据
- 局部变量表用于方法间参数传递,以及方法执行过程中存储基本数据类型的值和对象的引用
class Operation{
public int add(){
int a=100;
int b=200;
int c=a+b;
System.out.println(c);
return c;
}
}
对应局部变量表信息
3.1.2.1.2 操作数栈
概念和特征
- 是一个后进先出栈,由若干个Entry组成,长度由编译器决定,由Code属性的stacks指定
- 单个Entry即可以存储一个Java虚拟机中定义的任意数据类型的值,包括long和double类型,但是存储long和double类型的Entry深度为2,其他类型的深度为1
- 在方法执行过程中,操作数栈用于存储计算参数和计算结果;在方法调用时,操作数栈也用来准备调用方法的参数以及接收方法返回结果
同上代码的操作数栈(Operation)
- bipush:将单字节的常量值(-128~127)推送至栈顶
- sipush:将一个短整型常量值(-32768~32767)推送至栈顶
3.1.2.1.3 动态连接
-
每个栈帧内部都包含一个执行运动时常量池中该栈帧所属方法的引用
-
每一次方法调用时,动态的将符号引用转成直接引用(入口地址),支持多态
3.1.2.1.4 方法返回地址
方法结束有正常结束和异常结束
- 正常结束
- 当前栈帧承担着回复调用者状态的责任,其中包括恢复调用者的局部变量表和操作数栈、正确递增程序计数器,将返回值压入调用者的操作数栈
- 异常结束
- 如果当前方法中没有处理此种异常,当前栈帧恢复调用者状态的同时,不会返回任何值,而是通过athrow指令将当前异常抛给调用者
3.1.3 本地方法栈(私有)
特征
- 线程私有
- 后进先出(LIFO)栈
- 作用是支撑Native方法的调用,执行和退出
- 可能出现OutOfMemoryError错误和StackOverflowError错误
- HotSpot虚拟机将Java虚拟机和本地方法栈合并实现
补充
HotSpot将虚拟机栈和本地方法栈合二为一
Java虚拟机栈和本地方法栈可能发生如下异常情况:
- 如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量时,Java虚拟机将会抛出一个StackOverflowError错误
- 如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存区创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常
- 调整栈空间大小:-Xsss1m
- 在JDK1.5之前栈默认大小为256k,JDK1.5之后为1m
3.1.4 Java堆(共享)
由JVM自动管理的线程共享区域,在JVM启用时创建
堆空间大小:默认为总内存大小的1/64
堆可以处于逻辑上连续,物理上不连续的空间当中。既可以实现固定大小的,也可以实现为可扩展的,当前主流虚拟机实现都是可扩展的,可通过相关的参数配置
Java堆相关参数
- -Xms:初始堆大小
- -Xms:最大堆大小
- -XX:+PrintGC:打印产看GC日志
- -XX:+PrintGCDetails:打印查看GC日志详情
- -XX:+PrintCommandLineFlags:打印虚拟机的参数
特征
- 全局共享
- 通常是Java虚拟机中最大的一块内存区域
- 作用是做为Java对象或数组的主要存储区域
- JVM明确要求该区域需要实现自动内存管理,即常说的GC,但并不限制采用哪种算法和技术去实现
- 可能出现OutOfMemoryError错误
HotSpot
- HotSpot虚拟机采用分代存储(Java内存模型JMM)的方式
- 分代存储:因对象的声明周期不同,HotSpot虚拟机将堆中对象按照年龄进行划分,分别存入"新生代"和"老年代"
- 相关参数
- -Xmn:新生代大小
- -XX:NewRatio=?:表示年轻代和老年代的比例,默认1:2
- -XX:SurvivorRadtio=?:Eden和Survivor的比例,默认8:1:1,但实际调整6:1:1
- 对象分配策略
- 优先分配Eden区
- 绝大多数对象都是“朝生夕死”的对象,Eden区的回收时间短,效率高,适用于频繁回收
- 大对象直接进入老年代
- Enden和Survivor的空间不足时,大对象直接进入老年代。
- -XX:PretenureSizeThreshold=KB,对象大小超过此值,直接进入老年代
- 长期存活对象进入老年代
- 在Survivor区存活N岁(来回复制N此)的对象,将直接进入老年代。
- -XX:MaxTenuringThreshold=15(只有单线程收集器可用)
- 优先分配Eden区
Java对象
- 对象
- 对象头
- Mark Word(8),Klass Pointer(4),[数组长度](4) 【数组对象】
- 实例数据
- 对象属性信息
- 对齐填充
- 必须是8的倍数,所以Object对象16个字节
- 对象头
Java堆内存异常
如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那Java虚拟机将会抛出一个OutOfMemoryError
3.1.5 方法区和运行时常量池(共享)
方法区的特征
- 全局共享
- 作用是存储Java类的结构信息,常量,静态变量,即时编译器编译后的代码
- JVM不要求该区域实现自动内存管理,但是商用Java虚拟机都能够自动管理该区域的内存
- 可能出现OutOfMemoryError
运行时常量池
- 全局共享
- 是方法区的一部分
- 作用是存储Java类文件常量池中的符号信息
- 可能出现OutOfMemoryError
HopStop方法区实现的变迁
- 永久代与方法区
- JDK1.2~JDK1.6,HotSpot使用永久代(PermGen)实现方法区
- JDK7开始,HotSpot开始了移除永久代计划
- 符号表被移到Native Heap中
- 字符串常量和类的静态变量被移到Java Heap中
- 在JDK8开始,永久代已被原空间(Metaspace)所代替
- 字符串常量和类的静态变量仍然在堆中
3.1.6 直接内存
- 直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分也被频繁地使用
- 应用在某场景中能显著提高性能,因为其避免了在Java堆和Native堆中来回复制数据
- 在JDK1.4的NIO中已经出现直接内存的使用
- 可能出现OutOfMemoryError
3.1.7 其他空间
在HotSpot虚拟机中还有其他一些空间
- TLAB:TLAB的全程是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域
- CodeCache:Code Cache用于存储JVM JIT 产生的编译代码
- Compressed Class Space(JDK1.8以后):类指针压缩空间,目的为了节省内存
七、逃逸分析
Java HotSopt虚拟机可以分析新创建对象的使用范围,并决定是否在Java堆上分配内存的一项技术
逃逸分析的JVM参数
- 开启逃逸分析:-XX:+DoEscapeAnalysis
- 关闭逃逸分析:-XX:-DoEscapeAnalysis
- 显示分析结果:-XX:+PrintEscapeAnalysis
逃逸分析技术在Java SE 6u23+开始支持,并默认设置为启用状态,可以不用额外加这个参数
对象逃逸
- 当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数或返回值传递到其他地方中,称为对象逃逸。反之没有逃逸
使用逃逸分析,编译器可以对代码做如下优化:
-
锁消除
- 线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁
- 锁消除的JVM参数
- 开启锁消除:-XX:+EliminateLocks
- 关闭锁消除:-XX:EliminateLocks
- 锁消除在JDK8中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上
//演示锁消除 -XX:-EliminateLocks public class Demo7 { public static void main(String[] args) { long start=System.currentTimeMillis(); //438 550 329 310 420 开启锁消除 //556 559 613 698 860 禁用锁消除 for(int i=0;i<10000000;i++) { append(); } long end=System.currentTimeMillis(); System.out.println("用时:"+(end-start)); } public static void append(){ StringBuffer sb=new StringBuffer(); sb.append("hello"); } }
-
标量替换
- 标量:不能被进一步分解的数据,基本类型数据和对象的引用可以理解为标量
- 聚合量:可以被进一步分解成标量,比如对象
- 标量替换:将对象成员变量分解为分散的标量,这就叫做标量替换
- 标量替换的参数
- 开启标量替换:-XX:+EliminateAllocations
- 关闭标量替换:-XX:-EliminateAllocations
- 显示标量替换详情:-XX:+PrintEliminateAllocations
- 标量替换同样在JDK8中都是默认开启的,并且都要建立在逃逸分析的基础上
如果一个对象没有发生逃逸,那压根就不用创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能
-
栈上分配
- 当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的声明周期一致,随着栈帧出栈时销毁,减少了GC压力,提高了应用程序性能
//演示变标量替换 -XX:-EliminateAllocations -XX:+PrintGC public class Demo8 { public static void main(String[] args) throws Exception{ for(int i=0;i<10000000;i++){ createPerson(); } System.in.read(); } public static void createPerson(){ Student s=new Student(); s.setName("测试"); s.setAge(20); } }