【Java】JVM

一、介绍

1.什么是JVM?

  1. JVM是一种用于计算设备的规范,它是一个虚构出来的机器,是通过在实际的计算机上仿真模拟各种功能实现的。
  2. JVM包含一套字节码指令集,一组寄存器,一个栈,一个垃圾回收堆和一个存储方法域。
  3. JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

在这里插入图片描述

JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。

2.JDK、JRE、JVM是什么关系?

  1. JDK(Java Development Kit),Java 语言的软件开发工具包,是开发者用来编译、调试程序用的开发包。JDK是整个java开发的核心,它包含了JAVA的运行环境(JVM+Java系统类库)和JAVA工具。JDK也是Java程序需要在JRE上运行。

JDK包含的基本组件包括

  • javac – 编译器,将源程序转成字节码
  • jar – 打包工具,将相关的类文件打包成一个文件
  • javadoc – 文档生成器,从源码注释中提取文档
  • jdb – debugger,查错工具
  • java – 运行编译后的java程序(.class后缀的)
  • appletviewer:小程序浏览器,一种执行HTML文件上的Java小程序的Java浏览器。
  • Javah:产生可以调用Java过程的C过程,或建立能被Java程序调用的C过程的头文件。
  • Javap:Java反汇编器,显示编译类文件中的可访问功能和数据,同时显示字节代码含义。
  • Jconsole: Java进行系统调试和监控的工具
  1. JRE(Java Runtime Environment),也就是Java平台。所有的Java程序都要在JRE环境下才能运行。

JRE包括

  • 一个Java虚拟机(Java Virtual Machine,JVM)
  • 一些标准的类别函数库(Class Library
  1. JVM(Java Virtual Machine),是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

在这里插入图片描述

  • JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。
  • Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。

3.JVM原理

  1. JVM是Java的核心和基础,在Java编译器和OS平台之间的虚拟处理器,可在上面执行字节码程序。
  2. Java编译器只要面向JVM,生成JVM能理解的字节码文件。Java源文件经编译成字节码程序,通过JVM将每条指令翻译成不同的机器码,通过特定平台运行。

在这里插入图片描述

  • 线程私有区域:Java栈、本地方法栈、程序计数器;
  • 线程私有区域不会有垃圾回收;
  • 方法区属于特殊的堆;
  • JVM调优几乎都是调整堆;

4.JVM体系结构:

  • 类加载器:加载class文件;
  • 运行时数据区:包括方法区、堆、Java栈、程序计数器、本地方法栈
  • 执行引擎:执行字节码或者执行本地方法

在这里插入图片描述

二、运行时数据区

  • 方法区:属于共享内存区域,存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等数据。运行时常量池存在方法区中。

  • :Java虚拟机所管理的内存中最大的一块,一个JVM只有一个堆,堆的大小可以调节,唯一的目的是存放对象实例。由于是垃圾收集器管理的主要区域,因此有时候也被称作GC堆。

  • :用于描述Java方法执行的模型。每个方法在执行的同时都会创建一个栈帧,用于存储8大数据类型对象的引用局部变量表操作数栈动态链接方法出口等信息。每一个方法从调用至执行完成,对应于一个栈帧在虚拟机栈中从入栈到出栈。

  • 程序计数器:当前线程所执行字节码的行号指示器。每一个线程都有一个独立的程序计数器,线程的阻塞、恢复、挂起等一系列操作都需要程序计数器的参与,因此必须是线程私有的。

  • 本地方法栈:与虚拟机栈作用相似,只不过虚拟机栈为执行Java方法服务,而本地方法栈为执行Native方法服务,比如在Java中调用C/C++

三、类加载机制

类加载器通过一个类的全限定名来获取描述此类的二进制文件流的代码模块。

1. 类的生命周期(7个)

加载、验证、准备、解析、初始化、使用、卸载

2. 类加载的五个过程

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、装换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。

  • 加载:类加载器获取二进制字节流,将静态存储结构转化为方法区的运行时数据结构,并生成此类的Class对象,即把字节码通过二进制的方式转化到方法区中的运行数据区。

  • 验证:验证文件格式、元数据、字节码、符号引用,确保Class的字节流中包含的信息符合当前虚拟机的要求。

  • 准备:为类变量分配内存并设置其初始值,这些变量使用的内存都将在方法区中进行分配。

  • 解析:将常量池内的符号引用替换为直接引用,包括类或接口的解析、字段解析、类方法解析、接口方法解析。

  • 初始化:前面过程都是以虚拟机主导,而初始化阶段开始执行类中的 Java 代码。

3. 类加载器

  • 启动类加载器(BootStrap ClassLoader):主要负责加载jre/lib/rt.jar相关的字节码文件的。

  • 扩展类加载器(Extension Class Loader):主要负载加载 jre/lib/ext/*.jar 这些jar包的。

  • 应用程序类加载器(Application Class Loader):主要负责加载用户自定义的类以及classpath环境变量所配置的jar包的。

  • 自定义类加载器(User Class Loader):负责加载程序员指定的特殊目录下的字节码文件的。大多数情况下,自定义类加载器只需要继承ClassLoader这个抽象类,重写findClass()和loadClass()两个方法即可。

4. 类加载机制(双亲委派)

类的加载是通过双亲委派模型来完成的,双亲委派模型即为下图所示的类加载器之间的层次关系。

在这里插入图片描述

  • 工作过程:如果一个类加载器接收到类加载的请求,它会先把这个请求委派给父加载器去完成,只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己去加载。可以得知,所有的加载请求最终都会传送到启动类加载器中。

5. JVM类初始化顺序

  1. 父类静态代码块和静态成员变量
  2. 子类静态代码块和静态成员变量
  3. 父类代码块和普通成员变量
  4. 父类构造方法
  5. 子类代码块和普成员变量
  6. 子类构造方法

6. 对象的创建过程

  1. 检查:类是否已被加载,没有加载就先加载类

  2. 分配内存空间:使用 new 关键字在堆内存中分配一块空间,使用CAS方式分配,防止在为A分配内存时,执行当前地址的指针还没有来得及修改,对象B就拿来分配内存。

  3. 初始化对象头:在分配的内存空间中,Java 虚拟机会为对象头分配一定的空间,用于存储对象的元数据信息,如对象的类型、哈希码、GC 信息等。

  4. 执行构造方法:在对象头初始化完成后,Java 虚拟机会调用对象的构造方法,对对象进行初始化,包括成员变量的赋值、方法的调用等。

  5. 返回对象引用:构造方法执行完毕后,会返回对象的引用,该引用可以被赋值给变量,或者作为参数传递给其他方法。

7. 对象头中有哪些信息

对象头中有两部分:

  • 一部分是MarkWork,存储对象运行时的数据,如对象的hashcode、GC分代年龄、GC标记、锁的状态、获取到锁的线程ID等;
  • 另外一部分是表明对象所属类,如果是数组,还有一个部分存放数组长度。

四、垃圾回收

GC(Garbage Collection)垃圾收集,回收垃圾,释放内存。

程序计数器、虚拟机栈、本地方法栈是线程私有的,所以会随着线程结束而消亡。 Java 堆和方法区是线程共享的,在程序处于运行期才知道哪些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收所关注的就是这部分内存。

GC的目的实现内存的自动释放,使用可达性分析法判断对象是否可回收,采用了分代回收思想,将堆分为新生代、老年代,新生代中采用复制算法,老年代采用整理算法,当新生代内存不足时会minorGC,老年代不足时会fullGC

1、判断对象已死

在进行内存回收之前要做的事情就是判断那些对象是‘死’的,哪些是‘活’的。

引用计数法
给对象中添加一个引用计数器,当一个地方引用了对象,计数加1;当引用失效,计数器减1;当计数器为0表示该对象已死、可回收;

注意:如果不下小心直接把 Obj1-reference 和 Obj2-reference 置 null。则在 Java 堆当中的两块内存依然保持着互相引用无法回收。引用计数法很难解决循环引用问题;

可达性分析
通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。

可作为 GC Roots 的对象

  1. 虚拟机栈中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中native方法引用的对象

引用:下面四种引用强度依次减弱

  • 强引用:默认情况下,对象采用的均为强引用(new的对象)。哪怕内存溢出也不会回收
  • 软引用:SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收(只有内存不足时才会回收)。
  • 弱引用:WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
  • 虚引用:PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

什么情况下会内存溢出

  • 堆内存溢出:
  1. 当对象一直创建而不被回收时
  2. 加载的类越来越多时
  3. 虚拟机栈的线程越来越多时
  • 栈溢出:方法调用次数过多,一般是递归不当造成

2、垃圾收集算法

  • 标记清除算法:先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
  • 复制算法:将可用内存分为大小相等的两块,每次只使用其中一块,当这一块内存用完了,就将存活的对象复制到另一块,最后- 将此块内存一次性清理掉。
  • 标记整理算法:先标记所有需要回收的对象,然后让所有存活的对象向一端移动,最后直接清理掉边界以外的另一端内存。
  • 分代收集算法:把Java堆分为新生代和老年代。新生代中只有少量对象会存活,就选用复制算法;老年代中对象存活率较高,选用标记清除算法。

3、垃圾收集器

  • Serial收集器:单线程收集器。收集垃圾时必须暂停其他所有工作线程,直到它收集结束。
  • Parnew收集器:Serial收集器多线程版本。
  • Parallel Scavenge收集器:使用复制算法的新生代收集器。
  • Serial Old收集器:使用标记-整理算法的老年代单线程收集器。
  • Parallel Old收集器:使用标记-整理算法的老年代多线程收集器。
  • CMS收集器:基于标记-清除算法的低停顿并发收集器。运作步骤为①初始标记②并发标记③重新标记④并发清除。
  • G1收集器:最前沿的面向服务端应用的垃圾收集器。运作步骤为①初始标记②并发标记③最终标记④筛选回收。

CMS:收集器有以下特点

  1. 以最小的停顿时间为目标,优先降低GC停顿时间(相对地降低吞吐量)。
  2. 只运行在老年代的垃圾回收器。
  3. 使用标记-清除算法,可以并发收集。

G1收集器有以下特点

  1. 并行与并发:无需停顿Java线程来执行GC动作。
  2. 分代收集:可独立管理整个GC堆。
  3. 空间整合:运行期间不会产生内存空间碎片。
  4. 可预测的停顿:除了低停顿,还能建立可预测的停顿时间模型。

4、JVM内存分代机制

方法区即被称为永久代,而堆中存放的是对象实例,为了回收的时候对不同的对象采用不同的方法,又将堆分为新生代和老年代,默认情况下新生代占堆的1/3,老年代占堆的2/3。
在这里插入图片描述

注意:逻辑上存在,物理上不存在

新生代(Young)
HotSpot将新生代划分为三块,一块较大的Eden(伊甸园)空间和两块较小的Survivor(幸存)空间,默认比例为8:1:1。

老年代(Old)
在新生代中经历了多次GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。

永久代(Permanent):永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,一般而言不会进行垃圾回收。

元空间(metaspace)
从JDK 8开始,Java开始使用元空间取代永久代,元空间并不在虚拟机中,而是直接使用本地内存。那么,默认情况下,元空间的大小仅受本地内存限制。当然,也可以对元空间的大小手动的配置。

GC 过程
新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

  • GC开始时,对象只会存在于Eden区和Survivor From区,Survivor To区是空的(作为保留区域)。
  • GC进行时,Eden区中所有存活的对象都会被复制到Survivor To区,而在Survivor From区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的Header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到Survivor To区。接着清空Eden区和Survivor From区,新生代中存活的对象都在Survivor To区。
  • 接着,Survivor From区和Survivor To区会交换它们的角色,也就是新的Survivor To区就是上次GC清空的Survivor From区,新的Survivor From区就是上次GC的Survivor To区,总之,不管怎样都会保证Survivor To区在一轮GC后是空的。
  • GC时当Survivor To区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。

5、Minor GC、Major GC、Full GC之间的区别

Minor GC
Minor GC指新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作,当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,虽然它会触发stop-the-world,但是它的回收速度很快。

Major GC
指发生在老年代的垃圾收集动作,出现了 Major GC,经常会伴随至少一次 Minor GC(非绝对),MajorGC 的速度一般会比 Minor GC 慢10倍以上。

Full GC
Full GC是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。Full GC不等于Major GC,也不等于Minor GC+Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。

6、Minor GC、Major GC、Full GC触发条件

Minor GC触发条件
虚拟机在进行minorGC之前会判断老年代最大的可用连续空间是否大于新生代的所有对象总空间

  1. 如果大于的话,直接执行minorGC
  2. 如果小于,判断是否开启HandlerPromotionFailure,没有开启直接FullGC
  3. 如果开启了HanlerPromotionFailure, JVM会判断老年代的最大连续内存空间是否大于历次晋升(晋级老年代对象的平均大小)平均值的大小,如果小于直接执行FullGC
  4. 如果大于的话,执行minorGC

Full GC触发条件

  1. 老年代空间不足:如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存在老年代当中,如果老年代空间也不足,就会触发Full GC。为了避免这种情况,最好就是不要创建太大的对象。
  2. 方法区空间不足:系统当中需要加载的类,调用的方法很多,同时方法区当中没有足够的空间,就出触发一次Full GC
  3. 老年代最大可用连续空间小于Minor GC历次晋升到老年代对象的平均大小
  4. 调用System.gc()时(系统建议执行Full GC,但是不必然执行)

五、JVM监控和调优

工具:

  • Eclipse里面有 Eclipse Memory Analyzer tool(MAT)插件可以测试
  • Idea中也有这么一个插件,就是JProfiler

命令:

-Xms 设置初始化内存分配大小,默认 1/64
-Xmx 设置最大分配内存,默认 1/4
-XX:+PrintGCDetails 打印GC垃圾回收信息
-XX:+HeapDumpOnOutOfMoryError OOM DUMP

-Xmx[]:堆空间最大内存

-Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的

-Xmn[]:新生代的最大内存

-xx:[survivorRatio=3]:eden区与from+to区的比例为31,默认为41

-xx[use 垃圾回收器名称]:指定垃圾回收器

-xss:设置单个线程栈大小

一般设堆空间为最大可用物理地址的百分之80

版权声明:本文为qq_44544908原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>