JVM

174 阅读9分钟

jvm是可运行java代码的虚拟计算机。jvm是运行在操作系统之上的,他并不与硬件交互,不同平台的jvm各自实现,所以造就了java的跨平台属性,也有了Write Once, Run Anywhere的口号。目前已经不限于java代码,还可以运行Java, Kotlin, Groovy, Scala, Jruby, Jython, Fantom, Clojure, Rhino, Ceylon。额,只知道其中几个,感兴趣的同学自行查阅吧。jvm包括类加载子系统,运行时数据区(程序计数器,虚拟机栈,本地方法栈,方法区(永久代),堆)执行引擎(即时编译器,垃圾回收器),本地库接口(链接本地方法库),直接内存区(并不属于JVM中的内存结构,直接内存不由JVM进行管理。直接内存是属于系统内存,在nio中做数据缓冲区比较常见,读写性能高,回收成本高)。

网上扣俩图帮助理解 image.png 上图红色部分是线程共享的,灰色部分是线程私有的

image.png

类加载子系统

image.png

类加载子系统的作用是将磁盘中或者网络中的字节码class文件加载到内存中,加载的类信息存放在方法区中。ClassLoader只负责class文件的加载,至于其运行还得需要JVM中的执行引擎来执行。这个过程需要对数据进行校验转换解析和初始化。 类加载子系统分为三个阶段,加载阶段,链接阶段,初始化阶段。

首先是加载阶段,加载二进制流文件进内存。
然后是链接阶段,分为检查,准备和解析阶段。先检查class文件是否合法,然后准备阶段,各静态变量赋初始值。然后解析阶段,将符号引用转为直接引用,就是将数据从class常量池中,转移到 运行时常量池中.
最后初始化阶段,将数据存到方法区后,开始为各变量赋值(用户给定的值),自动生成一个clinit方法。

类加载器

BootsrapClassLoader(启动类加载器)

c/c++编写的加载java核心类库的加载器比如JAVA_HOME/jre/lib/rt.jar、resources.jar等jvm自身需要的类。加载扩展类和应用类加载器,并指定为他们的父类。

Extension ClassLoader(扩展类加载器)

加载负责加载JAVA_HOME/jre/lib/ext/子目录下的类(用户创建的jar放在这里也可以加载) 父类加载器为BootsrapClassLoader

Application ClassLoader(应用程序类加载器或系统类加载器)

负责加载用户类路径(Classpath)上的所有类库
父类加载器为Extension ClassLoader 该类加载器是程序中默认的类加载器,一般来说,JAVA应用的类都由它来加载
通过ClassLoader的getSystemClassLoader()方法获取。

自定义类加载器(继承java.long.ClassLoader抽象类)

双亲委派机制

简单描述就是类加载器加载类的时候首先交给父类加载器去加载,层层传递直到最顶层的加载器无法加载的时候才由当前加载器加载这个类。

如果一个类加载器收到了加载类的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到最顶层的启动类加载器中,只有当父加载器无法完成这个加载请求(它的搜索范围没有找到所需的类)时,子加载器才会尝试自己去完成加载。 好处:
(1)避免类的重复加载
(2)保护程序安全,防止核心API被篡改。比如自己定义一个java.long.Object类,是不会被加载的,因为启动类加载器会在rt.jar中找到它,完成加载。在该类中定义一个main方法,会报错,找不到main方法,说明调用的是系统里的java.long.Object类。这样对JAVA的核心代码进行保护,就是沙箱安全机制
其他:
(1)两个class对象是否为同一个类存在两个必要条件,完整类名必须一致,类加载器必须相同
(2)如果一个类是由应用程序类加载器加载的,JVM会将这个累加器的一个引用作为类型信息存在方法区中,当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

运行时数据区

image.png

线程

jvm允许一个应用并发执行多个线程,Hotspot JVM中的java线程与操作系统原生线程有着映射关系,当线程本地存储、缓存区分配、同步对象、栈、程序计数器等准备好后就会创建一个操作系统原生线程,当java线程结束原生线程被回收,操作系统负责调度所有线程并将他们分配到可用的cpu上,当原生线程初始化完成调用java的run方法。线程结束释放所有系统原生线程和java线程。 后台运行的线程主要有,虚拟机线程,周期性任务线程,gc线程,编译器线程,信号分发线程。

jvm内存区域

抠图帮助理解跟上图内容一致

image.png

JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区 域【JAVA 堆、方法区】、直接内存。
线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在 Hotspot VM 内, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存/否跟随本地线程的 生/死对应)。
线程共享区域随虚拟机的启动/关闭而创建/销毁。
直接内存并不是 JVM 运行时数据区的一部分, 但也会被频繁的使用: 在 JDK 1.4 引入的 NIO 提 供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后使用DirectByteBuffer 对象作为这块内存的引用进行操作, 这样就避免了在 Java堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。

程序计数器(线程私有)

image.png

这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域,足以看出这块空间是非常小的。它是线程私有的内存,每一个线程都有自己的程序计数器,他记录当前线程执行字节码指令的地址,如果当前线程执行的是本地方法指令则记录为空。

虚拟机栈(线程私有)

image.png

他是线程私有的,描述java方法执行的内存模型,每个方法执行的时候都会创建一个栈帧(Stack Frame 后面在其他文章中在详细描述)用于存储局部变量,动态链接,方法出口,操作数栈等信息。方法从调用到执行完成的过程对应他的栈祯从入栈到出栈的过程。

本地方法栈(线程私有)

与虚拟机栈功能类似,区别是虚拟机栈是为java方法服务,而本地方法栈是为jvm本地方法(native方法)服务。hotspot vm 将二个区域合二为一了。

堆(线程共享)

是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行 垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以 细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。

方法区/永久代(线程共享)

即我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静 态变量、即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即使用Java 堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存, 而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型 的卸载, 因此收益一般很小)。运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池。 (Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加 载后存放到方法区的运行时常量池中。 Java 虚拟机对 Class 文件的每一部分(自然也包括常量 池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会 被虚拟机认可、装载和执行。

jdk1.8的内存模型图 image.png 运行时常量池,字符串常量池是存放在方法区(永久代)还是存放在堆中一直搞不清楚,查阅后先出结论后续在其他文章中详细描述。结论就是在方法区内,但是方法区是一个概念,方法区的具体实现是随jdk版本升级不断优化的,1.7之前方法区是由永久代实现的所以常量池就在永久代中实现1.7之后永久代开始准备被元空间替换掉(此时还没有被替换)所以迁移到了堆中,1.8永久代彻底被替换掉,但是运行时常量池等并没有迁移到元空间中。所以说常量池在方法区内是正确的,要说具体实现在哪个区域需要参考jdk的版本。