暂无图片
暂无图片
2
暂无图片
暂无图片
暂无图片

图解快速入门Java虚拟机JVM

一叶扁舟 2021-03-08
1305

目录

一、JVM架构图

1.1、宏观jvm

image.png

1.2、JVM架构图

image.png

二、类装载器

2.1、类装载器

2.1.1、分类

加载器 存在位置 特点
BOOT(根加载器) Environment/jdk8/jre/lib/rt.jar 原生C++代码来实现的,并不继承自java.lang.ClassLoader(查不到)
EXC(扩展类加载器) Environment/jdk8/jre/lib/ext/*.jar Java 虚拟机提供一个扩展库目录用来加载 Java 的扩展库,该类加载器在此目录里面查找并加载 Java 类。
APP(应用程序加载器 / 系统类加载器) CLASSPATH 它根据 Java 应用的类路径来加载自己写的 Java 类
自定义类加载器 除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

2.1.2、作用

作用加载class文件到虚拟机

将class文件码加载到内存中,
将静态数据转化成方法区的运行时数据结构
在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。

代码查看

// 创建Car实例 Car car1 = new Car(); Car car2 = new Car(); // 可以看出同一个类,创建出的实例不同 System.out.println(car1.hashCode()); System.out.println(car2.hashCode()); // 实例转类,类时相同的 Class<? extend Car> clazz1 = car1.getClass() Class<? extend Car> clazz2 = car2.getClass() System.out.println(clazz1.hashCode()); System.out.println(clazz2.hashCode()); // 获取类加载器 System.out.println(ClassLoader.getSystemClassLoader()); // AppClassLoader System.out.println(clazz1.getClassLoader()); // AppClassLoader System.out.println(clazz1.getClassLoader().getParent()); // ExtClassLoader System.out.println(clazz1.getClassLoader().getParent().getParent()); // BootClassLoader(null:获取不到)

流程梳理

image.png

2.2、双亲委派机制

双亲委派机制

1、类加载器收到类加载的请求

2、将请求一直向上委托直到根加载器(启动类加载器)

3、从根加载器开始向下检查,能加载就结束,不能加载就抛异常,通知子加载器进行加载,直到可以加载当前这个类。

三、Native

3.1、引言

一般在类中是不能定义接口的,比如

private void start0(); // 定义在类中会报错

但是,查看线程的代码可以发现

new Thread().start(); private native void start0(); // 在Thread类中,存在这样一个奇怪的接口,由native修饰

这是由于,java本身是不能直接控制操作系统的线程的,这超过了java的作用范围,因此,底层使用C++/其他语言进行功能的实现,提供接口,并以native修饰,用于java调用。从而扩展java的能力

3.2、Native功能

调用其他语言,扩展java的能力

image.png

四、Program Counter Register

4.1、概述

4.1.1、PC寄存器由来

冯.诺依曼计算机体系结构的主要内容之一:程序预存储,计算机自动执行。

处理器要执行的程序都是以二进制代码块的方式存储在计算机的存储器中,

处理器将这些代码逐条的取到处理器中,再编译执行,以完成整个程序的执行。

为了保证程序能够连续的执行下去,CPU必须具有某些手段来确定下一条取址指令的地址

程序计数器正是起到这种作用,因此又被称为指令计数器。

4.1.2、JVM指令与PC寄存器、CPU的关系

  • 写个测试类Demo.java
void test() { int ids[]=new int[5]; Object objs[]=new Object[5]; Object obj=new Object(); Hello hello=new Hello(); int len=objs.length; }
  • 使用命令javap -v Demo.class 进行反编译, 可以得到代码的JVM指令(加序列号的指令
void test(); Code: 0:iconst_5 1:newarray int 3:astore_1 4:iconst_5 5:anewarray#2; //class java/lang/Object 8:astore_2 9:new#2; //class java/lang/Object 12:dup 13:invokespecial#1; //Method java/lang/Object."":()V 16:astore_3 17:new#3; //class Hello 20:dup 21:invokespecial#4; //Method "":()V 24:astore4 26:aload_2 27:arraylength 28:istore5 30:return
  • JVM指令与PC寄存器、CPU的关系
image.png

4.2、功能

功能

保存当前执行指令的地址,一旦指令执行,程序计数器将更新到下一条指令

保证程序可以正常往下执行

特性

  • 每一个线程都有一个PC寄存器,是线程私有的,生命周期与线程的生命周期保持一致
  • 它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
  • 任何时间,一个线程只会有1个方法在执行,而PC寄存器便是存的改方法的JVM指令的序号,而如果执行的是Native方法,则存储undefined,因为程序计数器不负责本地方法栈
  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
  • 它是唯一一个在java虚拟机规范中没有规定任何OOM(Out Of Memery)情况的区域,而且没有垃圾回收

常见面试问题

1、为什么要使用PC寄存器?

在多线程中,CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行,
而PC寄存器可以存储指令的序号,方便线程切换回来还能继续执行

2、PC寄存器为什么会设定为线程私有?

因为线程是来回切换的,一个线程一个PC寄存器,更方便各个线程间独立计算,从而避免相互干扰的情况。

五、元空间

发展历程:永久代、方法区 ----> 元空间

java8之前有永久代,永久代与方法区在一起,并与堆是同一块内存(逻辑上是分开的),Java8,HotSpots取消了永久代,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)。当Java Heap空间不足时会触发GC,但Native memory空间不够却不会触发GC。元空间存在于本地内存,意味着只要本地内存足够,它不会出现像永久代中“java.lang.OutOfMemoryError: PermGen space”这种错误。默认情况下元空间是可以无限使用本地内存的,但为了不让它如此膨胀,JVM同样提供了参数来限制它使用的使用。

  • jdk1.6及之前:存在永久代,常量池在方法区与堆是同一块内存(逻辑上是分开的)
  • jdk1.7: 存在永久代,慢慢去永久代,常量池放在堆中
  • jdk1.8及之后:无永久代,常量池放在元空间,元空间的内存不再与堆公用,元空间存在于本地内存

为什么使用元空间替换永久代?

避免了OOM异常。因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误。

当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。

特点

1、被所有线程共享,属于共享区间

2、静态变量 + 类信息(构造方法/接口定义) + 运行时常量池存元空间中 。实例变量字符串常量池存在 堆内存

运行时常量池和静态常量池存放在元空间中,而字符串常量池依然存放在堆中。

六、栈

6.1、栈帧

什么是栈帧?

每个方法调用时占用的内存

组成

局部变量表、操作数栈、动态链接、方法返回地址

  • 局部变量表

    存放局部变量的列表

  • 操作数栈

    一个先进后出的栈

  • 动态链接

    指向运行时常量池的引用

  • 方法返回地址

    包括正常/异常返回,不同的返回类型返回不同的指令

6.2、栈

什么是栈?

在线程执行的过程中,需要内存空间,而栈即是线程的内存空间,每个线程都会有一个栈,是线程私有的。

是一种先进后出的数据结构(栈先进后出(FIFO: First input First output) ,这就是为什么main先执行最后结束 main方法最先压栈)

与栈帧的关系

程序的执行是调用方法实现的,每个方法定义一个栈帧,因此在虚拟机栈中会有一个或多个栈帧

每个线程有一个栈,每个栈只有一个活动栈帧(对应着正在执行的那个方法)

功能

主管程序运行、生命周期、线程同步,程序结束,栈内存释放,所以对于栈来说不存在垃圾回收问题

存放

8种基本数据类型、对象的引用地址、实例的方法

栈满了

StackOverflowError

七、堆

7.1、概述

JVM中,用于存放对象实例的内存空间,线程共享。

7.2、堆的组成

一个JVM只有一个堆,堆内存的大小可以调节

image.png

八、GC(垃圾回收机制)

8.1、垃圾回收概述

概述

当一个启动类加载了过多的jar包、Tomcat部署了太多的应用、大量动态生成的反射类等情况会造成JVM堆中内存不足,则会报OOM错误,而垃圾回收机制就是用来清理内存,控制OOM的发生

作用区域

image.png

8.2、GC算法

8.2.1、常见算法

引用计数法

  • 算法描述

    每个对象都配置一个计数器,清除掉计数为0(没被使用)的对象

image.png

  • 缺点

    要给每个对象创建一个计数器,计数器本身也消耗内存

复制算法

  • 算法描述

    1、对象进入S0(From),进入后S1(To)为空

    2、将S0数据复制到S1(S1变为From),删除S0数据(S0变为To)

    3、循环往复

    image.png

  • 优缺点

    • 优点:没有内存的碎片、没有计数器效率高
    • 缺点:浪费一般的内存(To区一直是空的)
  • 使用场景

    由于S0 S1要相互复制,避免进入的对象较多,复制消耗大,因此使用场景是要对象存活率低,而新生代对象存活率较低,比较适合复制算法

标记清除算法

  • 算法描述

    1、扫描所有对象,并对对象进行标记(标记非垃圾对象)

    2、扫描,对没有标记的对象进行清除

image.png

  • 优缺点
    • 优点:不需要额外的空间,相对于复制算法节省空间
    • 缺点:需要扫描两次浪费时间、会产生内存碎片

标记整理算法

  • 算法描述

    1、扫描所有对象,并对对象进行标记(标记非垃圾对象)

    2、扫描,对没有标记的对象进行清除

    3、扫描,压缩,防止碎片内存产生

image.png

  • 优缺点
    • 优点:不需要额外的空间,相对于复制算法节省空间、解决了产生内存碎片的问题
    • 缺点:需要扫描三次两次浪费时间

分代收集算法

  • 算法描述

    分代收集算法是针对不同代,采用不同的算法,年轻代使用复制算法,年老代使用标记清除+标记压缩混合

8.2.2、常见算法对比

  • 算法对比
    • 内存效率:复制算法 > 标记清除算法 > 标记整理算法(时间复杂度)
    • 内存整齐度:复制算法 = 标记整理算法 > 标记清除算法
    • 内存利用率:标记整理算法 = 标记清除算法 > 复制算法

每种算法都有优缺点,没有最好的算法,只有最适合的算法

可以根据采用分代收集算法,对不同代采用不同的算法

  • 算法选择:分代收集算法
    • 年轻代
      • 复制算法(因为年轻代存活率低)
    • 年老代
      • 标记清除+标记压缩混合(因为存活率较高)

8.3、Java GC流程

Java GC才用了分代收集算法,对新生代和老年代分别使用了不同的GC算法

image.png
最后修改时间:2021-03-10 12:10:19
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论