Loading...
墨滴

lijunsong

2021/05/19  阅读:33  主题:默认主题

JVM一篇就够了

方法区
Person person = new Person();

栈stack

  1. 存储局部变量,操作数,方法出口等
  2. JVM为每个线程创建一个栈,用于存放线程执行方法的信息(实际参数,局部变量等)
  3. 先进后出,后进先出
  4. 栈是由系统自动分配,速度快,是一个连续的内存空间
  5. 线程私有

堆heap

  1. 存储创建好的对象,数组
  2. JVM只有一个堆,线程共享
  3. 是一个不连续的内存空间,分配灵活,速度慢

方法区 method area

  1. JVM只有一个方法区,线程共享
  2. 实际上也是堆,只是用于存放类、常量
  3. 用来存放程序中永远不变或唯一的内容(类信息-class对象、静态变量、字符串常量等)

内存回收

  • 判断对象是否‘死亡’
  1. 引用计数法 Reference Counting

对象如果没有任何与之关联的引用,即引用数为零,说明对象‘死亡’

不足:对象循环引用不能判断问题

  1. 根搜索算法 GC Roots Tracing

GC Roots作为起点,向下搜索,当对象到任何GC Roots没有引用链时,说明对象‘死亡’

VM栈中引用,方法区中的静态引用,JNI中的引用

  • 算法
  1. 标记清除 Mark-Sweep

标记阶段:首先标记出所有需要回收的对象

清除阶段:在标记完成后统一回收所有被标记的对象

主要不足:空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

  1. 复制 Copying

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,实现简单,运行高效。

不足:只是这种算法的代价是将内存缩小为了原来的一半,并且还有复制步骤

  1. 标记整理 Mark-Compact

首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

  1. 分代回收 根据不同代使用上面一种或多种算法进行回收

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收

  • 新生代可用GC:串行copying,并行回收copying,并行copying
  • Minor GC触发机制以及日志格式
  • 旧生代可用GC:串行Mark-Sweep-Compact,并行Compacting,并行Mark-Sweep

内存状况分析方法

  1. jconsole
  2. visualvm
  3. jstat

用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据

  1. jmap

jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等

  1. MAT

JVM 生命周期

Class文件是一组以8位字节为基础单位的二进制流

  1. 加载(Loading)

虚拟机需要完成以下3件事情:

通过一个类的全限定名来获取定义此类的二进制字节流。

将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

  1. 验证(Verification)

是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。但从整体上看,验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

  1. 准备(Preparation)

是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

这个阶段中有两个容易产生混淆的概念需要强调一下,首先,这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,

假设一个类变量的定义为: public static int value=123; 那变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。表7-1列出了Java中所有基本数据类型的零值。 假设上面类变量value的定义变为:public static final int value=123; 编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。

  1. 解析(Resolution)

是虚拟机将常量池内的符号引用替换为直接引用的过程

  1. 初始化(Initialization)

是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,

或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。 <clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞。

  1. 使用(Using)
  2. 卸载(Unloading)

lijunsong

2021/05/19  阅读:33  主题:默认主题

作者介绍

lijunsong