1. JDK JRE JVM三者的区别
-
JDK(Java Development Kit):Java开发工具包
JDK包含JRE,还包括其他例如:编译器(javac)、javadoc、jar等,JDK是能够创建和编译程序的。
-
JRE(Java runtime environment):Java运行环境
JRE是运行已编译Java程序所需要的所有内容的集合,包括JVM,java类库,java命令和其他基础构建,JRE不能够创建和编译程序。
-
JVM(Java Virtual Machine):Java虚拟机
JVM是运行java字节码1的虚拟机,JVM有针对不同系统的特定实现(这是因为不同操作系统二进制所代表的不完全相同),目的是使用相同的字节码,在不同的操作系统上都能给出相同的结果,如下图所示
JDK、JRE、JVM三者之间的关系
Java文件在三者之间的流转
2. Java的运行机制(宏观)
-
编译:JDK调用
javac
工具将.java
文件编译成.class
文件
如果在编译时该类所依赖的类还没有被编译则会自动编译所依赖的类并引用
如果找不到所依赖的类则会报Can't found sysbol
的异常 -
运行(启动JVM进程):
类加载的生命周期:加载-->验证-->准备-->解析(验证、准备、解析称为类的连接)-->初始化-->使用-->卸载
-
加载:读取class文件到内存的过程
- 加载
.class
文件 - 将class文件的信息存储到方法区
- 生成一个引用对象(java.lang.Class)
- 加载
-
检验合格性:分为四个阶段
格式验证:在加载阶段检查字节流是否符合class文件规范
.class文件是否以魔数(Magic Number)0XCAFEBABE
开头
小版本号Minor version
和大版本号Major version
常量池中常量是否有不支持类型
Constant_Utf8_info型的常量中是否有不符合UTF-8编码的数据
class文件中各个部分及文件本身是否有被删除的或附加的其他信息
……
元数据验证:进行字节码语义分析
检查这个类是否有父类(除了java.lang.Object之外,所有的类都应该有父类)
这个类的父类是否继承了不允许被继承的类(final修饰的类)
如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
类中的字段、方法是否与父类产生了矛盾(例如覆盖了父类的final字段,或者不符合规则的方法重载)
……
字节码验证:通过数据流分析和控制流分析,确定程序语法是否合法,是否符合逻辑,是最复杂的验证阶段
保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于在操作栈上放置一个int类型的数据,使用时却是按long类型来载入本地的变量表中
这样的情况
保证任何跳转指令都不会跳转到方法体以外的字节码指令上
保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把一个父类对象赋值给子类数据类型,甚至把对象赋值给与它毫不相关的数据类型,这是危险的,不合法的
……
符号引用验证:
符号引用中通过字符串描述的全限定名是否能找到对应的类
在指定的类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
符号引用中的类、字段、方法的可访问性—检查权限,验证可被当前类访问
……
由JVM校验.class文件是否符合规范,为了防止代码危害虚拟机本身以及底层系统 -
准备:类变量分配内存,在分配内存的过程中,类变量就有了默认值
为类变量分配内存并设置类变量初始值(默认值,不是赋值),这些类变量所使用的内存全都在方法区进行分配(不包括实例变量:实例变量会在对象实例化随着对象分配到Java堆中) -
解析:将class常量池内的符号引用替换为直接引用的过程
符号引用:就是字符串,通过字符串中的信息就能直接找到对应的类的数据(只需要无分歧对应数据位置即可)
直接引用:将符号引用转化为直接引用是因为直接引用是JVM虚拟机可以直接使用的,是虚拟机可读的地址信息,是直接指向目标的指针,不同的JVM虚拟机解析出来的直接地址是不同的,一旦出现直接地址就意味着该目标已经存在于内存中
如果符合规范就通过类加载器加载到JVM中运行得出结果 -
初始化:按照程序代码进行初始化,对数据进行赋值
类的执行
JVM主要是在程序第一次运行时,不得不使用类的时候才会立即加载并生成一个java.lang.Class对象,并存在方法区,且只加载一次,这就是为什么实例变量和静态方法只执行一次的原因,执行代码的过程是JVM解释.class文件给操作系统的过程
注意:.class文件并不能直接与操作系统进行交互,而是通过JVM这个中间层间接与操作系统进行交互
仅有JVM是不能解释.class文件的,必须是jvm通过调用一个lib类库,而jre就包含这个lib类库
-
Java的main方法为什么这么写
-
public:设置访问权限,因为main方法被作为一个主方法,需要且必须被直接访问
-
static:在JVM中没有实例化main所在的类,而是直接通过类名.main()的方式进行调用的,所以如果不写staticJVM就无法访问到main方法
-
void:main方法不能有返回值,因为,jvm就算接收到返回值,它返回给谁?所以main中不能存在返回值,包括所有直接对接到JVM层的方法都不能有返回值
-
String[] args:是用于在main方法执行前接收命令行参数的,作用主要是开发过程无数据情况下以命令行数据进行测试,作用效果不大,但是是规定
JVM在将类字节码读到内存中后,会找到加载的类中的主类,然后在主类中找到main方法之后,将main线程压栈到线程栈中执行main方法
new对象的底层存储
以Hello hello=new Hello()为例
-
对象声明:Hello hello
在栈中开辟一个地址空间,用于存放声明的对象 -
创建对象:new Hello()
1)申请堆内存空间,将Hello类的相关信息(实例变量,实例方法等)加载到堆内存中
2)执行构造方法就是<init>
方法 -
使声明的对象与堆内存中的信息产生关联:
即将堆内存中的地址赋值给栈中声明的对象,由声明的对象携带这个地址指向堆内存对象信息
堆内存中开辟对象的结构是什么?
- 头部信息
1)偏移值(对齐填充)
2)持有指向方法区的指针
3)描述信息(持有当前对象锁的线程id和持有对象锁线程的个数,在GC中存活的生命周期,偏向锁2的标志) - 实例信息
实例信息为对象的属性和行为,后续会进行详细的解释,目前可以理解为:你定义的类的信息会加载到堆内存中的实例信息部分
JVM能理解的代码就叫做字节码,文件以.class结尾,不面向任何特定的处理器,只面向JVM虚拟机通过字节码的方式,Java语言成功解决了传统解释型语言执行效率低的问题,同时保留了解释型语言可移植的特性虽然执行效率低于C/C++等语言,但是在切换操作系统的情况下,字节码文件无需重新编译,即可运行。 ↩︎
什么是偏向锁:当线程已经对此对象加锁后,执行完毕,如果下一次访问该线程也是上一次的线程,那么不对此线程重新上锁 ↩︎