Java笔记 ·

JVM基础小结

前言

本篇仅是一个粗略了解JVM的小结,时间有限不能深入展开,留待日后逐一了解。

主要结构

主要结构图

JVM主要包括四个部分:

  • 1、类加载器(Class Loader)
  • 2、执行引擎
  • 3、内存区(也称运行时数据区),内存区又包含:

(1)方法区(Method Area)

(2)堆(HEAP)

(3)Java虚拟机栈(Java VM Stack)

(4)程序计数器(Program Counter Register,亦简称PC Register)

(5)本地方法栈(Native Method Stack)

  • 4、本地方法接口

在内存区,方法区和堆是所有Java线程共享的,而Java虚拟机栈、本地方法栈、PC寄存器则由每个线程私有

类加载器(ClassLoader)

类加载器负责加载编译好的.class字节码文件,并装入内存,使JAVM可以实例化或以其它方式使用加载后的类。

JVM的类加载器支持在运行时动态加载。动态加载的好处如:

  • a.节省内存空间;
  • b.灵活的从网络上加载类;
  • c.可以通过命令空间的分隔来实现类的分离,增强系统安全性。

1、ClassLoader的分类:

a.启动类加载器(Bootstrap ClassLoader):负责将放在<JAVA_HOME>\lib目录中的,或被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,直接使用null代替即可。

Java的核心类都是由该ClassLoader加载。在Sun JDK中,这个类加载器是由C++实现的,并且在Java语言中无法获得它的引用。

b.扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launche $ExtClassLoader 实现,负责加载<JAVA_HOME>\lib\ext目录中的,或是被java,ext.dirs系统变量(-Djava.ext.dirs)所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

c.系统类加载器(System ClassLoader):应用程序类加载器(Application ClassLoader)由sun.misc.Launche $App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也成它为系统类加载器。负责加载用户类路径(Class-Path)上所指定的类库,开发者可以直接使用这个加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这就是程序中默认的类加载器。

d.用户自定义类加载器(User-Defined ClassLoader & User ClassLoader):由用户自定义类的加载规则,可以手动控制加载过程中的步骤。

2、ClassLoader的工作原理

类加载分为装载、链接、初始化三步。

a.装载

通过类的全限定名和ClassLoader加载类,主要是将指定的.class文件加载至JVM。当类被加载以后,在JVM内部就以“类的全限定名+ClassLoader实例ID”来标明类。

在内存中,ClassLoader实例和类的实例都位于堆中,它们的类信息都位于方法区。

b.链接

链接的任务是把二进制的类型信息合并到JVM运行时状态中去。

链接分为以下三步:

  • a.验证:校验.class文件的正确性,确保该文件是符合规范定义的,并且适合当前JVM使用。
  • b.准备:为类分配内存,同时初始化类中的静态变量赋值为默认值。
  • c.解析(可选):主要是把类的常量池中的符号引用替换为直接引用,这一步可以在用到相应的引用时再解析。
c.初始化

初始化类中的静态变量,并执行类中的static代码、构造函数。

JVM规范严格定义了何时需要对类进行初始化:

  • a、通过new关键字、反射、clone、反序列化机制实例化对象时。
  • b、调用类的静态方法时。
  • c、使用类的静态字段或对其赋值时。
  • d、通过反射调用类的方法时。
  • e、初始化该类的子类时(初始化子类前其父类必须已经被初始化)。
  • f、JVM启动时被标记为启动类的类(简单理解为具有main方法的类)。

执行引擎

负责执行class文件中包含的字节码指令(执行引擎的工作机制,这里也不细说了,这里主要介绍JVM结构);

内存区(也叫运行时数据区)

是在JVM运行的时候操作所分配的内存区。

(1)方法区(Method Area)

类型信息和类的静态变量都存储在方法区中。方法区中对于每个类存储了以下数据:

  • a.类及其父类的全限定名(java.lang.Object没有父类)
  • b.类的类型(Class or Interface)
  • c.访问修饰符(public, abstract, final)
  • d.实现的接口的全限定名的列表
  • e.常量池
  • f.字段信息
  • g.方法信息
  • h.静态变量
  • i.ClassLoader引用
  • j.Class引用
    可见类的所有信息都存储在方法区中。由于方法区是所有线程共享的,所以必须保证线程安全。

(2)堆(HEAP)

堆用于存储对象实例以及数组值。

堆中有指向类数据的指针,该指针指向了方法区中对应的类型信息。

堆中还可能存放了指向方法表的指针。

堆是所有线程共享的,所以在进行实例化对象等操作时,需要解决同步问题。

此外,堆中的实例数据中还包含了对象锁,并且针对不同的垃圾收集策略,可能存放了引用计数或清扫标记等数据。

在堆的管理上,Sun JDK从1.2版本开始引入了分代管理的方式。主要分为新生代、旧生代。分代方式大大改善了垃圾收集的效率。

存储java实例或者对象的地方。这块是GC的主要区域。从存储的内容我们可以很容易知道,方法区和堆是被所有java线程共享的。

(3)Java虚拟机栈(Java VM Stack)

Java栈由栈帧组成,一个帧对应一个方法调用。

调用方法时压入栈帧,方法返回时弹出栈帧并抛弃。

Java栈的主要任务是存储方法参数、局部变量、中间运算结果,并且提供部分其它模块工作需要的数据。

Java栈是线程私有的,这就保证了线程安全性,使得程序员无需考虑栈同步访问的问题,只有线程本身可以访问它自己的局部变量区。

它分为三部分:局部变量区、操作数栈、帧数据区。

java栈总是和线程关联在一起,每当创建一个线程时,JVM就会为这个线程创建一个对应的java栈。在这个java栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。所以java栈是现成私有的。

(4)程序计数器(Program Counter Register,亦简称PC Register)

用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。

类似于PC寄存器,是一块较小的内存区域,通过程序计数器中的值寻找要执行的指令的字节码,由于多线程间切换时要恢复每一个线程的当前执行位置,所以每个线程都有自己的程序计算器。

这一个区域不会有OutOfMemeryError。当执行Java方法时,这里存储的执行的指令的地址,如果执行的是本地方法,这里的值是Undefined。

(5)本地方法栈(Native Method Stack)

本地方法栈类似于Java栈,主要存储了本地方法调用的状态。在Sun JDK中,本地方法栈和Java栈是同一个。

本地方法接口

主要是调用C或C++实现的本地方法及返回结果。

内存分配

java内存申请一般有两种:静态内存动态内存

编译时就能够确定的内存就是静态内存,即内存是固定的系统一次性分配,比如int类型变量;

动态内存分配就是在程序执行时才知道要分配的存储空间大小,比如java对象的内存空间。

java栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭,栈中的栈帧随着方法的结束也会撤销,内存自然就跟着回收了。所以这几个区域的内存分配与回收是确定的,我们不需要管的。

但是java堆和方法区则不一样,我们只有在程序运行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态的。一般我们所说的垃圾回收也是针对的这一部分。

总之Stack的内存管理是顺序分配的,而且定长,不存在内存回收问题;

而Heap 则是为java对象的实例随机分配内存,不定长度,所以存在内存分配和回收的问题。

垃圾检测、回收算法

垃圾收集器一般必须完成两件事:检测出垃圾;

垃圾检测方法:

  • 引用计数法:给一个对象添加引用计数器,每当有个地方引用它,计数器就加1;引用失效就减1。

但,如果我有两个对象A和B,互相引用,除此之外,没有其他任何对象引用它们,实际上这两个对象已经无法访问,即是我们说的垃圾对象。但是互相引用,计数不为0,导致无法回收。

  • 可达性分析算法:以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。这里的根集一般包括java栈中引用的对象、方法区常良池中引用的对象、本地方法中引用的对象等。

总之,JVM在做垃圾回收的时候,会检查堆中的所有对象是否会被这些根集对象引用,不能够被引用的对象就会被垃圾收集器回收。

回收算法

1、标记-清除(Mark-sweep)

算法和名字一样,分为两个阶段:标记和清除标记所有需要回收的对象,然后统一回收。这是最基础的算法,后续的收集算法都是基于这个算法扩展的。

不足:效率低;标记清除之后会产生大量碎片。

2、复制(Copying)

此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。

当这一块用完了,就将还存活着的对象复制到另外一块上,然后再把已经使用过的内存空间清理掉。

这样使得每次都是对整半个区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。

当然,此算法的代价也是很明显的,就是将内存缩小为了原来的一半

现在的商业虚拟机都采用这种收集算法来回收新生代

3、标记-整理(Mark-Compact)

此算法结合了“标记-清除”和“复制”两个算法的优点。

也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

4、分代收集算法(Generational Collection)

当前商业虚拟机的垃圾收集都采用该算法。

根据对象的存活周期的不同将内存划分为几块。根据各年代的特点采用最适当的收集算法。

新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理” 算法来进行回收。

参考资料

浅析Java虚拟机结构与机制

JVM结构、GC工作机制详解

JVM虚拟机结构

《深入理解Java虚拟机:JVM高级性能与最佳实践》(第2版)

额外收集

深入理解java虚拟机 精华总结(面试)

参与评论