JVM笔记
引言:
用户->字节码文件->JVM->操作系统->硬件
计算机不识别高级语言 高级语言->汇编语言->机器指令->CPU
书籍:java虚拟机规范(不利于学习,可以查阅),深入理解java虚拟机(推荐)-周志明老师
一、JVM和Java的体系结构
1、Java和JVM
Java:跨平台的语言
JVM:跨语言的平台 不同的语言编译成字节码之后只要符合java虚拟机的规范,就能在java虚拟机上运行
java虚拟机只关注字节码文件,不关注是什么语言写的程序
Java不是最强大的语言,但是jvm是最强大的虚拟机
多语言混合编程成为主流:各种语言之间的交互不存在任何困难,因为他们最终都运行在一个虚拟机上
虚拟机(VM)是一个软件 分为系统虚拟机和程序虚拟机
系统虚拟机:对物理计算机的仿真,可以运行一个完整的操作系统
程序虚拟机:执行单个计算机程序而设计的,例如JVM
两种虚拟机上运行的软件都被限制于虚拟机提供的资源中
Java虚拟机:是一台执行Java字节码的虚拟计算机
作用:Java虚拟机就是二进制字节码的运行环境
特点:一次编译,到处运行 自动内存管理 自动垃圾回收
2、JVM的结构
方法区和堆是线程共享的 虚拟机栈,本地方法栈,程序计数器是线程私有的
class文件->通过类装载器子系统->进入运行时数据区->执行引擎(包括编译器(后端)和垃圾回收器) 运行时数据区:方法区,堆,虚拟机栈,本地方法栈,程序计数器 。
执行引擎把高级语言编译成机器语言,包括翻译字节码和JIT即时编译器,把重用的热点的字节码放在方法区的缓存区。
java代码的执行过程:java程序通过前端编译器编译成字节码文件通过JVM在操作系统上执行
3、JVM的架构模型
java编译器输入的指令流基本上是基于栈的指令集架构和基于寄存器的指令集架构
两种架构的区别:
基于栈式架构的特点:
①、设计实现简单,适用于资源受限的系统
②、避开了寄存器的分配难题,使用零地址指令方式分配
③、指令流中的指令发部分是零地址指令,其执行依赖栈。指令集更小,编译器易实现
④、 不需要硬件支持,可移植性好,更好实现跨平台
基于寄存器机构的特点:
①、依赖硬件,可移植性差
②、性能优秀和执行高效
③、花费更少的指令去完成一项操作
由于跨平台的设计,Java的指令都是根据栈来设计的。优点是跨平台,指令集小,缺点是性能下降,实现同样的功能需要更多的指令。
栈:跨平台性、指令集小,指令多;执行性能比寄存器差。
寄存器:和机器耦合,可移植性差
4、JVM的生命周期
虚拟机的启动:虚拟机的启动是通过引导类加载器创建一个初始类来完成的。
虚拟机的执行:程序开始执行时他才运行,程序结束时他就停止
执行一个所谓的java程序的时候,真正在执行的时一个叫做java虚拟机的进程
虚拟机的退出:
①、程序正常结束
②、程序出现异常活着错误异常终止
③、操作系统的错误导致java虚拟机进程终止
④、线程调用Runtime类halt方法的或者System类的exit方法
补:Runtime类是单例的,因为一个java程序对应一个虚拟机,一个虚拟机进程包括一个运行时数据区(Runtime Data Area)
5、JVM的发展历程
①、Sun Classic VM 世界上第一款商用Java虚拟机,这款虚拟机只提供解释器,没有JIT及时编译器,性能较差。
②、HotSport 最初由一个小公司设计,97年被Sun公司收购,09年被甲骨文公司收购,目前称霸市场,再服务器,移动端,嵌入式都有应用
③、BEA 的 Jrockit
专注于服务器端的应用,内部不包含解析器实现,全部代码都靠即时编译器编译后执行,是世界上最快的JVM
④、IBM 的 J9
接近于HotSpot,应用场景多,在IBM自己的产品上应用性能极佳,但是在其他应用上不太好。目前是最有影响力的商用虚拟机之一。
⑤、TaobaoJVM
基于OpenJDK开发了自己的定制版本AlibabaJDK
基于OpenJDK Hotpot VM 发布的国内第一个优化、深度定制且开源的高性能服务器版Java虚拟机
将生命周期较长的java对象从heap中移到heap之外,并且GC不能管理GCIH内部的java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。
GCIH中的对象还能够在多个JAVA虚拟机进程中实现共享.
⑥、Graal VM
跨语言的全栈虚拟机,可以作为“任何语言”的运行平台使用
如果有一天HotSpot被取代了,Graal VM希望最大。
二、类加载子系统
1.类加载器和类加载的过程
类加载子系统负责加载Class文件,类加载的过程分为三个阶段:加载阶段,链接阶段,初始化阶段
ClassLoader只负责class文件的加载,至于是否可以运行,则由执行引擎(Exexution Engine)决定。加载类信息存放在方法区,方法区还有运行时常量池信息。
加载阶段:
①、通过一个类的全限类名获取定义此类的二进制字节流
②、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
③、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
链接阶段:
①、验证:确保class文件中包含的信息符合当前虚拟机的要求,保证被加载类的安全性
②、准备:为变量分配内存并且设置该变量的默认初始值,即零值(先给属性值赋0值 例 int a=3先设值 int a =0)
这里不包含final修饰的static,因为final在编译的时候就会分配了(例 final static int a=5 final一般和static一起使用)
这里不会给实例变量赋值,类变量会分配在方法区中,而实例变量会随着对象一起分配到java堆中
③、解析:将常量池内的符号引用转换为直接引用的过程
补:类变量和实例变量的区别:类变量是static修饰的变量,是全类共有的变量,实例变量是没有被static修饰的变量,是本类的对象私有的。
初始化阶段:
初始化过程是执行类的构造方法<clinit>()的过程
此过程不需要自己定义是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来的
public class ClassInitTest {
static int num = 1;
static {
num = 2;
number = 5;
//System.out.println(number); //报错 因为number是在外面声明的
}
private static int number = 3;
public static void main(String[] args) {
num = 7;
System.out.println(num);
System.out.println(number);
}
}
构造方法中的指令按照语句在源文件中出现的顺序执行
<clinit>()不同于类的构造器,(关联:构造器是虚拟机视角下的<init>())
若该类有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕
虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁,只加载一次
补:类的构造器就是类的构造方法,创建类的对象时调用。
2.类加载器的分类
JVM支持两种类加载器:引导类加载器(Bootstrap ClassLoader)、自定义类加载器(User-Defined ClassLoader)(将所有派生于抽象类(ClassLoader)的类都划分为自定义类加载器)
前者是非java语言编写的,后者都是java语言编写的
java的核心类库都是使用引导类加载器加载的
①、虚拟机自带的类加载器:启动类加载器(引导类加载器,Bootstrap ClassLoader)
这个类加载器是使用C/C++实现的,嵌套在JVM的内部
它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar\resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
没有父类加载器
加载扩展类加载器和应用程序类加载器,并指定为他们的父类加载器
出于安全考虑,Bootstrap启动类加载器只加载包名为java,javax,sun等开头的类
②、扩展类加载器
java语言编写,由sun.misc,Launcher$ExtClassLoader实现
派生于ClassLoader类
父加载器为启动类加载器
③、应用程序类加载器(系统类加载器,AppClassLoader)
java语言编写,由sun.misc.Launcher$AppClassLoader实现
派生于ClassLoader类
父类加载器为扩展类加载器
它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
该类加载是程序中默认的类加载器,一般来说,java应用 的类都是由它来完成加载
通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
3、用户自定义类加载器:
为什么要自定义类加载器:
隔离加载类:由于中间中间件都有自己的jar包,有可能会出现类的冲突,通过隔离加载类避免这种情况
修改类加载的方式:可以让类在你需要的时候加载
扩展加载源:XXX
防止源码泄漏:对字节码进行加密,使用自定义类加载器解密
用户自定义类加载器的实现步骤:
1、继承java.lang.ClassLoader类的方式
2、jdk1.2之前,覆盖父类中的loadClass()方法,jdk1.2之后把类加载逻辑写在findClass()方法中
3、如果没有特别复杂的需求,可以直接继承URLClassLoader类,这样避免自己去编写findClass()方法及其获取字节码流的方式。使自定义类加载器编写更加简洁
关于ClassLoader
是一个抽象类,其后的类加载器都是继承自此类(不包括自动类加载器)
方法名称:
getParent() 放回该类加载器的超类加载器
locadClass(String name )加载名字为name 的类,返回结果为java.lang.Class类的实例
findClass(String name) 查找名称为name的类,返回结果为java.lang.Class类的实例
findLoadedClass(String name) 查找名称为name的已被加载过的类,返回结果为java.lang.Class类的实例
defineClass(String name,byte[] b,int off,int len) 把字节数组b中的内容转换为一个Java类,返回结果为java.lang.Class类的实例
resolveClass(Class<?>c) 连接指定的一个java类
获取ClassLoader的途径:
方式一:获取当前类的ClassLoader
clazz.getClassLoader()
方式二:获取当前线程上下文的ClassLoader
Thread.cuurentThread().getContextClassLoader()
方式三: 获取系统类加载器
ClassLoader.getSyatemClassLoader()
方式四:获取调用者的ClassLoader()
DriverManager.getCallerClassLoader()
public static void main(String[] args) throws ClassNotFoundException {
//1.获取当前(String)类的类加载器 null
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println("classLoader = " + classLoader);
//2.获取当前线程上下文的ClassLoader sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
System.out.println("contextClassLoader = " + contextClassLoader);
//3.获取系统类加载器 sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
//获取扩展类加载器 sun.misc.Launcher$ExtClassLoader@1b6d3586
ClassLoader parent = systemClassLoader.getParent();
System.out.println("systemClassLoader = " + systemClassLoader);
System.out.println("parent = " + parent);
}
4、双亲委派机制
Java对class文件采用的使按需加载的方式,当我们需要这个类的信息的时候才会的到它的class文件加载到内存中,加载class文件时,java虚拟机采用的是双亲委派模式,即把请求交给父类处理,他是一种任务委派模式。
工作原理
当一个类加载器收到了类加载的请求时,这个类加载器不会直接处理,而是交给自己的父类去处理,父类如果还有父类,那就交给爷爷类处理,如果爷爷类处理不了,再重新给父类处理,父类处理不了,这个类加载器再加载。
例:
双亲委派机制的优势
1、避免类的重复加载
2、保护程序安全,防止核心API被随意篡改
例:自定义类 java.lang.String、java,lang.Lqy
沙箱安全机制:
当我们自定义一个java.lang.String类时,运行其中的main方法,会报找不到main方法,原因是启动类加载器加载的是rt.jar中的String 类,这样可以保证java的核心源码
5、类的主动加载和被动加载
判断JVM中的两个class对象是否是同一个类的条件:
1、类的权限类名必须一致
2、加载本类的类加载器必须相同
所以,即时两个类对象来自同一个class文件,被同一个虚拟机所加载,但是只要加载他们的ClassLoader实例对象不同,那么这两个类对象也是不相同的
如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分存放在方法区中
Java虚拟机对类的使用方式分为:主动使用和被动使用
主动使用的七种情况:
1、创建类的实例
2、访问某个类或接口的静态变量,或者对该静态变量赋值
3、调用类的静态方法
4、反射
5、初始化一个类的子类
6、Java虚拟机启动时被标记为启动类的类
7、JDK7开始提供的动态语言支持:
java.lang.invoke.MethodHandle实例的解析结果
REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
除以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化