浅谈Java虚拟机

本博客基于《深入理解Java虚拟机——JVM高级特性与最佳实践》——周志明

前言

终于拿到了众多知乎大佬推荐的JVM神书,可以开始学习Java虚拟机的相关知识了,以前学Java的时候一直都听到很多相关词汇,GC、HotSpot、新生代、老年代、堆、栈、双亲委派模型,但一直没有机会去认真看一看JVM相关的书籍,这段时间实习,正好可以好好看看JVM的书,也将学到的知识总结于此,由于博主只是一名大三的学生,有不对的地方欢迎指出。

Java内存区域

运行时数据区域

Java虚拟机在执行Java程序的过程中会将它所管理的内存划分为若干个不同的数据区域,每个区域都有不同的功能。

程序计数器

程序计数器是当前线程所执行的字节码的行号指示器,是一块比较小的内存空间,总的来说就是指示代码的执行位置的。每一个线程都会分配一个程序计数器,因此这类内存区域被称为“线程私有”或“线程独立”的内存。

Java虚拟机栈

Java虚拟机栈也是“线程私有”的内存,其描述的是Java方法执行的内存模型,每当有一个方法开始执行时就会创建一个栈帧来存储局部变量表、操作数栈、动态链接、方法出口等信息。

本地方法栈

与Java虚拟机栈非常类似,Java虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用到的Native方法服务。

Java堆

虚拟机管理的内存中最大的一块,堆是“线程共享”的内存区域,即所有线程都能使用这块内存,堆的唯一目的就是存放对象的实例(而非对象的引用)。堆是垃圾收集器GC管理的主要区域,根据对象的存活时间堆还可以被划分为新生代老年代,新生代又被划分为Eden、From Survior和To Suvivor三块区域,这些之后在GC算法的时候会细讲。

方法区

方法区也是“线程共享”的内存区域,它用于存储已被虚拟机加载的类信息、静态变量、常量、即时编译器编译后的代码等数据。由于GC在回收内存时不经常回收方法区的内存,因此方法区又被称为永久代。但是不经常回收不代表不回收,方法区的内存回收目标主要是针对常量池的回收和对类型的卸载。

运行时常量池

运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量符号引用

对象的创建

我们时常会疑惑,当我们new了一个类后Java就自动帮我们实例化了一个类的对象出来,这个new的过程到底是怎样的?这就要和我们上面学到的运行时数据区域的知识有关了。

虚拟机遇到一条new指令时,会先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。如果没有,那么就会执行类的加载过程,关于类加载过程之后会提到。

类加载完成后,虚拟机就要为新创建的对象分配内存空间,分配内存空间有两种方式,一种是“指针碰撞”,一种是“空闲列表”。

内存分配完毕后,虚拟机需要将分配到的内存空间都初始化零值。这样就可以保证对象的成员变量可以在不赋与其初始值的情况下直接被调用(但是方法内部的局部变量是必须要手动初始化的,不要混淆),程序可以访问到这些成员变量的数据类型对应的零值(例如 int age默认为0)。

接着虚拟机会对创建的对象进行一系列必要的设置,例如这个对象是哪个类的实例,这些信息会被放在对象头之中。

从虚拟机的角度来看,一个创建对象的过程是完成了,但在整个Java程序上来说,对象的init初始化方法还没被执行,只有执行了对象的初始化方法,按照程序员的意愿进行初始化后,一个真正完整的对象才算被完全创建。

垃圾收集器(GC)与内存分配策略

我们谈到的所有的GC回收都是针对堆和方法区,其他三个运行时数据区域都是线程隔离的,在线程结束后会被自动回收,因此没什么好说的。

判断对象是否死亡

回收对象的一个前提就是需要知道这个对象是否已经死亡?只有死亡的对象才有被回收的价值,回收一个活着的对象会导致Java程序出现严重的错误。

引用计数算法

给对象中添加一个引用计数器,每当对象被引用时,计数器就加一,引用失效计数器就减一,当计数器为0的时候说明这个对象没有被任何地方引用,因此这个对象就处于一种死亡的状态,可以被GC回收,这就是一种比较古老的用来判断对象是否死亡的算法——引用计数算法。

但是这种算法有一个缺陷,它无法处理对象的互相引用:

Object a = new Object();
Object b = new Object();
a.instance = b;
b.instance = a;
a = null;

在上面代码中,对象a引用了b,对象b又引用了a,因此a和b的引用计数器的值都为1。但除此之外,这两个对象再没有任何引用,这两个对象也再也不可能被访问,因此应该被GC回收,但是因为这两个对象相互引用这对象,导致引用计数器的值都不为0,因此GC就不能判断这两个对象为“死亡”状态,无法进行回收。

可达性分析算法

在Java虚拟机中判断一个对象是否死亡,使用的是可达性分析算法。这个算法的思路就是通过一些列的称为“GC Roots”的对象作为起始点,从这些起始点开始向下搜索,走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明该对象是死亡状态,可以被回收;若有引用链连接到GC Roots,则说明该对象是存活状态,不能被回收。

在Java中,可以作为GC Roots的对象有以下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. Native方法中引用的对象

引用的分类

强引用

强引用就是在代码之中普通存在的,使用new关键字实例化的对象关联的引用,下面的obj就是一种强引用:

Object obj = new Object();

只要强引用还在,GC就不会回收强引用关联的对象。

软引用

软引用是用来描述一些还有用但并非必需的对象。对于软引用关联的对象,在系统要发生OOM时,才会将这些对象列入回收范围进行二次回收,如果一直不发生OOM,那么软引用关联的对象就一直不会被回收

弱引用

弱引用的强度比软引用要弱一些,被弱引用关联的对象只能生存到下一次垃圾回收发生之前

虚引用

又称幽灵引用和幻影引用。一个对象是否有虚引用的存在完全不影响其生存时间,也无法通过虚引用获得对象实例,为对象设置虚引用的唯一目的就是能在这个对象被GC回收时收到系统通知。

拯救对象

即使是在可达性分析算法中不可达的对象,也并非一定会被GC回收,这些对象还有抢救的空间。要真正宣布一个对象进入死亡状态,可以被GC回收,要经历两次标记过程:可达性分析算法判定对象没有到GC Roots的引用链,这是第一次标记;该对象没有重写finalize()方法,或者finalize()方法已经被虚拟机执行过一次,对象会被第二次标记,这时对象才正式宣布进入死亡状态。

这就是说,如果对象重写了finalize方法,并在方法中将自己赋值给某个类变量或者对象的成员变量,建立了一个到GC Roots的引用链,那么这个对象就不会进入死亡状态,也就不会被GC回收。

需要注意的是,任何对象的finalize方法都只会被虚拟机执行一次,也就是说,任何对象只有一次被抢救的机会。

垃圾收集算法

标记-清除算法

标记清除算法是最基础的垃圾收集算法,其主要过程如下:

  1. 标记出要回收的对象
  2. 统一回收被标记过的对象

对象的标记就是基于引用计数法或可达性分析算法实现的。

标记清除算法有两个不足之处:

  1. 效率问题:标记、清除两个过程的效率均不高
  2. 空间问题:标记清除后会产生大量不连续的内存片段,不连续的内存片段过多的话会影响需要连续内存的较大对象的存储。

复制算法

为了解决效率的问题,复制算法出现了,复制算法是针对新生代的一种回收算法。这种算法将可用内存划分为大小相等的两块,每次只用其中的一块,当这一块内存用完后,就将还活着的对象复制到另外一块内存空间上,然后把已经使用过的内存空间全部清空。

这种算法有一个弊端就是舍弃了一半的内存空间,造成的内存浪费实在是太多了。经过IBM的研究发现,Java中98%的对象都是朝生夕死的,大部分对象的存活期都不会长,因此我们没有必要按照一比一的比例来划分内存,比较好的方式是将内存划分为一块较大的Eden区和两块较小的Suvivor区,每次只使用Eden和一块Survivor区(假设这个为Survivor一区)。在进行垃圾回收时,将Eden和Survivor中还存活的对象复制到剩下的那块Survivor区(假设这个为Survivor二区)中,然后清理刚才使用过的Eden和Survivor区。在HotSpot虚拟机中,默认的Eden和Survivor的大小比例是8 :1,也就是说新生代中的对象可以使用的空间为90%(80%+10%),只有10%的内存空间被浪费了。

但是我们没有办法保证每次回收时都只有不到10%的对象存活下来,因此当Survivor二区空间不够用时,需要依赖其他内存(老年代)进行分配担保。

所谓分配担保就是当Survivor二区没有足够的空间存放Eden和Survivor一区存活下来的对象时,这些对象将直接进入老年代。

标记-整理算法

复制算法针对的是对象存活率比较低的新生代区,老年代中的对象存活时间都比较长,因此并不适合在老年代区应用复制算法,在JVM中针对老年代的垃圾回收算法是标记整理算法。

标记整理算法的标记过程和标记清楚算法过程中的标记是一样的,但后续步骤不再是直接清除被标记的对象,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

针对对象存活时间较短的新生代区域应用复制算法,对于对象存活时间很长的老年代区域应用标记整理算法,这就是分代收集算法。

类加载机制

概述

什么是类加载机制?虚拟机将描述类的数据从Class二进制字节流文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是类加载机制。

类加载的时机

类从被加载到虚拟机内存,到其卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载,其中验证、准备、解析被统称为连接。

(未完待续)

你可能感兴趣的:(java)