目录
一、JVM执行流程
JVM虚拟机主要是将javac编译的字节码文件(.class文件),转换成系统能看懂的可执行指令。系统不同,转换的可执行指令也不同。
将字节码文件转换为可执行指令的过程(加载--解析):
1、通过类加载器,将字节码文件加载到内存中运行时数据区;
2、调用其他语言的接口(本地库接口),使用特定的命令解析器(执行引擎),将字节码文件翻译成底层系统指令。
二、类加载过程
1、类加载的结果
类加载器是将javac生成的.class文件从硬盘读入到JVM内部,转换为一个与目标类对应的java.lang.Class(Class)对象实例。
2、类加载的5个过程
(1)加载
根据类的全限定名,找到硬盘上的.class文件,打开文件,读取到文件内容(字节码数据),并创建对应的Class对象。
(2)验证
确保被加载的类符合JVM规范,例如:类文件格式、语义等。
(3)准备
为类的定义变量(类的静态变量--static修饰)分配内存空间,并设置默认初始值。确保静态变量在使用之前都有一个确定的初始值,避免出现未初始化的情况。
eg:此时如果有一行代码为:public static int a=123;
准备过程中初始化value的int值为0,而非123(初始化阶段赋值)。
(4)解析
将类中的符号引用转换为直接引用。
eg:此时如果有一行代码为:public String s="hello";
.class文件中就会包含s这个变量,那么s变量存储的是地址吗???当然不是,地址是指在内存中的地址,而.class文件不存在"地址"这样的概念,此时为了描述s变量所指内容,就给s填充的内容是"hello"的偏移量,即符号引用。后期将.class文件加载到jvm内存中后,就有了地址,此时就可以把s的值替换为"hello"的地址了。
(5)初始化
负责对类进行初始化操作,包括执行类的静态变量赋值和静态代码块等初始化代码。
3、双亲委派模型
(1)概念
描述了查找.class文件的过程。
(2)jvm的3个类加载器
jvm默认是有3个类加载器的。
①BootstrapClassLoader---引导类加载器
虚拟机的一部分,负责加载JDK中的标准库;
②ExtensionClassLoader---扩展类加载器
负责加载java的扩展类库;
③ApplicationClassLoader---应用程序类加载器
负责查找当前项目法代码目录以及第三方库的目录。
(3)双亲委派模型的过程
①从应用程序类加载器作为入口,开始加载;
②应用程序类加载器不会立即加载自己库里的,而是进入到扩展类加载器;
③扩展类加载器也不会立即加载自己库里的,而是进入到引导类加载器;
④进入到引导类加载器后,通过全限定类名,尝试在标准库中找到符合要求的.class文件,如果找到就开始进行文件的打开、读、加载;如果未找到就回到扩展类加载器。
⑤扩展类加载器收到父亲未找到.class文件的反馈后,此时该类加载器就开始在扩展类库中进行查找,如果找到就开始进行文件的打开、读、加载;如果未找到就回到应用程序类加载器。
⑤应用程序类加载器收到父亲未找到.class文件的反馈后,此时该类加载器就开始在项目目录/第三方库目录中进行查找。
三、运行时数据区
JVM运行时数据区也叫内存布局。该布局由5部分组成:
1、堆---线程共享
代码中new出来的对象保存在堆中,对象中的非静态成员变量也保存在堆中。
堆中有两个区域:新生代和老生代,新生代放新建的对象,当经过一定次数之后还存活的对象会放入老生代。
2、虚拟机栈---线程私有
java虚拟栈和线程周期是相同的,描述了java方法执行的内存模型,每个方法在执行时都会在虚拟机栈中创建一个栈帧,在该栈帧中存储局部变量表、方法出口等信息。
线程私有:每条线程独立拥有的某个区域,各条线程之间互不影响,独立存储。
3、本地方法栈---线程私有
本地方法和虚拟机栈类似,虚拟机栈是给JVM使用,而本地方法栈是给本地方法使用的。
4、程序计数器---线程私有
程序计数器记录了当前线程所执行的字节码的行号指示器。
5、元数据区(方法区)---线程共享
用来存储被虚拟机加载的类信息、常量、静态变量、方法信息等数据。
ps:经典笔试题
class Test{
private int n;
private static int m;
}
main(){
Test t=new Test();
}
上述n、m、t都存储在jvm那个内存区域?
①t:方法中的局部变量,在栈上;
②n:对象中的非静态变量,在堆上;
③m:类中的静态变量,在方法区上。
四、垃圾回收机制
在以上内存区域中,虚拟机栈、本地方法栈、程序计数器三个区域是线程私有的,当方法或线程运行结束时,也就自动回收了,而对于堆和方法区这两个区域是线程共享的,没有办法自动回收,需要采用一定垃圾回收机制进行回收。
1、找出需要被回收的对象(堆上)
(1)该对象要被回收的情况
例如:
void f(){
Test t=new Test();
t.start();
}
当以上代码执行到‘{’之后,此时t作为局部变量就被自动回收了,此时new Test()对象就没有引用指向他了,此时这个对象就是垃圾,要被回收了。
(2)找寻垃圾对象的方法
以上代码较简单,但对于一些比较复杂的代码,需要采用方法找出垃圾对象。
①引用计数
核心思想:为创建的对象单独开辟出一部分空间,用来记录当前被引用指向次数,有专门的扫描线程来计算当前所拥有的引用,释放一个引用就--,增加一个引用就++,当次数为0时,表明没有引用指向该对象,该对象就可以被标记为垃圾了。
缺点:需要为对象单独开辟出一部分空间,消耗额外的内存空间;可能会产生循环引用的问题,循环引用指两个或多个对象之间相互引用,导致他们的引用计数永远不会变为0,从而无法释放。
②可达性分析
核心思想:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,当一个对象到“GC Roots”没有任何的引用链相连时,表明此对象是垃圾对象。
GC Roots对象:可以是栈中的对象、方法区中的静态变量、方法区中的常量引用,使用这些引用来判断垃圾对象。
2、回收对象
(1)把标记为垃圾的对象进行释放
①标记-清除算法
核心思想:把标记为垃圾的对象直接释放掉。
缺点:直接清除会产生大量不连续的内存碎片,可能会导致在程序运行中需要分配内存较大的对象时,无法找到一段足够连续的内存从而不得不触发另一次垃圾收集。
②标记-复制算法
核心思想:不直接释放内存,把标记为垃圾的对象复制到内存的另一半,然后进行整体释放。
缺点:总的可用内存变少了,复制也会产生不必要的开销。
③标记-整理算法
核心思想:不直接释放内存,而是将可存活对象统一向一个方向移动,释放掉另一端边界对象。
缺点:搬运内存开销大。
(2)分代回收
核心思想:根据对象存活周期的不同将堆划分为新生代和老年代。在新生代中,每次垃圾回收都有大量对象成为垃圾,只有少量存活,采用标记--复制算法进行垃圾回收;在老年代中,对象存活率高,内存空间小,采用标记--清理或者标记--整理算法进行垃圾回收;
新生代(Minor GC):一般刚创建的对象会进入新生代;
老年代(Full GC):在新生代对象经历了一定次垃圾回收依然存活的对象会从新生代移动到老年代。