JVM

一、类的生命周期

1、加载阶段

1、类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息,例:通过本地文件,动态代理生成

2、加载完类之后,java虚拟机会将字节码的信息保存到方法区中

3、其实java虚拟机会在方法区生成一个instanceKlass对象,这个对象保存了类的所有信息,另外还会在堆中生成一个java.lang.class的对象,并且这两个对象会建立关联关系

为什么这里要创建两个对象呢?用一个instanceKlass不行吗?事实上,堆中生成的对象是给我们程序员使用的,它去掉了一些底层的参数

2、连接阶段

1、验证

这一步主要是为了验证内容是否满足java虚拟机的规范

  • 文件格式的验证
    • 字节码文件是否cafebabe字节开头,版本号是否满足当前java虚拟机的要求
  • 元信息的验证
  • 验证程序执行指令的正确性
  • 符号引用验证,是否访问了其他类中的私有方法等

2、准备

这一步主要是为了给静态变量赋初始值,因为如果没有初始值,可能会导致这块内存区域原本残留的内容打印出来,对用户不友好,如果该静态变量为final修饰的,则不赋初始值,而是直接赋予代码中的值

比如:

int = 0

long = oL

short = 0

char = ‘\u0000’

byte = 0

boolean = false

double = 0.0

引用类型 = null

3、解析

这一步主要是为了将类常量池中的符号引用替换成直接引用

具体如何转换的呢?

这里以方法调用为例:

1、通过字节码指令invokevirtual 去找到对应的方法符号引用

2、根据当前方法符号引用中的class_index去找到类的符号引用

3、通过类的符号引用找到当前类,然后通过当前类的对象头中的信息去找到instanceKlass

4、找到instanceKlass之后再通过符号引用中的name_type_index找到方法名和描述符

5、通过方法名和描述符在instanceKlass的方法表,vtable,itable中寻找对应的方法

6、如果是(静态/私有/构造方法)则在方法表中寻找并返回一个访问地址,而如果是实例方法则在虚方法表中寻找并返回虚方法表中对应的索引,如果是接口方法则根据接口类型和方法索引定位具体方法

7、将方法的符号引用替换成访问地址或者虚方法表索引

字段调用为例

1、如果是静态变量则直接将这个字段的地址赋予给符号引用就完成了替换

2、如果是实例变量则通过类的符号引用找到instanceKlass,然后在instanceKlass中的字段表获取字段的偏移量

3、最后通过对象的内存地址加字段的偏移量找到真实地址,这个地址就是直接引用

3、初始化阶段

初始化阶段会执行静态代码块的代码,会对静态变量赋值

初始化阶段会执行字节码中clinit部分的字节码指令

以下情况是不会出现clinit指令的:

无静态代码块和无静态变量赋值的语句

有静态变量声明但没有赋值语句例(public static int a;)

静态变量使用final关键字并且等号右边是常量

以下几种方式会触发类的初始化

  • 访问类的静态变量或者方法,final修饰的属性并且等号右边不是常量
  • 调用Class.forName方法
  • new一个该类的对象
  • 执行Main方法的当前类

如果直接访问父类的静态变量,不会导致子类的初始化,子类初始化前会先调用父类的初始化

数组的创建不会导致数组中元素的类进行初始化,例:(A[] a = new A[10])这里的A不会初始化

二、类加载器

1、类加载器介绍

类加载器有:

  • 启动类加载器
  • 扩展类加载器
  • 应用程序类加载器

启动类加载器主要用于加载JAVA中最核心的类

扩展类加载器加载JAVA中比较通用的类

应用程序类加载器加载我们自己写的类,包括maven引入第三方类

2、双亲委派机制

在加载一个类时,会从应用程序加载器–>扩展类加载器–>启动类加载器从底部往上寻找是否加载过这个类,如果有加载器加载过这个类就会进行加载。如果找到顶部都没有加载器加载过这个类,则从启动类加载器–>扩展类加载器–>应用程序加载器从上往下尝试加载,如果这个类不在加载器的加载路径中,则会委派给下一级的加载器加载。直到找到一个加载器可以加载。

向下委派可以起到加载优先级的作用,因为如果有一个类可以被这三个加载器加载,那肯定是启动类加载器加载

这个机制主要是为了防止恶意代码替换JAVA核心类,保证核心类库的安全,也避免了一个类会被多个加载器重复加载

双亲委派机制核心伪代码:

parent = findParent();
if (parent != null) {
c = parent.loadClass(name);
} else{
c = findparentClass(name);
}

if (c == null) {
c = findClass(name);
}

首先通过findParent找到父类加载器,如果找到了,就递归进入loadClass,因为上面的方法就是在loadClass类中,然后进入父类加载器的loadClass中之后继续调用findParent,直到找到扩展类加载器的父类时,parent为null,则调用findparentClass去启动类加载器中寻找是否有这个类,如果有这个类,则c就不为null,就找到了。如果没有则c为null,则在当前扩展类加载器调用findClass寻找这个类,如果还是为null,这个方法就结束出栈了,递归回溯到应用程序加载器调用findClass。这就是一个典型的递归回溯,递归查询有无加载器加载过这个类,再递归回溯加载类。

3、打破双亲委派机制

1、自定义类加载器

一个Tomcat是可以运行多个web程序的,如果说这两个应用都用了相同限定名的类,Tomcat就可以保证这两个类都能加载

Tomcat的底层为每一个web应用单独生成了一个类加载器,它不走双亲委派机制了

三、运行时数据区

线程共享:

方法区、堆

线程不共享:

程序计数器、java虚拟机栈、本地方法栈

1、程序计数器

也叫PC寄存器,每个线程会通过程序计数器记录当前执行的字节码指令的地址,每一条字节码指令都有自己的内存地址

在多线程的情况下,切换线程时,程序计数器会记录当前执行的字节码指令地址,同样的,切换到另一个线程时会执行切换之前的程序计数器的字节码指令

程序计数器是不会发生内存溢出的,因为他存储的是一个固定长度的地址


已发布

分类

,

来自

标签:

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注