令人头痛的JVM

klass模型

Java的每个类,在JVM中,都有一个对应的Klass类实例与之对应,存储类的元信息如:常量池、属性信息、方法信息……

1606013740690

从继承关系上也能看出来,类的元信息是存储在元空间的

普通的Java类在JVM中对应的是instanceKlass类的实例里面放着元信息,虚拟机层面与Java类对等的数据结构它的三个字类

元信息:属性信息 方法信息

  1. InstanceMirrorKlass:用于表示java.lang.Class的实例,Java代码中获取到的Class对象,实际上就是这个C++类的实例,存储在堆区,学名镜像类, 静态属性是存储在镜像类中的
  2. InstanceRefKlass:用于表示java/lang/ref/Reference类的子类
  3. InstanceClassLoaderKlass:用于遍历某个加载器加载的类,描述类加载器的实例

Java中的数组不是静态数据类型,是动态数据类型,即是运行期生成的,Java数组的元信息用ArrayKlass的子类来表示:

  1. TypeArrayKlass:用于表示基本类型的数组
  2. ObjArrayKlass:用于表示引用类型的数组

类加载的过程:

类的生命周期是由7个阶段组成,但是类的加载说的是前5个阶段

加载:

1、通过类的全限定名获取存储该类的class文件 ,并获取其二进制字节流

2、解析成运行时数据,即instanceKlass实例,存放在方法区

3、在堆区生成该类的Class对象,即instanceMirrorKlass实例

验证:

  1. 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  4. 符号引用验证:确保解析动作能正确执行。

准备:

正式为类中定义的变量分配内存并设置类变量初始值

  1. 为静态变量分配内存、赋初值 int value=0; String value =null;
  2. 实例变量是在创建对象的时候完成赋值的,没有赋初值一说
  3. 如果被final修饰,在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值
  4. 准备过程通常分配一个结构用来存储类信息,这个结构中包含了类中定义的成员变量方法接口信息

解析:

实际上是把类的符号引用替换为直接引用的过程。

解析后的信息存储在ConstantPoolCache()类实例中

常量池缓存是为常量池预留的运行时数据结构。保存所有字段访问和调用字节码的解释器运行时信息。缓存是在类被积极使用之前创建和初始化的。每个缓存项在解析时被填充

  1. 类或接口的解析
  2. 字段解析
  3. 方法解析
  4. 接口方法解析

初始化:

  1. 执行静态代码块,完成静态变量的赋值
  2. 静态字段、静态代码段,字节码层面会生成clinit方法
  3. 方法中语句的先后顺序与代码的编写顺序相关

初始化的步骤

  1. 如果此类没被加载连接,则先加载连接此类;
  2. 如果此类的直接父类还未被初始化,则先初始化其直接父类;
  3. 如果类中有初始化语句,则按照顺序依次执行初始化语句。

初始化的时机

  1. 创建类的实例(new关键字);
  2. java.lang.reflect包中的方法(如:Class.forName(“xxx”));反射
  3. 对类的静态变量进行访问或赋值;
  4. 访问调用类的静态方法
  5. 初始化一个类的子类父类本身也会被初始化;
  6. 作为程序的启动入口,包含main方法(如:SpringBoot入口类)。

主动引用

主动引用:在类加载阶段,只执行加载连接操作,不执行初始化操作

被动引用

被动引用: 在类加载阶段,会执行加载连接初始化操作。

被动引用的几种形式:

  1. 通过子类引用父类的的静态字段,不会导致子类初始化;
  2. 定义类的数组引用不赋值,不会触发此类的初始化;
  3. 访问类定义的常量,不会触发此类的初始化

JVM内存结构:

JVM内存结构主要有三大块:

  1. 堆内存:是JVM中最大的一块,由年轻代和老年代组成,而年轻代内存又被分成三部分,

    • Eden空间、8

    • From Survivor空间、1

    • To Survivor空间,1

  2. 方法区 :存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。

  3. : java栈 本地方法栈 程序计数器

控制参数

  • -Xms设置堆的最小空间大小。
  • -Xmx设置堆的最大空间大小。
  • -XX:NewSize设置新生代最小空间大小。
  • -XX:MaxNewSize设置新生代最大空间大小。
  • -XX:MetaspaceSize设置元空间最小空间大小。
  • -XX:MaxMetaspaceSize设置元空间最大空间大小。
  • -Xss设置每个线程的堆栈大小。

调优 -xms和-xmx最大最小调成一样大-XX:MaxNewSize和-XX:MetaspaceSize最大最小调成一样大防止内存抖动

或者物理内存的1/32

Java堆(Heap)

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。最小值为内存1/4

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,

所以Java堆中还可以细分为:新生代和老年代;

新生代:

再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。

类出生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,

老年代:

新生代经过多次GC仍然存货的对象移动到老年区。若老年代也满了,这时候将发生Major GC(也可以叫Full GC),进行老年区的内存清理。若老年区执行了Full GC之后发现依然无法进行对象的保存,就会抛出OOM

方法区(Method Area)

java调用c,c++的动态链接库,运行里面函数需要的栈

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

方法区有时被称为持久代(PermGen ), 但是实际上应该说使用永久代来实现方法区,1.8叫元空间而已。

可以吧方法区比作java的接口,永久代和原空间是对接口的实现

虚拟机栈(Stack):

一个线程有一个虚拟机栈

一共有方法调用次数个栈帧

每个方法从被调用到执行完成的过程,其实就是一个栈帧在虚拟机栈中从入栈到出栈的过程。

不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致

本地方法栈(Native Method Stack):

和栈作用很相似,区别不过是Java栈为JVM执行Java方法服务,

而本地方法栈为JVM执行native方法服务。

登记native方法,在Execution Engine执行时加载本地方法库

程序计数器(Program Counter Register)

其是很小的一块内存空间,可以看作是当前线程所执行的字节码的行号

因为Java虚拟机的多线程时通过线程轮流切换,轮流得到处理器的资源来进行的,所以就需要在线程中有一个计数器来记录执行到哪一行,方便切换回来,所以每个线程都需要一个独立的程序计数器,他们相互独立,互不影响。

类加载机制

全盘负责委托机制

当一个ClassLoader加载一个类的时候,除非显示的使用另一个ClassLoader,该类所依赖和引用的类也由这个ClassLoader载入

双亲委派机制

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类

  • 1、当 AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  • 2、当 ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader```去完成。
  • 3、如果 BootStrapClassLoader加载失败(例如在 $JAVA_HOME/jre/lib里未查找到该class),会使用 ExtClassLoader来尝试加载;
  • 4、若ExtClassLoader也加载失败,则会使用 AppClassLoader来加载,如果 AppClassLoader也加载失败,则会报出异常 `ClassNotFoundException
双亲委派模式的优势
  • 沙箱安全机制:比如自己写的String.class类不会被加载,这样可以防止核心库被随意篡改
  • 避免类的重复加载:当父ClassLoader已经加载了该类的时候,就不需要子ClassLoader再加载一次

GC算法和收集器

如何判断对象可以被回收

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)

引用计数法

给对象添加一个引用计数器,每当有一个地方引用,计数器就加1。当引用失效,计数器就减1。任何时候计数器为0的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中没有选择这个算法来管理内存,最主要的原因是它很难解决对象之前相互循环引用的问题。所谓对象之间的相互引用问题,通过下面代码所示:除了对象a和b相互引用着对方之外,这两个对象之间再无任何引用。但是它们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数器法无法通知GC回收器回收它们。

可达性分析算法

这个算法的基本思想就是通过一系列的称为”GC Roots“的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象时不可用的。

GC Roots根节点:类加载器、Thread、虚拟机栈的局部变量表、static成员、常量引用、本地方法栈的变量等等

如何判断一个类是无用的类

需要满足以下三个条件:

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾回收算法:

1.标记-清除算法:

它是最基础的收集算法,这个算法分为两个阶段,“标记”和”清除“。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它有两个不足的地方:

  1. 效率问题,标记和清除两个过程的效率都不高;
  2. 空间问题,标记清除后会产生大量不连续的碎片

2.复制算法

为了解决效率问题,复制算法出现了。它可以把内存分为大小相同的两块,每次只使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块区,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收

3.标记-整理算法

根据老年代的特点提出的一种标记算法,标记过程和“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行回收,而是让所有存活的对象向一段移动,然后直接清理掉边界以外的内存

4.分代收集算法

现在的商用虚拟机的垃圾收集器基本都采用"分代收集"算法,这种算法就是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

在新生代中,每次收集都有大量对象死去,所以可以选择复制算法,只要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率时比较高的,而且没有额外的空间对它进行分配担保,就必须选择“标记-清除”或者“标记-整理”算法进行垃圾收集

posted on 2020-12-05 11:06  Ruayo  阅读(77)  评论(0编辑  收藏  举报

1