JVM 总结

文章目录

  • java类加载
    • 类加载过程
      • 加载
      • 链接
        • 验证
        • 准备
        • 解析
      • 初始化
    • 类加载时机
    • 类加载器
    • 类加载机制
      • 全盘负责
      • 双亲委派
      • 缓存机制
  • 程序计数器
    • 什么是程序计数器
    • 程序计数器的特点
  • JVM运行时数据区
    • 虚拟机栈(VM Stack)
    • 本地方法栈
    • Java堆
      • Java堆结构
      • GC
      • GC发生情况
  • 垃圾回收算法
    • 标记-清除(Mark-Sweep)
    • 复制(Copy)
    • 标记-整理(Mark-Compact)
    • 分代收集算法
      • 年轻代
      • 老年代
      • 持久代

java类加载

类加载过程

.java --> .class --> 内存

加载

加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象

class文件来源:

  • 本地文件系统
  • JAR包
  • 网络加载
  • java源文件动态编译生成

链接

把类的二进制数据合并到 JRE

验证

用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。

验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。

包括:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

准备

类准备阶段负责为类的静态变量分配内存,并设置默认初始值。

  • 不包括实例变量(未被static修饰)
  • 不包含用final修饰的static(在编译时分配)

解析

将类的二进制数据中的 符号引用 替换成 直接引用

  • 符号引用就是一组符号来描述目标,可以是任何字面量。
  • 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化

为类的静态变量赋予正确的初始值。

如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析,到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。

类加载时机

常见

  1. 创建类的实例(即new对象)
  2. 调用某个类/接口的静态变量,或对静态变量赋值
  3. 调用类的静态方法

没那么常见

  1. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  2. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  3. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

类加载器

  • 启动类加载器
    • 由C/C++写的,主要负责加载 jre\lib 目录下的类;
  • 扩展类加载器
    • 主要负责加载 jre\lib\ext 目录下的类;
  • 应用程序类加载器
    • 主要负责加载我们自己编写的类;
  • 当然还能自己写类加载器,即自定义加载器。
    • 程序主要由前面三个类加载器相互配合加载的。

类加载机制

全盘负责

所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

双亲委派

所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。

通俗的讲,就是每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

优点:

  • 采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
  • 其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

缓存机制

缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。

这就是为什么修改了Class后,必须重新启动 JVM,程序所做的修改才会生效的原因。

程序计数器

什么是程序计数器

程序计数器是一个记录着当前线程所执行的字节码的行号指示器。

程序计数器的特点

  1. 线程隔离性,每个线程工作时都有属于自己的独立计数器。
  2. 执行java方法时,程序计数器是有值的,且记录的是正在执行的字节码指令的地址。
  3. 执行native本地方法时,程序计数器的值为空(Undefined)。
  4. 程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。
  5. 程序计数器,是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。

JVM运行时数据区

虚拟机栈(VM Stack)

虚拟机栈是每个Java方法的内存模型,虚拟机栈中元素叫做“栈帧”,每一个方法被执行的时候都会压入一个栈帧,执行完毕则出栈,这个栈帧里面存放着这个方法的局部变量表(包括参数)、操作栈、动态链接、方法返回地址。

1. Java虚拟机栈也是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)

2. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;

如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;

3. Java虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时会创建一个栈帧。

对于我们来说,主要关注的stack栈内存,就是虚拟机栈中局部变量表部分。

本地方法栈

Navtive方法是Java通过JNI直接调用本地C/C++库。

本地方法栈(Native Method Stacks)我们可以理解为本地方法的虚拟机栈。

Java堆

堆是Java虚拟机所管理的内存中最大的一块存储区域。

  • 堆内存被所有线程共享
  • 主要存放使用new关键字创建的对象。
  • 所有对象实例以及数组都要在堆上分配。
  • 垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间(收集的是对象占用的空间而不是对象本身)

Java堆结构

  • 年轻代
    • 伊甸园(Eden)
    • 幸存区(Survivor区)
      • From Survivor空间
      • To Survivor空间
  • 老年代

GC

  • Minor GC : 清理年轻代
  • Major GC : 清理老年代
  • Full GC : 清理整个堆空间,包括年轻代和永久代

GC发生情况

  • 年轻代存储“新生对象”,我们新创建的对象存储在年轻代中。当年轻内存占满后,会触发Minor GC,清理年轻代内存空间。
  • 老年代存储长期存活的对象和大对象。年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储。
  • 老年代空间占满后,会触发Full GC

垃圾回收算法

标记-清除(Mark-Sweep)

  • GC分为两个阶段,标记和清除。
  • 首先标记所有可回收的对象,在标记完成后统一回收所有被标记的对象。
  • 同时会产生不连续的内存碎片
  • 碎片过多会导致以后程序运行时需要分配较大对象时,无法找到足够的连续内存,而不得已再次触发GC。

复制(Copy)

  • 将内存按容量划分为两块,每次只使用其中一块。
  • 当这一块内存用完了,就将存活的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。
  • 这样使得每次都是对半个内存区回收,也不用考虑内存碎片问题,简单高效。
  • 缺点需要两倍的内存空间

标记-整理(Mark-Compact)

  • 也分为两个阶段,首先标记可回收的对象,再将存活的对象都向一端移动,然后清理掉边界以外的内存。
  • 此方法避免标记-清除算法的碎片问题,同时也避免了复制算法的空间问题。

分代收集算法

  • 一般年轻代中执行GC后,会有少量的对象存活,就会选用复制算法,只要付出少量的存活对象复制成本就可以完成收集。
  • 而老年代中因为对象存活率高,没有额外过多内存空间分配,就需要使用标记-清理或者标记-整理算法来进行回收。

年轻代

a) 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

b) 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

c) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。

d) 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。

老年代

a) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

b) 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

持久代

用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

你可能感兴趣的:(JVM,java,jvm)