JVM基础知识总结

一、为什么要学习JVM

Java采用了自动的内存管理方式,程序员实际上不用关心内存管理的细节,那为什么我们仍然需要了解Java内存管理的内幕?原因如下:
1、了解Java内存管理的细节,有助于程序员编写出性能更好的程序。
2、一旦内存管理出现问题,有助于找到问题的根本原因所在。
3、了解Java内存管理的内幕,进行JVM性能调优,从而使得自己的应用获得最佳的性能体验。

如果说Spring是Java届里面像“降龙十八掌”这样的绝妙招式,那么JVM便是像“北冥神功”这样的内功心法。了解JVM的重要性不言而喻。

以往的经验告诉我,学习一门新的知识,需要根据自己目前的水平和工作来衡量,对于一些深奥的细节可以跳过,以免舍本逐末,事倍功半。参与到JVM实现的人肯定是少数,当然目前也不包括我。于是我就抱着“门外看”的态度浅尝辄止地了解JVM的相关知识,毕竟先有知识,才有把知识内化成技能的根基。

二、类的加载机制

1、类的加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

2、类的生命周期

JVM基础知识总结_第1张图片
1.jpg

1.加载:类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口

2.连接:连接又包含三块内容:验证、准备、初始化。1)验证,确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;2)准备,为类的静态变量分配内存,并将其初始化为默认值;3)解析,把类中的符号引用转换为直接引用(引用的目标必定已在内存中存在)

3.初始化:类加载最后一个阶段,为类的静态变量赋予正确的初始值

4.使用:new出对象程序中使用

5.卸载:执行垃圾回收

3、类加载器

JVM基础知识总结_第2张图片
2.jpg

1.启动类加载器(Bootstrap ClassLoader)
负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库

2.扩展类加载器(Extension ClassLoader)
该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

3.应用程序类加载器(Application ClassLoader)
该类加载器由
sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器

4、双亲委派模型

双亲委派模型:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。简单说就是“让我爸先处理,我爸搞不定我再出马”。

JVM通过双亲委派模型进行类的加载,好处是保证使用不同的类加载器最终得到的都是同样一个Object对象。当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。

5、类加载机制

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

2)父类委托
先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。

3)缓存机制
缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。

虚拟机规范严格规定了有且只有以下五种情况(jdk1.7)必须对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)

  1. 遇到new,getstatic,putstatic,invokestatic这失调字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:
    • 使用new关键字实例化对象
    • 读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)
    • 调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

以下情况不会触发初始化:

  1. 通过数组定义来引用类,不会触发此类的初始化
  2. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化:

三、JVM内存结构

JVM基础知识总结_第3张图片
3.jpg

1、程序计数器

程序计数器(Program Counter Register) 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条执行字节码指令。每条线程都有一个独立的程序计数器。

如果执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址。如果是native方法,计数器为空。此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

注:一个Native Method就是一个java调用非java代码的接口

2、虚拟机栈(JVM Stacks)

同样是线程私有,描述Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法对应一个栈帧。
局部变量表存放了各种基本类型、对象引用和returnAddress类型(指向了一条字节码指令地址)。其中64位长度long 和 double占两个局部变量空间,其他只占一个。

规定的异常情况有两种:
1.线程请求的栈的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
2.如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就抛出OutOfMemoryError异常。

3.本地方法栈(Native Method Stacks)

和Java虚拟机栈很类似,不同的是本地方法栈为Native方法服务。

4.Java堆(Heap)

Java虚拟机所管理的内存中最大的一块。由所有线程共享,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。

堆中可细分为新生代和老年代,再细分可分为Eden空间、From Survivor空间、To Survivor空间。默认情况下年轻代按照8:1:1的比例来分配。

当堆无法扩展时,抛出OutOfMemoryError异常

5.方法区(Method Area)

又叫“非堆”(Non-Heap),所有线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
当方法区无法满足内存分配需求时,抛出OutOfMemoryError
运行时常量池:方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池(Const Pool Table),用于存放编译期生成的各种字面量和符号引用。并非预置入Class文件中常量池的内容才进入方法运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。
当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。

6. 更高维度再看JVM与系统调用的关系

JVM基础知识总结_第4张图片
4.jpg

方法区和堆是所有线程共享的内存区域;而虚拟机栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。

四、垃圾回收与内存分配

1、对象死亡判断(What:哪些是垃圾)

JVM基础知识总结_第5张图片
5.jpg

Java采用可达性分析算法来判断对象是否已经死亡,即可回收。基本的思路是”GC Roots“的对象作为起始点,从这些节点向下搜索,当从GC Roots到某个对象不可达时证明此对象不可用。

在Java语言中可作为GC Roots对象包括:
• 虚拟机栈中引用的对象
• 方法区中类静态属性引用的对象
• 方法区中常量引用的对象
• Native方法引用的对象

即使在可达性分析算法中不可达的对象也并非是“非死不可“的,此时它们处于”缓刑“阶段。宣告一个对象的死亡的过程如下:

  1. 对象没有与GC Roots相连接的引用链(第一次标记)
  2. 没必要执行finalize()方法 => 回收(第二次标记)
  3. 有必要执行finalize()方法 => 放置到F-Queue队列,由Finalizer线程执行,不承诺等待运行结束
  4. 如果对象重新与引用链上的任何一个对象建立关联,则移出“即将回收”集合,否则回收(第二次标记)

类无用的条件(同时满足以下3个条件)

  1. 类的所有实例都已被回收
  2. 加载该类的ClassLoader已被回收
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
    以上只是说明“可以回收”,但并不是和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机将提供一些参数进行控制。

2、垃圾收集(GC)算法(How:如何收集)

我们常用的垃圾回收器一般都采用分代收集算法。

  1. 标记-清除算法
    定义:标记,统一回收
    缺点:效率不高,产生大量不连续的内存碎片

2.复制算法

定义:安容量划分大小相等两块,每次只使用其中一块,当一块内存用完了将还村花的对象复制到另一块上,把已使用过的内存空间一次清理掉
优点:解决标记-清除算法的效率问题
缺点:内存缩小,代价太大
优化:不按照1:1比例划分内存,当担保空间不够用时依赖其他内存分配担保

3.标记-整理算法

又叫“标记-压缩算法”,标记过程仍然与“标记-清除”算法一样,但不直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

4.分代收集算法

根据对象存活周期的不同将内存划分为几块,“新生代”采用复制算法,“老年代”采用“标记-清理”或“标记-整理”算法

3、垃圾回收器(Who:谁来收集这些垃圾)

JVM基础知识总结_第6张图片
6.jpg

1.Serial收集器

串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收,垃圾收集时必须暂停其他所有的工作线程直至收集结束。Client模式下很好的选择。

2.ParNew收集器

“多线程版”的Serial收集器。Server模式下很好的选择,能与CMS收集器配合工作

3.Parallel Scavenger收集器

“高吞吐量版”的ParNew收集器

4.Serial Old收集器

Serial收集器的老年代版本,使用“标记-整理”算法,给Client模式下的虚拟机使用

5.Parallel Old收集器

Parallel Scavenger收集器的老年代版本,使用多线程和“标记-整理”算法,使“吞吐量优先”收集器有名副其实的应用组合,在注重吞吐量和CPU资源敏感的场合可以优先考虑Parallel Scavenger + Parallel Old

6.CMS(Concurrent Mark Sweep)收集器

以获取最短回收停顿时间为目标,符合互联网站或B/S系统的服务端,基于“标记-清除”算法

7.Gl收集器

面向服务端应用,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征,被视为JDK7中HotSpot虚拟机的一个重要进化特征。

4、内存分配规则*

简单来说,对象内存分配主要是在堆中分配。但是分配的规则并不是固定的,取决于使用的收集器组合以及JVM内存相关参数的设定。
几条最普遍的内存分配规则:

  1. 对象优先分配在Eden区
  2. 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)
  3. 长期存活的对象进入老年代。
  4. 动态判断对象的年龄。
  5. 空间分配担保。

你可能感兴趣的:(JVM基础知识总结)