一、JVM类执行流转的流程
- 我们在开始学习Java语言的时候就知道的JVM可以一次编译多次运行。与操作系统底层不相关。那它时如何实现这样的特性的呢?其实很简单就是通过JVM呗。我们在他们的官网可以看到如下的图片:
提供了不同平台的JDK。而在JDK的底层又是通过JVM去屏蔽底层的操作系统的差异。 - 一个类需要运行。首先肯定需要将类的描述文件加载到JVM中。那在JVM谁来做这件事呢?就是类加载子系统实现类的检查、验证和加载。而类加载子系统中类加载器就在其中扮演着非常重要的角色。在Java中内置的类加载器有三种:启动类加载器、扩展类加载器、应用类加载器,其中启动类加载器是JVM进程在启动的时候就会在初始化,而后JVM通过调用Java中的Launcher类实现的扩展类加载器和应用类加载器的加载。其实本质上除了启动类加载器之外其余的两个也是由Java实现的。类加载器在加载需要加载的类的时候遵循一种加载模式双亲委派。那何为双亲委派呢?我们先来看看他们之间加载的关系图
这种加载模式可以简单的概括为:在接受的加载类的请求的时候,线委托给我父加载器加载,层层向上委托,当启动类加载器也没有找到对应的类的时候,在返回由子加载器尝试去加载,子加载器还是没有找到对应的类直接抛出异常。 - 前面类已经加载好了,那加载到何处呢?那肯定是JVM中。那是JVM中的那块区域呢?在类加载器解析阶段会将常量、静态变量、类的元信息加载到方法区也就是我们说的元空间。然后在运行的时候对象的创建可能是在堆区。为啥说可能呢?在JVM中有一个钟机制就是逃逸分析,经过一个JVM分析这个对象之后在一个方法内部使用,那就会使用标量替换的方式将组成对象的数据存储在栈中。到此数据已经全部加载到了内存。剩下就是执行代码了。
- 执行代码是由字节码执行引擎实现的。它执行代码的过程中通过改变的程序计数器来指向代码已经运行到哪儿。总的概括起来的如下图所示:
二、JVM运行时数据区详解
- 在上面展示的图中运行时数据区按照是否是线程私有的可以划分为两大部分
- 线程私有区域:栈、本地方法栈、程序计数器
- 栈我们都知道是一种特殊的数据结构,是一种后进先出的数据结构。在java中每一个线程都会分配一个线程栈。线程的栈的大小可以通过参数
-Xss
进行调整。默认值是1M。每一个方法运行都对应着一个栈帧的入栈和出栈的过程。当有多个方法运行的就是会一直不断的向栈中放入对应的栈帧。这也就是方法调用层次太深会出现栈内存溢出的问题的原因。在一个栈帧中包含如下的几个部分组成:局部变量表、操作数栈、动态链接、方法出口。那这几个部分都有啥作用呢? - 方法出口:存在方法A和方法B。当方法A在运行的中需要使用到方法B。那肯定会调用方法B。那调用完成之后如何去返回呢?就是通过方法出口返回发起调用的方法现场。
- 动态链接:有动态链接那与之相对的就会有静态链接。那静态链接是发生在那个解析呢?对于类的加载来说静态链接发生在类加载的初始化节点。在这个阶段会把我们字面量转换为实际需要运行的程序在内存的位置。如下的代码
public static void main(String[] args) {
// 实例上stackOverflow是执行这个对象在堆内存中的地址
StackOverflow stackOverflow = new StackOverflow();
stackOverflow.redo();
}
至于具体是在堆内存中的那个位置需要在运行的时候才去触发解析。
- 操作数栈和局部变量表,首先看如下的代码
public int compute(){
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
经过javap指令反编译之后得到如下的指令:
public int compute();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
// 将int类型的常量1压入操作数栈,在这里就是将代码中a的值压入操作数栈
0: iconst_1
// 将int类型的值存入局部变量1,在这里就是将1这个值存入存入到a并且放入到局部变量表中
1: istore_1
// 将int类型的常量压入操作数栈,在这里就是将代码中的值压入操作数栈
2: iconst_2
// 将int类型的值存入局部变量2,在这里就是将2这个值存入变量b
3: istore_2
// 从局部变量1中装载int类型的值
4: iload_1
// 从局部变量2中装载int类型的值
5: iload_2
// 执行整型的加法
6: iadd
// 将常量10压入到操作数栈
7: bipush 10
// 执行乘法
9: imul
// 将int类型的值存入局部变量3
10: istore_3
// 从局部变量3中加载int类型的值
11: iload_3
// 返回结果
12: ireturn
结合java源代码以及的反编译之后的代码我们可以得出如下的结论:对变量值的操作都会经过操作数栈。方法内部定义的局部变量是保存在局部变量表中。我们在上面看到的是基础类型都是直接保存的值。那对应其他复杂的类型呢?如对象呢。在这里保存应该是对应引用的地址。其实总结下来如下图
- 所有线程共享区域:堆、方法区
- 堆区:这块是线程公用的空间对象的创建是在这个区域。这个区域也是垃圾回收的主要的区域。然后这个区域根据对象的生存的时间又被划分为的如下的几个区域
- 年轻代
Eden区:对象初次创建的时候有很大的概率会被分配在这个区域。
s0:当Eden被放满之后会触发垃圾回收,将存活的对象放入s0。
s1:当Eden以及s0都满了之后会再一次出发垃圾回收。将还存活的对象转移到s1。
下一次当Eden又满了会回收Edne和s1然后将存活的对象放入s0。下一次会回收Eden和s0。对象每经历过一次垃圾回收分带年龄就会加1当到达十五的时候对象就会被放入老年代。 - 老年代:
- 老年代占了堆的三分之二的空间也是垃圾回收的主战场。那会有哪些对象会进入老年代呢?类对象类型的变量肯定会进入老年代。在老年代会使用不同的垃圾回收算法来提高垃圾回收的效率,降低垃圾回收的时间。在实际的开发中调优其实就是尽量减小fullGC的次数。
- 方法区:
- 存放类信息的区域。默认的大小是21M。当方法区内存空间不足的时候会出发fullGC。回收整个堆。在这个区域还有独特的扩容机制可以使用参数-
XX:MaxMetaspaceSize
限制。默认是不限制的。