JVM万字详解,复习必备。


JVM主要从五方面来看,下面来一次详解
在这里插入图片描述

JVM基本概念及内存区域

基本概念

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。JVM帮我们处理了不同硬件之间的差异,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。(一次编译到处运行)

运行过程

  • Java 源文件(.java)—>编译器—>字节码文件(.class)—>JVM—>机器码(可以在不同的平台上到处跑)

每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。

内存区域

JVM 内存区域主要分为虚拟机栈、本地方法栈、程序计数器、方法区、堆,其中程序计数器、虚拟机栈和本地方法栈为线程私有,堆和方法区为线程共有。

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

在这里插入图片描述

堆 (线程共享)

在这里插入图片描述

堆主要用于存放各种类的实例对象和数组。堆是JVM中最大的一块内存。在java中被分为两个区域:年轻代和老年代。

  • 新生代:新创建的数据会在新生代,当经历一定次数的GC后活下来的数据,会移动到老年代(HotSpot默认的垃圾回收是15次)
    新生代又分为三个区域:Eden、S0、S1(Eden经常和S0或S1中的一个配合使用)垃圾回收的时候会将 Endn 中存活的对象放到⼀个未使⽤的 Survivor 中,并把当前的 Endn 和正在使⽤ 的 Survivor 清楚掉。
  • 老年代:存放的是经过了一定次数还存活的对象和大对象(因为大对象的创建和销毁所需要的时间比较多,如果放在新生代,可能会频繁的创建和销毁,从而导致性能比较慢,JVM运行效率降低,所以直接将大对象放在老年代)
    Java虚拟机规范规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。也就是说堆的内存是一块块拼凑起来的。要增加堆空间时,往上“拼凑”(可扩展性)即可,但当堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

Java虚拟机栈 (线程私有)

栈是描述 java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调 用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
在这里插入图片描述

  • 栈帧(StackFrame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完
    成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
  • 局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型 (指向一条字节码指令的地址)。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
  • 操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操 作。
  • 动态链接,Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。
  • 方法返回地址,无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行

程序计数器 (线程私有)

程序计数器是线程私有、占用内存较小、没有OOM异常,主要用于指令切换。
在这里插入图片描述
程序计数器的作用可以看做是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变计数器的值来选取下一条字节码指令。其中,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来完成。 Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计 数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的 内存。

本地方法栈 (线程私有的)

和虚拟机栈类似,只不过 Java 虚拟机栈是给 JVM 使⽤的(执行Java方法(字节码)服务),⽽本地⽅法栈是给本地⽅法使⽤的(C/C++)(Native方法服务。)

方法区 (线程共享)

方法区也是线程共有的,存储的内容主要有常量、静态变量和类信息。
在这里插入图片描述
方法区即我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. JDK8 已经被元空间取代。

  • 字符串常量池
  • 运⾏时常量池: 运⾏时常量池是⽅法区的⼀部分,Class 文件中除了有 类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
  • 字⾯量 : 字符串(JDK 8 移动到堆中)、final常量、基本数据类型的值。
  • 符号引⽤ : 类和结构的完全限定名、字段的名称和描述符、⽅法的名称和描述符。

元空间是JDK1.8之后的叫法,元空间存储在本地,而不在虚拟机当中(不受JVM最大运行内存的限制,只和本地内存的大小有关),并将字符串常量池放到了堆中。

内存布局小结

在这里插入图片描述

JVM运行时内存结构

JVM类加载机制

JVM类加载过程

JVM将编译好的.class文件(字节码文件)以二进制流的方式加载到我们内存中,并且将二进制流中静态的数据结构转换成我们方法区中动态运行数据结构,并且在对堆内存生成一个java.lang.class对象,作为提供给外界访问我们方法区动态运行数据结构的一个入口。
JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看 一下这五个过程。
加载(loading) -> 验证 -> 准备 -> 解析 -> 初始化 -> 使⽤ -> 卸载
在这里插入图片描述
加载:将类转换为二进制字节流加载到内存中去
验证:验证这个二进制字节流是否符合要求
准备:将需要复制的变量先创建好,但是不赋值
解析:将常量池的的符号引用转换为直接引用
初始化:开始为变量真正的赋值
加载 “加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的⼀个阶段。在加载阶段,Java虚拟机需要完成以下三件事情:

  • 1)通过⼀个类的全限定名来获取定义此类的⼆进制字节流。
  • 2)将这个字节流所代表的静态存储结构转化为⽅法区的运⾏时数据结构。
  • 3)在内存中⽣成⼀个代表这个类的java.lang.Class对象,作为⽅法区这个类的各种数据的访问⼊⼝。

验证: 验证是连接阶段的第⼀步,这⼀阶段的⽬的是确保Class⽂件的字节流中包含的信息符合《Java虚拟机规 范》的全部约束要求,保证这些信息被当作代码运⾏后不会危害虚拟机⾃身的安全。
验证选项:

  • ⽂件格式验证
  • 字节码验证
  • 符号引⽤验证…

准备: 准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。⽐如此时有这样⼀⾏代码: public static int value = 123;它是初始化 value 的 int 值为 0,⽽⾮ 123。
解析: 解析阶段是 Java 虚拟机将常量池内的符号引⽤替换为直接引⽤的过程,也就是初始化常量的过程。
初始化: 初始化阶段,Java 虚拟机真正开始执⾏类中编写的 Java 程序代码,将主导权移交给应⽤程序。初始化阶段就是执⾏类构造器⽅法的过程。

类加载器

虚拟机设计团队把加载动作放到 JVM 外部实现,以便让应用程序决定如何获取所需的类,Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件) 类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。 JVM 提供了 3 种类加载器:
在这里插入图片描述

  • 引导类加载器BootStrap ClassLoader:负责加载支撑JVM运行的位于JRE的lib目录下核 心类库,比如rt.jar、charsets.jar等
  • 扩展类加载器Extension ClassLoader:负责加载支撑JVM运行的位于JRE的lib目录下的 ext扩展目录中的JAR类包
  • 应用类加载器Application ClassLoader:负责加载ClassPath路径下的类包,主要就是加 载你自己写的那些类

双亲委派

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,(坑爹)每一个层次类加载器都是如此,因此所有的加载请求都应该传送到引导类加载其中, 只有当父类加载器反馈自己无法完成这个请求的时候 (在它的加载路径下没有找 到所需加载的 Class),子类加载器才会尝试自己去加载。
优点:

  • 唯一性:采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的引导类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象
  • 安全性:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改

破坏双亲委派

使用自定义类加载器,继承ClassLoader类,并重写loadclass和findclass方法
双亲委派模型被破坏总共发⽣过 3 次:

  1. 第⼀次是 JDK 1.2 引⼊双亲委派模型的时候,因为之前在 JDK 就存在了 ClassLoad 代码,所以在 JDK 1.2 为了兼容⽼代码也做过⼀些妥协破坏了双亲委派模型。
  2. 第⼆次是⾃⼰的问题所导致的,当⽗类想要调⽤⼦类的⽅法时,使⽤双亲委派模型就没办法调⽤,这是 第⼆次破坏。
  3. 第三次是最近⼏年对“热”更新和“热”部署的追求,说⽩了就是希望
  • 21
    点赞
  • 105
    收藏
    觉得还不错? 一键收藏
  • 24
    评论
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值