JVM加载

1. JVM内存模型与类加载机制

1.1. JVM内存模型

Java代码是运行在Java虚拟机(JVM)上的,Java虚拟机通过解释执行(解释器)或编译执行(编译器)来完成。Java内存模型分为5个部分:
方法区(Method Area),Java堆(Heap),Java栈(VM Stack),本地方法栈(Native Method Stack),程序计数器(PC 寄存器)
在这里插入图片描述

1.1.1. 虚拟机栈(栈)

​ 线程私有的,与线程在同一时间创建。管理JAVA方法执行的内存模型。每个方法执行时都会创建一个桢栈来存储方法的的变量表(基本数据类型)、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小)。栈的大小可以是固定的,或者是动态扩展的。如果请求的栈深度大于最大可用深度,则抛出StackOverflowError;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出OutOfMemoryError。

1.1.2 Java堆(堆)

​ 线程共享的,存放所有对象实例和数组。垃圾回收的主要区域。可以分为新生代(Eden)、老年代(tenured)、永久代(PermGen,Java8废弃,替换为Metaspace:本地内存)。新生代用于存放刚创建的对象以及年轻的对象,如果对象一直没有被回收,生存得足够长,老年对象就会被移入永久区。 新生代又可进一步细分为eden、survivorSpace0(s0,from space)、survivorSpace1(s1,to space)。刚创建的对象都放入eden,s0和s1都至少经过一次GC并幸存。如果幸存对象经过一定时间仍存在,则进入老年代(tenured)。
​ 如果在堆中没有内存完成实例分配,且堆也无法再扩展时,将会抛出OutOfMemoryError 错误
在这里插入图片描述

1.2.2. 方法区

​ 线程共享的,用于存放被虚拟机加载的类的元数据信息:如常量、静态变量、即使编译器编译后的代码。也成为永久代。如果hotspot虚拟机确定一个类的定义信息不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装载该类的ClassLoader被回收
​ 根据Java 虚拟机规范的规定, 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError。

​ JDK1.8之后方法区中的常量存储被转移到堆空间。

1.2.3. 程序计数器

​ 多线程时,当线程数超过CPU数量或CPU内核数量,线程之间就要根据时间片轮询抢夺CPU时间资源。因此每个线程有要有一个独立的程序计数器,记录下一条要运行的指令。线程私有的内存区域。如果执行的是JAVA方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为空。

1.2.4.本地方法栈
本地方法栈和JVM栈非常相似,它们之间的区别不过是jvm栈是为执行java方法服务,而本地方法栈是为jvm使用到对的本地方法服务(由C/C++编写)。HotSpot虚拟机中直接把本地方法栈和JVM栈合二为一了。

1.2. GC机制

1.2.1. 概述

​ 在C/C++中,存在指针的概念,指针(向内存申请空间)创建之后,在使用完时需要手动回收,因此经常会出现回收不当,导致内存溢出;但是,java中不存在指针的概念(实际是引用类型),因为JVM中有GC(Garbage Collection:垃圾收集器)机制,能够在内存不够时自动运行清理可回收空间,不再需要程序手动对内存清理。

1.2.2. 垃圾回收算法
1.2.2.1. 标记清除法(Mark-Sweep)

从根节点开始标记所有可达对象,其余没标记的即为垃圾对象,执行清除。但回收后的空间是不连续的。

在这里插入图片描述

从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片(碎片化),碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

1.2.2.2. 复制算法(Copying)

​ 为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题
在这里插入图片描述

这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

1.2.2.3. 标记压缩法(Mark-compact)

适合用于老年代的算法(存活对象多于垃圾对象)。标记后不复制,而是将存活对象压缩到内存的一端,然后清理边界外的所有对象。

在这里插入图片描述

缺点:效率慢。并且移动他的内存空间会对用户产生速度的影响。

1.2.2.4. 分代收集算法(Generational Collection)

​ 分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。(Java8中PermGen替换成Metaspace)

java8之前该区域位于方法区,Java8之后替换到堆区

1.3. 类加载机制

​ 类加载机制指的是将java源文件产生的*.class文件中的二进制数据装载到内存(JVM)的过程,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口;
​ JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化。以下是一个java类的生命周期:

在这里插入图片描述

通常重点关注:

  • 加载

    • 加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个Class文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。
  • 连接

    • 验证

      • 这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。主要包含四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。
    • 准备

      • 准备阶段是正式为类变量(static变量)分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
    • 解析

      • 解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程

      符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

      ​ 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

  • 初始化

    • 初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。
      初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。ps: 如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。

    注意以下几种情况不会执行类初始化:

    1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
    2. 定义对象数组,不会触发该类的初始化。
    3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
    4. 通过类名获取Class对象,不会触发类的初始化。
    5. 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个 参数是告诉虚拟机,是否要对类进行初始化。
    6. 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。
1.3.1 类加载器

JDK中提供了三个内置的类加载器:

  • BootstrapClassLoader:Bootstrap Loader是由C++撰写而成,预设上它负责搜寻JRE所在目录的classes或lib目录下的.jar档案中(例如rt.jar)是否有指定的类别并加载(实际上是由系统参数sun.boot.class.path指定)

  • **ExtensionClassLoader:**ExtClassLoader负责搜寻JRE所在目录的lib/ext 目录下的classes或.jar中是否有指定的类别并加载(实际上是由系统属性:java.ext.dirs指定)

  • **AppClassLoader:**AppClassLoader则搜寻 Classpath中是否有指定的classes并加载(由系统参数java.class.path指定)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

西伯利亚大熊猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值