JVM 简述

java通过编译.java源文件文件生成.class字节码文件,然后字节码文件通过java虚拟机的解释器,生成机器码来执行。
.java -> 编译器 -> .class -> jvm解释器 -> 机器码
Java之所以能跨平台,是因为每种平台的解释器不同,但实现的虚拟机是相同的。当一个程序运行,虚拟机就会开始实例化,多个程序运行会创建多个虚拟机,每个虚拟机之间的数据都不共享。当程序退出或停止,虚拟机实例消亡。
线程
jvm允许一个应用并发的执行多个线程,Hotspot JVM中的java线程与原生操作系统的线程有直接映射关系。
当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好后,就会创建一个操作系统原生线程。java线程结束,原生线程随之被收回。
操作系统负责调度所有线程,把它们分配到任何可用的cpu上。当原生线程初始化完毕,就会调用java线程的run()方法。当线程结束,会释放所有java线程和原生线程资源。

Hotspot JVM 后台运行的主要线程有以下几个:
| name | detail |
|---|---|
| 虚拟机线程(VM Thread) | 这个线程等待jvm到达安全点操作出现。这些操作都必须在独立的线程内执行,因为当堆修改无法进行时,线程都需要jvm位于安全点。这些操作的类型有stop-the-world垃圾回收、线程栈dump、线程暂停、线程偏向锁(biased locking)解除。 |
| 周期性线程任务 | 负责定时器事件(也就是中断),用来调度周期性操作的执行 |
| GC线程 | 这些线程支持jvm中不同的垃圾回收活动 |
| 编译器线程 | 这些线程动态的将字节码文件编译成本地平台的机器码 |
| 信号分发线程 | 这些线程接收发送到jvm的信号,并调用适当的jvm方法来处理 |
jvm内存区域
jvm 内存区域主要分为:线程私有区域:[虚拟机栈,程序计数器,本地方法区]。线程共享区:[堆,方法区(jdk8中取消了方法区,替换成了metaspace),直接内存(direct memory)]。
线程私有数据区域的生命周期与用户线程相同,依赖用户线程 启动/停止 而 创建/销毁 。(在hotspot jvm 中每个线程都与操作系统本地线程对应,因此这部分内存区域是否存在和本地线程生/死对应)
线程共享区域随虚拟机的 启动/停止 而 创建/销毁。

以下为线程私有内存
程序计数器 Program Counter Register
一块较小的内存空间,是线程所有执行的字节码的信号指示器,每一条线程都有一个独立的程序计数器,这类内存称为线程私有内存。
程序计数器记录的是java方法的虚拟机字节码指令地址(当前指令的地址)。执行的native方法会记录在本地方法区,此时程序计数器为空。
程序计数器是虚拟机内存中,唯一一个没有被限制 OutOfMemoryError 的区域。
虚拟机栈 VM Stack
描述java方法执行的内存模型,每个方法执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量、操作数栈、动态链接、方法出口等信息。每一个方法调用到结束的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧(Stack Frame) 用来存储数据和部分过程结果的数据结构,同时也用来处理 动态链接(Dynamic Linking)、 方法返回值和异常分配(Dispatch Exception)。随着方法调用创建栈帧,结束时被销毁,无论是正常结束还是异常完成(抛出了方法内未捕获的异常)都算方法结束。

本地方法区 Native Method Stack
本地方法区和 VM Stack 类似,区别就是 VM Stack 存放的是 java方法,而本地方法区存放的是 native 方法。
以下为线程共享内存
堆 Heap
被线程共享的一块区域,java中创建的对象、数组实例都会保存在堆中,也是垃圾回收器进行垃圾收集的重要区域。由于VM采用了分代收集算法,因此在GC的角度也可分为:新生代(Eden, FromSurvivor, ToSurvivor),老年代。
方法区/永久代 Permanent Generation
用来存储被JVM加载的类信息、常量、静态变量、即时编译器编译的代码等数据。Hotspot 把 GC 分代收集器扩展至方法区,即使用java堆永久代来实现方法区,这样Hotspot的垃圾收集器就可以像管理堆一样来管理方法区,不用再单独为了方法区开发专门的内存管理器了。(永久代回收主要针对常量池和类的卸载,因此收益不高)
运行时常量池(Runtime Constant Pool)是方法区的一部分。class文件中除了有类的版本、字段、方法、接口描述等信息外,还有一项信息是常量池 Constant Pool Table, 用于存放编译时期生产的字面常量和符号引用,这部分内容是加载后存放到方法区的 运行时常量池中的。
java 虚拟机对class格式有严格的要求,每一个字节存储的数据都必须符合规范,这样才会被虚拟机认可,装载,执行。
jvm 运行时内存

新生代
用来存放新生的对象,一般占堆内存的1/3,由于频繁的生成对象,所以新生代会频繁的触发 MinorGC进行垃圾回收。
新生代分为以下几个部分:
Eden
- java新对象的出生地,如果对象占用内存过大,直接进入老年代。当eden区内存不够使,会触发 minorGC 来进行一次垃圾回收。
FromSurvivor
- 上一次GC的幸存者,作为这一次GC的扫描对象
ToSurvivor
- 保留了一次 minorGC 过程的对象
MinorGC 过程
MinorGC 采用复制算法(复制 -> 清空 -> 互换)
- eden & FromSurvivor 复制到 ToSurvivor 同时年龄加 1
将eden & FromSurvivor 中存活的对象复制到 ToSurvivor 中,同时这些对象年龄加一。(如果存活对象达到了年龄(默认是15)或者 ToSurvivor 内存不够存储对象,就会放入到老年代中)
-
清空 eden & FromSurvivor 中的对象。
-
将 FromSurvivor & ToSurvivor 互换,原来的 ToSurvivor 成为下一次的 FromSurvivor。
老年代
主要存放生命周期较长的对象,比较稳定,所以老年代不会频繁的执行 majorGC 。在进行 majorGC 之前一般都执行过一次 minorGC 使新生代对象晋身入老年代导致内存不够才触发。
当老年代无法找到足够大的连续空间分配给新创建的对象时,也会执行一次 majorGC 进行垃圾回收腾出空间。
当老年代内存在垃圾回收后也无法装下新对象时,会抛出OOM (Out Of Memory Error).
永久代
内存中永久保存的区域,主要存放 class 和 meta data, class 在被加载的时候就会进入永久区,永久区和实例存储区的区别就是GC不会在主程序运行期对永久区进行垃圾回收,所以随着时间加载的 class 越来越多,最终会抛出 OOM (Out Of Memory Error)。
java8 与元数据
在 java8 中永久代已经被移除, 元空间 metaspace 替换掉了它。metaspace 和永久代的区别就是 metaspace 使用的是本地内存,并不在虚拟机中。因此,默认情况下 metaspace 的大小仅受本地内存控制,class 和 元数据存储到本地内存,字符串池和常量放到java堆中,这样加载多少元数据就不再由 MaxPermSize 控制,由操作系统的实际内存可用空间来控制。
垃圾回收与算法


浙公网安备 33010602011771号