【Java进阶】06-JVM简介

Java 虚拟机

Java 虚拟机是一台想象中的机器,在实际的计算机上通过软件模拟来实现。Java 有自己想象的硬件,如堆栈、处理器、寄存器、相应的指令系统等。
在这里插入图片描述
正是 Java 虚拟机才实现了 Java 的跨平台特性。通过在不同的系统上实现了相应版本的 JVM,可以然后将字节码运行在不同的 JVM 上,实现了一次编写到处运行,而不必在源代码层面改写。

JVM 的生命周期

  • 一个 JVM 执行一个 Java 程序。程序开始执行的时候它运行,程序结束的时候它就停止。而启动 JVM 的指令就是我们熟悉的 java [类名]
  • 每个 JVM 起始于一个 main() 方法,且这个 main() 方法是有固定格式的
    public static void main(String[] args)
  • main() 方法是程序的起点,它被执行的线程被初始化为程序的初始线程。程序中的其它线程都由这个线程来启动。Java 中的线程分为守护线程和普通线程两种。守护线程是 JVM 自己使用的线程,比如负责垃圾回收的线程。也可以把自己的程序设置为守护线程。包含 main() 方法的初始线程默认不是守护线程。
  • 只要 JVM 中还有普通线程在执行,JVM 就不会停止。守护线程不会影响程序的结束。如果有足够的权限,就可以调用 exit() 方法终止程序。

JVM 的体系结构

  • 在 JVM 的规范中定义了一系列的子系统、内存区域、数据类型和使用指南。这些组件构成了 JVM 的内部结构,他们不仅仅为 JVM 的实现提供了清晰的内部结构,更是严格规定了 JVM 实现的外部行为。
  • 每一个 JVM 都有一个类加载器子系统(class loader subsystem),负责加载程序中的类型(类 class 和接口 interface),并赋予唯一的名字。每一个 JVM 都有一个执行引擎(execution engine)负责执行被加载类中包含的指令。

JVM 中使用的数据类型

  • 所有 JVM 中使用的数据都有确定的数据类型,数据类型和操作都在 JVM 规范中严格定义。Java 中的数据类型分为原始数据类型(primitive types)和引用数据类型(reference type)。原始数据类型包括4类8种。整型——byte, short, int, long; 浮点类型——float,double;字符类型——char;布尔类型——boolean。
  • 在 JVM 中还存在一个Java语言中不能使用的原始数据类型----返回值类型(return value)。这种类型被用来实现Java程序中的“ finally classes”。
  • 引用类型可能被创建为:类类型(class type),接口类型(interface type),数组类型(array type)。他们都引用被动态创建的对象。当引用类型引用 null 时,说明没有引用任何对象。

JVM 的内存划分

在这里插入图片描述
每个 Java 进程都会划分如下几块内存区域。其中方法区、堆是线程共享的,Java程序栈、本地方法栈、程序计数器是线程私有的。

程序计数器(PC)

就和寄存器中的 PC 类似,这个利用程序的线性执行原理,用来记录程序当前运行到哪个地方。这个属于线程私有,不可共享,如果共享会导致计数混乱,无法准确的执行当前线程需要执行的语句。由于 PC 只是记录代码的位置,所以不会出现任何的 OutOfMemoryError 的情况。

虚拟机栈

  • 虚拟机栈就是指经常说到的栈内存。Java 中每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
  • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法电请到足够的内存,就会抛出 OutofMemoryError 异常。

本地方法栈

本地方法栈中用于运行本地的非 Java 代码程序。有些代码是由其他的语言写的,但是在 Java 程序运行期间,需要使用其中的代码,所以也需要单独安排一个栈来调用其代码的运行。

堆是 JVM 中内存最大的一块区域,是线程共享的。唯一的目的是存储对象实例,这也是垃圾收集器主要收集的区域。由于现代的垃圾收集器是采用分代收集算法,所以堆被划分为新生代和老年代区域。

在运行的时候可以通过在命令行加参数 -Xmx(JVM 最大可用内存)和 -Xms(JVM 初始内存)来调整堆内存,如果扩大到无法继续扩展时,会出现 OutOfMemoryError 的错误。

方法区

是线程共享的。用来存储类信息、常量、静态变量、class 文件。垃圾收集器也会对这一部分的区域进行回收,比如清理常量池和类型的卸载。

方法区内存不够的时候也会抛出 OutOfMemoryError 异常。

JVM 的类加载机制

类加载机制的概念

  • JVM 将类的数据从 class 文件加载到内存,并且对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。
  • 在 Java 语言里面,类型的加载和连接过程都是在程序运行期间完成的。而不需要在运行之前加载和连接。

类的生命周期

  • 加载 loading
  • 验证 verification
  • 准备 preparation
  • 解析 resolution
  • 初始化 initialization
  • 使用 using
  • 卸载 unloading

加载

  • 通过一个类的全限定名来获取此类的二进制字节码。
  • 将这个字节码所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在Java堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口。

验证

  • 虚拟机规范:验证输入的字节流是否符合 Class 文件的存储格式,否则抛出一个 java.lang.VerifyError 异常。
  • 文件格式验证:验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。经过这个阶段的验证,字节流进入内存的方法区中进行存储。
  • 元数据验证:对类的元数据信息进行语义校验,保证不存在不符合 Java 语言规范的元数据信息。
  • 字节码验证:进行数据流和控制流分析,对类的方法体进行校验分析, 保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
  • 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候(解析阶段),对常量池中的各种符号引用的信息进行匹配性的校验。

准备

准备阶段是正式为类变量分配内存并**设置类变量初始值(各数据类型的零值)**的阶段,这些内存将在方法区中进行分配。但是如果类字段的字段属性表中存在 ConstantValue 属性,那在准备阶段变量值就会初始化为 ConstantValue 属性指定的值。

常量值是我们之前说过的如 String、final关键字修饰、static 关键字修饰等都是 ConstantValue。

举个例子:public static final int value=122;

解析

  • 解析阶段是在虚拟机将常量池内的符号引用替换为直接引用的过程。(标识符到地址的过程)
  • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

初始化

  • <clinit>() 方法:由编译器自动收集类中所有类变量的赋值动作和静态语句块中语句合并产生,收集的顺序是由语句在源文件中出现的顺序决定的。
  • 该方法与实例构造器 <init>() 不同,不需要显示的调用父类构造器。
  • <clinit>() 方法对于类或接口来说不是必须的。
  • 执行接口的 <clinit>() 不需要先执行父接口的 <clinit>() 方法。
  • 虚拟机会保证一个类的 <clinit>() 方法在多线程环境中被正确的加锁和同步。

类的主动引用

  • 遇到new、getstatic、putstatic、invokestatic 这四条字节码指令时(使用new实例化对象、读取一个类的静态字段、设置一个类的静态字段、调用一个类的静态方法)。
  • 使用java.lang.reflet包的方法对类进行反射调用的时候。
  • 当初始化一个类的时候,如果发现其父类没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,虚拟机会初始化主类(包含 main 方法的那个类)。

类的被动引用

  • 通过子类引用父类的静态字段,不会导致子类初始化(对于静态字段,只有直接定义这个字段的类才会被初始化)。
  • 通过数组定义类应用类:ClassA [] array=new ClassA[10]。触发了一个名为 LClassA 的类的初始化,它是一个由虚拟机自动生成的、直接继承于 Object 的类,创建动作由字节码指令 newarray 触发。
  • 常量会在编译阶段存入调用类的常量池。

判断对象是否存活

java 中不需要像 c++ 一样显式地 delete,而是由 JVM 的垃圾回收机制(一个系统级的守护线程)帮助我们回收无用的内存。

垃圾回收:当一个对象没有引用指向它的时候,这个对象就会成为无用内存,就不进行回收,以便用于对后续对象进行内存分配。

引用计数算法

算法的思想很简单,就是记录指向每个对象的引用的数量。
这种算法的实现简单而且效率很高。但是其没有解决对象之间相互循环引用的问题。

Obj objA = objB
Obj ojbB = objA
这种情况好像有2个引用需要计数,但是其实是对象之间相互循环引用,A 的引用指向对象B,B 的引用指向对象A。这两个对象没有额外的引用,意为需要回收,但是其计数都不为0,所以引用计数算法就不会回收它们。

可达性分析算法(根搜索算法)

首先每个进程开始的时候就会创建一个名为 GCRoot 的根节点(Garbage Collection),程序将所有的引用关系看作是一张图,每创建一个引用就会连接到这张图上面。通过一个节点和 GCRoot 之间有没有引用链存在来判断时候作为垃圾回收的对象,如果有循环引用还可以通过判断有没有环路来判断是否垃圾回收。

在 Java 语言里,可作为 GC Roots 对象的包括如下几种:

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

对象引用-强引用

只要引用存在,垃圾回收器永远不会回收。

Object obj = new Object();

obj 对象对后面 new Object 有一个强引用, 只有当obj这个引用被释放之后,对象才会被释放掉。
如果我们想回收这个对象,可以设置这个引用指向 null。这样垃圾回收器发现该对象没有引用指向之后就会回收该对象。

对象引用-软引用

非必须引用,内存溢出之前进行回收,可以通过以下代码实现

Object obi = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj= null;
sf.get();

软引用主要用于实现类似缓存的功能,在内存足够的情况下直接通过软引用实现对对象的一个副本,无需从繁忙的真实来源查询数据,提升速度;当内存不足的时候,会自动删除这部分缓存数据,从真正的来源来查询这些数据。

弱引用

会在第二次垃圾回收的时候回收弱引用

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<object>(obj);
obj = null;
wf.get()// 有时候会返回 null
wf.isEnQueued(); // 返回是否在垃圾回收队列里面

弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的 isEnQueued() 方法返回对象是否被标记为即将回收。

虚引用(幽灵引用/幻影引用)

在垃圾回收的时候回收,无法通过引用取到对象值

Object obj = new Object();
PhantomReference<Object> sf = new PhantomReference<Object>(obj);
obj = null;
System.out.println(sf.get());  // 永远返回 null,永远得到不了对象
System.out.println(sf.isEnqueued());

虚引用主要是用于检测对象是否已经从内存中删除。

分代垃圾回收

分代垃圾回收的提出

  • 在 Java 代码中,Java 语言没有显式的提供分配内存和删除内存的方法。一些开发人员将引用对象设置为null 或者调用 System.gc() 来释放内存。System.gc() 方法会大大影响系统的性能。
  • 在 Java 中,由于开发人员没有在代码中显式删除内存,所以垃圾收集器会去发现不需要(垃圾)的对象,然后删除它们,释放内存。
  • 分代垃圾收集器是基于以下两个假设而创建的。
    1. 绝大多数对象在短时间内变得不可达
    2. 只有少量年老对象引用年轻对象.

年轻代和老年代

  • 年轻代:新创建的对象都存放在这里。因为大多数对象很快变得不可达,所以大多数对象在年轻代中创建,然后消失。当对象从这块内存区域消失时,我们说发生了一次“ minor GC”。
  • 老年代:没有变得不可达,存活下来的年轻代对象被复制到这里。这块内存区域一般大于年轻代。因为它更大的规模, GC发生的次数比在年轻代的少。对象从老年代消失时,我们说“ major GC”(或 “fullGC” )发生了。

年轻代区域

  • 年轻代总共有3块空间,1块 Eden 区,2块 Survivor 区。各个空间的执行顺序如下:
    1. 绝大多数新创建的对象分配在 Eden 区
    2. 在 Eden 区发生一次 GC 后,存活的对象移动到一个 Survivor 区
    3. 一旦一个 Survivor 区已满,存活的对象移动到另外一个 Survivor 区。然后之前那个已满空间的 Survivor 区将置空,没有任何数据。
    4. 经过重复多次这样操作后任然存活的对象将被移到老年区。

典型的垃圾收集算法

Mark-Sweep (标记-清除)算法

在这里插入图片描述

  • 这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象(可以使用类似于位图的方式),清除阶段就是回收被标记的对象所占用的空间。
  • 从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

Copying(复制)算法

在这里插入图片描述

  • Copying 算法将内存按容量划分为大小相等的两块,每块只使用其中的一块。当一块的内存用完了,就将还存活的对象复制到另一块上,然后再将已经使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
  • 这种算法虽然简单,但是不容易产生内存碎片而且运行高效,但是却对内存空间的使用付出了高昂的代价,内存直接缩减到原来的一半。
  • 而且 Copying 算法的运行效率和存活对象的数目有很大的关系,如果存活对象很多,Copying算法的效率将会大大降低。

Mark-Compact(标记-整理)算法

在这里插入图片描述
和 Mark-Sweep 算法一样,但是在完成标记以后不是直接清理可回收对象,而是将村后对象都向一端移动,然后清理掉端边界以外的内存。

Generational Collection(分代收集)算法

  • 分代收集算法是目前大部分 JVM 的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
  • 目前大部分垃圾收集器对于新生代都采取 copying 算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中的一块 Survivor 空间,当进行回收时,将 Eden 和 Survivor 中还存活的对象复制。到另一块 Survivor 空间中,然后清理掉 Eden 和刚才使用过的 Survivor 空间。
  • 由于老年代的特点是每次回收都只回收少量对象,一般使用的是 Mark-Compact 算法。
  • 注意,在堆区之外还有一个代就是永久代(Permanent Generation),它用来存储 class 类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

典型的垃圾收集器

Serial/Serial Old

Serial/Serial Old 收集器是最基本最古老的收集器,它是一个单线程收集器, 并且在它进行垃圾收集时,必须暂停所有用户线程。Seria收集器是针对新生代的收集器,采用的是 Copying 算法,Serial Old 收集器是针对老年代的收集器,采用的是 Mark-Compact 算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。

ParNew

该收集器是 Serial 收集器的多线程版本。

Parallel Scavenge

  • Parallel Scavenge 收集器是一个新生代的多线程收集器(并行),它在回收期间不需要暂停其它用户线程,采用 Copying 算法,可以实现吞吐量的可控
  • Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和 Mark-Compact 算法。

CMS

CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,是一种并发收集器,采用 Mark-Sweep 算法。

G1

G1 收集器是当今收集器领域的最前沿技术,是一款面向拂去其端应用的收集器,能充分利用多 CPU、多核环境,是一款并行和并发的收集器,并且可以建立可预测的停顿时间模型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值