①Java是编译型+解释型语言(class通过JVM翻译在对应平台运行;翻译过程会有编译,JIT)
一次性的编译成可被该平台硬件执行的机器码,将包装成该平台所能识别的可执行性程序的格式。【C、C++、GoLang,一次性翻译整本书】编译型语言:
执行速度快、效率高;一考编译器、跨平台性差
把做好的源程序全部编译成二进制代码的可运行程序。然后直接运行这个程序。
解释型语言:
执行速度慢、效率低;依靠解释器、跨平台性好。
把做好的源程序翻译一句,然后执行一句,直至结束。
Java:编译型+解释性
并不是因为有javac将Java源码编译成class文件,才说Java属性编译+解释语言,因为这个编译
器编译之后,生成的类文件不能直接在对应的平台上运行。
那么为什么说Java是编译+解释型语言呢?因为class文件最终是通过JVM来翻译才能在对应的平台
上运行,而这个翻译大多数时候是解释的过程,但是也会有编译,称之为运行时编译,即JIT(Just
in Time)。
综上,Java是编译型+解释型的高级语言。
所谓类加载机制就是:
虚拟机把Class文件加载到内存,并对数据进行校验,转换解析和初始化,形成可以直接使用的Java类型,即:java.lang.Class

查找和导入class文件
Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。在Java堆中生成一个代表这个类的java.lang.Class对象,作为堆方法区中这些数据的访问入口


保证被加载类的正确性
为类的静态变量分配内存,并将其初始化为默认值
把类中的符号引用转换为直接引用【符号标识变为指针,a -> 直接指向某个目标的指针】
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。(a -> 指向某个确定的地址)
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号
引用进行
对类的静态变量,静态代码块执行初始化操作
用于装载Class文件的,在装载(Load)阶段,其中第一步:需要通过类的全限定名来获取其定义的二进制字节流,需要借助类加载器完成。

加载$JAVA_HOME中jre/lib/rt.jar里面的所有class,由C++实现,不是ClassLoader子类
加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中的jre/lib/*.jar
负责加载classpath中指定的jar包
通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的
ClassLoader,如:tomcat、jboss都会根据j2ee规范自行实现ClassLoader
(1)检查某个类是否已经加载
自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个ClassLoader已加载,就视为已加载此类,保证此类在所有ClassLoader中只加载一次
(2)加载的顺序
自顶向上:由上层逐层尝试加载此类
一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。【因此,tomcat需要打破双亲委派】
装载阶段的
第(2)步:将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
第(3)步:在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区中这些数据的访问入口,说白了就是类文件被类加载器加载进来之后,类中的内容(比如变量、常量、方法、对象等这些数据得要有个去处,也就是要存储起来,存储的位置肯定是在JVM中有对应的空间)

(1)方法区是各个线程共享的内存区域,在虚拟机启动时创建
(2)虽然Java虚拟机把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来
(3)用于存储被虚拟机加载的类信息、静态变量、常量、即时编译器编译后的代码等数据
(4)当方法区无法满足内存分配需求时,将抛出OutOfMemory异常
此时回看装载阶段的第2步:将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
如果此时把从Class文件到装载的第1、2步合并起来的话,可以如下图所示
注意:
JVM的方法区是一种规范,具体的实现如下:
JDK7:Perm Space,永久代
JDK8:MetaSpace, 元空间
(1)Java堆是Java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建,被所有线程共享
(2)Java对象实例以及数组就是在堆上分配
此时回看装载阶段的第3步:在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区中这些数据的访问入口
此时装载步骤(1)(2)(3)如下图:
经过上面的分析,类加载机制的装载过程已经完成,后续的链接,初始化也会对应生效。
那么后续就到了使用阶段,那么怎么才能被使用到呢?
(1)虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。换句话说,一个Java线程的运行状态,由一个虚拟机栈来保存,因此虚拟机栈肯定是线程私有的,随着线程的创建而创建。
(2)每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。调用一个方法,就会向虚拟机栈中压入一个栈帧,一个方法调用完成,就会把该栈帧从栈中弹出来。
void a(){
b();
}
void b(){
c();
}
void c(){
}

栈帧: 每个栈帧对应一个被调用的方法,可以理解为一个方法的运行空间
每个栈帧中包括:局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用、方法返回地址和附加信息。
局部变量表:方法中定义的局部变量以及方法的参数都存放在这张表中(int a = 1)
局部变量表中的变量不可以直接使用,如果需要使用的话,必须通过相关指令将其加载至操作数栈
中作为操作数使用。
操作数栈:以压栈和出栈的方式存储操作数的
动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了
支持方法调用过程中的动态链接(Dynamic Linking)
方法返回地址:当一个方法开始执行之后,只有两种方式可以退出:
1. 遇到方法返回的字节码指令
2. 遇见异常,并且该异常没有在方法体内得到处理
栈帧结构:

线程、程序计数器、虚拟机栈关系:

一个JVM进程中有多个线程在执行,而线程中的内容是否能够拥有执行权,是根据CPU调度来的
假如线程A正在执行到某个地方,突然失去了CPU的执行权,切换到线程B了,然后当线程A再获得CPU执行权的时,怎么能回到原来的位置继续执行呢?这个时候就需要在线程中维护一个变量,记录线程执行到的诶之。
如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是Native方法,则这个计数器为空。
如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行。
那如果在Java方法执行的时候调用的是Native(C、C++写的)的方法呢?
通过虚拟机栈的栈帧中的动态链接,直接链接到本地方法栈中

如果栈帧中有一个变量,类型为引用类型,比如Object obj = new Object(),这个时候就是典型的栈指向堆

方法区中会存放静态变量、常量等数据。
private static Object obj = new Object();

指向的是堆中的类信息
方法区中会包含类的信息,堆中会有对象,那怎么知道对象是哪个类创建的呢?
-- 因此需要堆指向方法区

由上面可知,我们需要堆指向方法区,来确定对象是哪个类来创建的。那么一个对象怎么知道它是由哪个类创建出来的呢?怎么记录?这个时候我们就需要了解Java对象的具体信息
Java对象内存模型:
一个Java对象在内存中包括3个部分:对象头、实例数据、对齐填充
对象头:
实例数据:包含对象所有的成员变量,基本数据类型(1、2、4、8字节)+引用数据类型(8字节)
对齐填充:为了保证对象的大小是8字节的整数倍