JVM简介

2021-11-16  本文已影响0人  Kuco_Shen

JVM基本概念

JVM是可运行java代码的假想计算机,运行在操作系统之上,与硬件没有直接交互。JVM的基本组成:


运行过程

java源文件通过编译器生成对应的.class文件(字节码文件),字节码文件通过jvm中的解释器,编译成特定机器上的机器码。
1、java源文件-->编译器-->字节码文件
2、字节码文件-->JVM-->机器码
每一种平台的解释器是不同的,但实现的虚拟机是相同的(跨平台的原因)。当一个程序开始运行,虚拟机就开始实例化,多个程序启动就会存在多个虚拟机实例。程序退出或关闭,则虚拟机实例销毁,多个虚拟机实例之间数据不能共享。


线程

线程指程序执行过程中的一个线程实体。JVM允许一个应用并发执行多个线程。


JVM内存区域

线程私有数据区域的生命周期与线程相同,依赖用户线程的启动/结束而创建/销毁VM内,每个线程都与操作系统本地线程直接映射,因此部分内存区域的存/否跟随本地线程的生/死对应。
线程共享区域随虚拟机的启动/关闭而创建/销毁。
线程私有:程序计数器、虚拟机栈、本地方法区
线程共享:JAVA堆、方法区

一、程序计数器

一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域

二、虚拟机栈

描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。

三、本地方法栈

本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务, 如果一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个C 栈,但 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 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。


运行时内存

Java从GC角度可以细分为新生代、老年代和永久代

新生代

是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为Eden 区、ServivorFrom、ServivorTo 三个区。
MinorGC 的过程(复制->清空->互换)
1)Eden区
Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。
2)servivorFrom
上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
3)servivorTo
保留了一次 MinorGC 过程中的幸存者。

老年代

主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

永久代

指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。


JAVA8与元数据

在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。


垃圾回收

如何判断垃圾

1)引用计数法
当对象被引用是计数+1(存在循环引用问题)
2)可达性分析
为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。
要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

垃圾回收算法

1)标记清除
最基础的垃圾回收算法,分为标记和清除两个阶段。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。
ps:该算法会导致内存碎片化,可能发生大对象不能找到可利用的空间
2)复制
为了解决标记清除算法导致的内存碎片化而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。
ps:该算法最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,Copying 算法的效率会大大降低。
3)标记整理
结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
4)分代收集算法
分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将 GC 堆划分为老年代(Tenured/Old Generation)和新生代(Young Generation)。老年代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法


四种引用

1)强引用

在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。

2)软引用

软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。

3)弱引用

弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

4)虚引用

虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。


垃圾回收器

新生代收集器

1)serial(单线程复制、串行)
2)parNew(serial+多线程)
3)parallel scavenge(多线程复制、吞吐量优先)

老年代收集器

1)serial Old(单线程标记整理)

2)parallel Old(多线程标记整理)

3)CMS(多线程标记清除)

整堆收集器(G1)

1、基于标记-整理算法,不产生内存碎片
2、可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。


GC调优


类加载机制

类加载流程:加载-->验证-->准备-->解析-->初始化

加载

这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口。

验证

验证Class 文件的字节流中包含的信息是否符合当前虚拟机的要求。

准备

对类变量分配内存并设置类变量的默认值,即在方法区中分配这些变量所使用的内存空间,常量池、静态变量在JDK8之前存储在方法区中,JDK8之后存储在堆中。
分配内存空间和赋默认值是两个步骤,分配空间在准备阶段,赋值在初始化阶段,但静态变量如果是final的基本类型或字符串常量,赋值则会在准备阶段完成。

解析

虚拟机将常量池中的符号引用替换为直接引用的过程

初始化

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。


类加载器

启动类加载器(BootStrapClassLoader)

加载$JAVA_HOME/jre/lib目录,无法直接访问

扩展类加载器(ExtensionClassLoader)

加载$JAVA_HOME/jre/lib/ext目录,上级为启动类加载器

应用程序类加载器(ApplicationClassLoader)

加载用户路径(classpath)上的类库,上级为扩展类加载器

自定义类加载器(CustomClassLoader)

通过继承 java.lang.ClassLoader实现自定义的类加载器


双亲委派模型

类加载

1.当Application ClassLoader 收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。
2.当Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap
ClassLoader去完成。
3.如果Bootstrap ClassLoader加载失败(在<JAVA_HOME>\lib中未找到所需类),就会让Extension ClassLoader尝试加载。
4.如果Extension ClassLoader也加载失败,就会使用Application ClassLoader加载。
5.如果Application ClassLoader也加载失败,就会使用自定义加载器去尝试加载。
优点:避免重复加载 + 避免核心类篡改
缺点:父类加载器无法委托子类加载器

打破双亲委派模型

重写classloader()方法
例子:
1、JDBC加载mysql驱动
DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,但是DriverManager位于 $JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载,而其Driver接口的实现类是位于服务商提供的 Jar 包,根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。也就是说BootStrap类加载器还要去加载jar包中的Driver接口的实现类,即打破双亲委派模型。
2、tomcat


IO流

阻塞IO模型(BIO)

最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。以流的方式处理数据。
适用于连接数较小且固定的架构。

多路复用IO模型(NIO)

多路复用IO中会有一个线程不断去轮询多个socket的状态,只有当socket有真正的读写事件时,才会调用实际的IO读写操作。以块(缓冲区)的方式处理数据。
适用于连接数多且连接比较短(轻操作)的架构,如:聊天服务器、弹幕系统等,JDK1.4开始支持
Channel(通道):支持非阻塞读写,也可以读写缓冲区,也支持异步读写。
Buffer(缓冲区):本质就是一块可以读写的内存,这块内存被包装成NIO Buffer对象,并提供了一组方法来访问该内存。
Selector(选择器):NIO组件,可以检查一个或多个通道,并确定哪些通道已经准备好进行读写。

异步非阻塞IO模型(AIO)

IO 操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。
适用于连接数多且连接比较长(重操作)的架构,如:相册服务器等,JDK1.7开始支持。

上一篇下一篇

猜你喜欢

热点阅读