JVM 垃圾自动回收机制

JVM 垃圾自动回收机制

虚拟机栈

:描述的是方法执行时的内存模型,是线程私有的,生命周期与线程相同,每个方法被执行的同时会创建栈桢(下文会看到),主要保存执行方法时的局部变量表、操作数栈、动态连接和方法返回地址等信息,方法执行时入栈,方法执行完出栈,出栈就相当于清空了数据,入栈出栈的时机很明确,所以这块区域不需要进行垃圾回收
栈和堆的区别详解

本地方法栈

:与虚拟机栈功能非常类似,主要区别在于虚拟机栈为虚拟机执行 Java 方法时服务,而本地方法栈为虚拟机执行本地方法时服务的。这块区域也不需要进行垃圾回收

程序计数器

:线程独有的, 可以把它看作是当前线程执行的字节码的行号指示器,比如如下字节码内容,在每个字节码`前面都有一个数字(行号),我们可以认为它就是程序计数器存储的内容
本地内存:线程共享区域,Java 8 中,本地内存,也是我们通常说的堆外内存,包含元空间和直接内存,注意到上图中 Java 8 和 Java 8 之前的 JVM 内存区域的区别了吗,在 Java 8 之前有个永久代的概念,实际上指的是 HotSpot 虚拟机上的永久代,它用永久代实现了 JVM 规范定义的方法区功能,主要存储类的信息,常量,静态变量,即时编译器编译后代码等,这部分由于是在堆中实现的,受 GC 的管理,不过由于永久代有 -XX:MaxPermSize 的上限,所以如果动态生成类(将类信息放入永久代)或大量地执行 String.intern (将字段串放入永久代中的常量区),很容易造成 OOM,有人说可以把永久代设置得足够大,但很难确定一个合适的大小,受类数量,常量数量的多少影响很大。所以在 Java 8 中就把方法区的实现移到了本地内存中的元空间中,这样方法区就不受 JVM 的控制了,也就不会进行 GC,也因此提升了性能(发生 GC 会发生 Stop The Word,造成性能受到一定影响,后文会提到),也就不存在由于永久代限制大小而导致的 OOM 异常了(假设总内存1G,JVM 被分配内存 100M, 理论上元空间可以分配 2G-100M = 1.9G,空间大小足够),也方便在元空间中统一管理。综上所述,在 Java 8 以后这一区域也不需要进行 GC

jvm包括两种数据类型,基本类型和引用类型。

基本类型包括,数值类型,boolean类型,和returnAddress类型。

数值类型包括,整型,浮点型,和char类型。

boolean类型同样只有true和false。

returnAddress类型是一个指针,指向jvm指令的操作码,在Java中没有与之对应的类型。

boolean类型的操作会被转化为int类型的操作进行,boolean数组会当成byte数组去操作。1表示true,0表示false。

引用类型包括三种,类类型,数组类型,和接口类型。

它们的值是动态创建的类实例,数组,或实现接口的类实例。

数组有component类型和element类型,component类型就是数组去掉最外层维度后剩下的类型,可能还是一个数组类型(对于多维数组)。

element类型就是数组里面存储的最小数据的类型,它必须是一个基本类型,类类型,或接口类型。

对于一维数组的话,component类型和element类型是相同的。

引用类型还有一个特殊值,就是null,表示没有引用任何对象。

运行时公有数据区

jvm有一个堆,在所有jvm线程间共享,堆是一个运行时数据区域,所有为类实例和数组分配的内存都来自于它。

堆在jvm启动时创建,堆中对象不用显式释放,gc会帮我们释放并回收内存。

方法区

jvm有一个方法区,在所有jvm线程间共享,它存储每一个类的结构。

像运行时常量池,字段和方法数据,方法和构造函数的代码,还有特殊的方法用于类和实例的初始化,以及接口的初始化。

方法区在jvm启动时创建,虽然方法区在逻辑上是堆的一部分。

但简单实现时可以选择不进行gc和压缩,本规范没有强制要求方法区的位置,也没有要求管理已编译代码的策略。

运行时常量池

运行时常量池就是类或接口的字节码文件里的常量池的运行时表示形式,它包含几种常量。

如在编译时就已经知道的数字字面量值,和必须在运行时解析的方法和字段的引用,运行时常量池的功能类似于传统语言的符号表,不过它包含的数据会更加宽泛。

运行时常量池分配在jvm的方法区,类或接口的运行时常量池在类或接口被jvm创建时才会构建。

运行时私有数据区

pc寄存器

jvm支持一次运行多个线程,每个线程都有自己的pc寄存器,任何时候一个线程只能运行一个方法的代码。

如果方法不是native的,pc寄存器包含当前正在被执行的jvm指令地址,如果方法是native的,pc寄存器的值是未定义的。

jvm栈

每一个jvm线程都有一个私有的jvm栈,随着线程的创建而创建,栈中存储的是帧。

jvm栈和传统语言如C的栈相似,保存局部变量和部分计算结果,参与方法的调用和返回。jvm栈主要用于帧的出栈和入栈,除此之外没有其它操作,

帧可能是在堆上分配的,所以jvm栈使用的内存不必是连续的。

native方法栈

native方法不是用Java语言写的,为了支持它需要使用传统栈,如C语言栈。不过jvm不能加载native方法,所以也不需要提供native方法需要的栈。

每次当一个方法被调用时一个新的帧会被创建。当方法调用完成时,与之对应的帧会被销毁,无论是正常完成还是抛异常结束。

所以帧是方法调用的具体体现形式,或称方法调用是以帧的形式进行的。帧用来存储数据和部分计算结果,和执行动态链接,方法返回值,分发异常。

帧分配在创建帧的线程的jvm栈上,每一个帧都有自己的本地变量数组,自己的操作数据栈,和一个对当前方法所在类的运行时常量池的引用。

本地变量数组和操作数栈的大小在编译时就确定了,它们随着和帧关联的方法编译后的代码一起被提供,因此帧这种数据结构的大小只依赖于jvm的实现,这些结构所需的内存可以在方法调用时同时被分配。

在一个线程执行的任何时刻,都只会有一个帧是处于激活的。这个帧被称为当前帧,与之对应的方法被称为当前方法,方法所在的类被称为当前类,此时用到的本地变量数组和操作数栈也都是当前帧的。

一个帧将不在继续是当前帧,如果它的方法调用了另一个方法,或者它的方法结束了。

当一个方法被调用,一个新的帧被创建,当执行控制由原来的方法传递到新的方法时,这个新的帧变为当前帧。

当方法返回时,当前帧把方法执行的结果传回到上一帧,当上一帧被激活的同时当前帧会被丢弃。

本地变量数组

每一帧都包含一个变量数组,就是都熟知的本地变量存储的地方。这个本地变量数组的长度在编译时确定,随着编译后的方法代码一起提供。

通常一个本地变量(的位置)能够存储一个类型的值,但是long和double类型却需要两个本地变量(的位置)才能存一个值。

本地变量按索引寻址,第一个本地变量的索引是0。long和double需要消耗两个连续的索引,但却是按照较小的这个索引寻址的。不能按照较大的那个索引去读数据,但是可以写入,当然这样将使本地变量内容错乱。

在方法被调用时,jvm使用本地变量来接收传递进来的参数值。在类(静态)方法调用时,所有参数被传入从索引0开始的连贯的本地变量数组里。

在实例(非静态)方法调用时,索引0处总是传入正在其上执行方法调用的那个对象的引用,(就是Java中的this了),所有参数被传入从1开始的连贯的本地变量数组里。

操作数栈

每个帧包含一个后进先出的栈,用于存储正在执行的jvm指令的操作数,就是都熟知的操作数栈,这个栈的最大深度在编译时就已确定,随着编译后的方法代码一起提供。

当帧被创建时,操作数栈是空的,jvm提供一些指令用于加载常量值,本地变量值,字段值到操作数栈上,另一些jvm指令采用操作数栈上的操作数进行操作,并把结果放回到操作数栈上。

操作数栈也用于准备将要传递给方法调用的参数和接收方法调用返回的结果。

long和double类型的值占用两个单位的栈深度,其它类型的值占用一个单位的栈深度。

动态链接

每一个帧都包含了对当前方法所属类型的运行时常量池的引用。目的是为了支持方法代码的动态链接。class文件中描述一个方法引用被调用的方法和被访问的变量的代码,是采用符号引用的形式实现的。

符号引用的形式可以粗略的认为是字符串的形式,就是用字符串标明需要调用哪个类的哪个方法或访问哪个字段或变量。就像符号引用这个名字一样,这些仅仅是符号,是拿不到具体值的,所以必须要进行转换。

动态链接就是把这些符号方法引用转换为具体的方法引用,在必要时加载类来解析尚未明确的符号,把符号变量的访问转换为这些变量运行时所在存储结构的适合的偏移量(索引)。这样的方式又称为后期绑定。

方法调用

一个方法调用正常完成(即没有抛异常)时,会根据所返回的值的类型执行一个适合的return指令,当前帧会去恢复调用者的状态,包括它的本地变量和操作数栈,使调用者的程序计数器适合的递增来跳过刚刚的那个方法调用指令。

返回值会被放到调用者帧的操作数栈上,然后继续执行调用者方法的帧。

一个方法在调用时抛出了异常,且这个异常没有在这个方法内被捕获处理,将会导致这个方法调用的突然结束,这种情况下永远不会向方法的调用者返回一个值。

特殊方法

站在jvm的级别,每一个用Java写的构造函数都以一个实例初始化方法出现,且都是特殊的名字,就是,这个名字是编译器提供的。

实例初始化方法只能在jvm内部使用invokespecial这个指令调用,且只能在尚未初始化的类实例上调用。

一个类或接口最多可以有一个类或接口初始化方法,通过调用这个方法被初始化。类或接口的初始化方法也有特殊的名字,就是,该方法没有参数,且返回值是void。

方法名称也是由编译器提供的,从Java7开始,在字节码中这个方法必须被标记为静态的才行。

这个初始化方法是被jvm隐式调用的,它们绝对不会直接被用任何jvm指令调用,仅作为类初始化进程的一部分被间接的调用。

Java类库

jvm必须为Java类库的实现提供足够的支持。一些类库中的类如果没有jvm协助是无法实现的。

反射,就是在运行时获取某个类的类型相关信息,如它的字段信息,方法信息,构造函数信息,父类信息,实现的接口信息。

这些信息都必须是把一个类加载完之后才可以知道的,只有jvm才可以加载类。如java.lang.reflect这个包下的类和Class这个类。

在Java中加载一个类或接口用类加载器,即ClassLoader,背后还是委托给jvm来实现的。

链接和初始化一个类或接口。

安全,如java.security包下的类,还有其它类像SecurityManager。

多线程,如线程这个类Thread。

弱引用,像java.lang.ref包下的类。

公有设计,私有实现

以上内容只是jvm的一个“相对宽泛”的规范,它并不是实现方案,也不是实现细节。

实现者可以根据自身的需要来实现jvm,如运行在后端服务器上的jvm和运行在移动设备上的jvm肯定侧重点有所不同。

从事Java的人都知道,事实上jvm是有较多的实现版本。

由于jvm是处在Java语言和操作系统之间的,所以它要向上提供对Java的支持,向下与操作系统良好交互。

PS:

高级语言(Java,C#)中的很多操作如文件操作,网络操作,内存操作,线程操作,I/O操作等,都不是高级语言自身能够实现的。也不是它们的虚拟机(JVM,CLR)能够实现的,实际最终是由操作系统实现的,因为这些都是系统资源,只有操作系统才有权限访问。
如果你用Java或C#代码创建了一个文件,千万不要以为是Java或C#创建了这个文件,它们只是层层向下调用了操作系统的API,然后到文件系统API,最后可能到磁盘驱动程序。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值