JVM太难了吧,小伙都学哭了

504 阅读12分钟

Java虚拟机运行时数据区域

Java运行时数据区域.png

程序计数器

虚拟机栈

虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表操作数栈动态连接方法出口

每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

本地方法栈

调用本地方法时候的类似的Java虚拟机栈

是虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的一块内存区域,在虚拟机启动的时候创建。此内存的唯一目的就是存放对象实例。

堆也是GC的最重点的区域

方法区(非堆)

方法区和Java对一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

运行时常量池

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还要一项信息是常量池表,用于存放编译器生成的各种字面量和符号引用,这部分内容在类加载后存放到方法区的运行时常量池中。

运行时常量池相对于Class文件常量池的另外一个重要特征就是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内筒才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,常见的就是String类的intern()方法

HotSpot虚拟机对象

对象的创建过程

程序执行遇到new关键字-》判断当前类在常量池中是否有对应的类引用-》如果存在判断当前类是否被加载、解析初始化过,如果没有则执行类加载机制,在类加载的过程中就知道了该为对象分配多大的内存空间-》执行对象的内存空间分配-》虚拟机为对象分配默认值-》设置对象元数据-》执行init()构造函数-》得到可用对象

HotSpot对象创建过程.png

引用

  • 强引用:Object o = new Object();
  • 软引用:有用当时非必须的对象,在系统要发生内存溢出前,会回收软引用对象。如果这部分被回收了之后,内存还不够,才会保内存溢出异常
  • 弱引用:描述有用但是非必须的对象,只能生存到一下次GC发生为止,也一次GC会回收掉弱引用对象。
  • 虚引用:当对象被回收的时候收到一个系统通知

垃圾收集算法

如何判断对象是否存活

引用计数法

使用一个计数器对象,当一个对象被引用时,就将计数器加一;当引用失效时,将计数器减一;当计数器为0的时候,说明当前对象可以被回收。

缺点:很难解决对象之间的相互循环引用问题

可达性分析算法

哪些可以作为GC ROOTS的对象
  • 虚拟机栈(栈帧中的本地变量表)中引用的对象,各个线程中被调用的方法堆栈中使用到的参数、局部变量、临时变量
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型变量
  • 在方法区中常量引用的对象,譬如字符串常量池里的引用
  • 在本地方法栈中JNI引用的对象
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象,还有类加载器
  • 所有被同步锁持有的对象
  • 反映Java虚拟机内部情况的JMX Bean,JVMTI中注册的回调、本地代码缓存等

分代收集理论

弱分代假说:绝大多数对象都是朝生夕死的 强分代假说:熬过越多次垃圾收集过程的对象就越难以消灭 跨代引用假说:跨代引用相对于同代引用来说仅占极少数

正是基于以上的特性,大多数的垃圾收集器都将Java堆分为不同的区域,针对不同区域各自的对象生命周期的特点而设计不同的收集算法;这就是为什么如今大多数垃圾收集器都将Java堆划分为不同区域的原因

标记-清除算法

标记清除算法.png

优点:实现非常简单

缺点: 1.产生大量不连续的内存碎片;2、随着堆内存扩大,在标记和清除上所花费的时间越长

标记-复制算法

目标是解决标记-清除算法面对大量回收对象时,执行效率低的问题。

思想:将堆内存空间划分为相同的两块,每次分配内存都在其中的一块上,另外一块的内存空间保留。垃圾回收时,将正在使用的那一块内存空间中的存活对象,复制保留区域中,然后将该区域整体清除。

标记-复制算法.png 优点:1、避免产生大量不连续的内存碎片;2、如果存活对象较少回收效率较高 缺点:1、存在内存空间的浪费,有一半的内存空间不能被分配使用;2、当存活对象较多的时候需要进行大量对象的复制,回收效率很低。

应用:根据分代收集理论中的第一点绝大多数对象都是朝生夕死的,说明在新生代存活的对象较少,那么采用标记-复制算法的效率就很高。

标记-整理算法

目标是解决标记-复制算法中当读对象存活率较高的时候进行较多的复制操作,同时不想浪费50%的内存空间

思想:与标记-清除算法一样,先标记但是不会直接对可回收对象进行清理,而是让存活的对象都想内存空间的一端移动,然后直接清理掉边界以外的内存。

标记-整理算法.png

优点:1、可以避免内存碎片;2、不必要浪费50%的内存空间 缺点:需要移动大量的对象,Stop The World的时间变长,系统吞吐量下降

垃圾收集器

新生代

Serial收集器

单线程收集器,采用标记-复制算法,会出现STW(Stop The World)

Parallel New收集器

Serial收集器的多线程版本(唯一一个可以配合CMS使用的新生代收集器)

Parallel Scavenge收集器

基于标记-复制算法实现的收集器,更够并行收集。它和ParNew非常相似.自适应调节策略也是ParallelScavenge收集器区别于ParNew收集器的一个重要特性

老年代

Serial Old收集器

是Serial收集器的老年代版本,单线程收集器,使用标记-整理算法。

Parallel Old收集器

Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法。搭配Parallel Scavenge实现高吞吐量

CMS收集器

Concurrent Mark Sweep-基于标记-清除算法 特点:尽可能缩短垃圾收集时用户线程的停顿时间

初始标记:需要STW,但是由于只是标记和GC ROOT直接相关的对象引用,所以耗时费非常短; 并发标记:不需要STW,标记和GC ROOT间借相关的对象引用; 重新标记:需要STW,因为在并发标记中GC线程和用户线程在同时运行,用户线程会产生一些增量垃圾,重新标记就是标记这些垃圾 并发清除:将标记出的垃圾清理掉;

优点:并发收集、低停顿 缺点:1、会产生浮动垃圾,在并发清除的时候,用户线程产生的新垃圾只能等到下次GC回收;2、在CPU核数不足的计算机上,会非常影响用户线程的执行。3、基于的是标记-清除算法会产生内存碎片

G1收集器

  • 特点:1、面向服务端应用的垃圾收集器;2、不在对垃圾收集器进行分代,它是作用于整个堆
  • 目标:在延迟可控的情况下,尽可能的提高吞吐量。
  • 步骤
    • 初试标记:只是标记和GC ROOTS直接相关联的对象,并修改TAMS指针的值,让下一阶段用户线程并发运行时,能够正确的在可以用的Region中分配对象。需要STW,但是耗时很短
    • 并发标记:递归扫描标记GC ROOTS简介相关的对象
    • 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录,需要STW
    • 筛选回收:对各个Region的回收价值和成本进行排序,把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间,需要STW

内存分配策略

类加载机制

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

类加载步骤.png

什么时候会触发类加载?

  • 遇到new、getstatic、putstatic或invokestatic这四条指令时,如果类型没有进行过初始化,就需要触发其初始化。
    • 使用new关键字实例化对象的时候
    • 读取或设置一个类型的静态字段的时候
    • 调用一个类型的静态方法的时候
  • 对类型进行反射调用的时候,如果类型没有被初始化过,则需要先触发其初始化
  • 当初始化类的时候,如果父类没有被初始化,那么会先初始化父类

类加载过程

  • 加载(Loading):是类加载(Class Loading)过程的一个阶段,主要完成三件事

    • 通过一个类的全限定名来获取定义此类的二进制字节流
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
    • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  • 验证:是连接阶段的第一部,目的是为了确保Class文件中的字节流信息是否合法,保证虚拟机的安全

    • 文件格式验证
    • 元数据验证
    • 字节码验证
    • 符号引用验证
  • 准备:为类只中定义的变量(即静态常量,被static修饰的变量)分配内存并设置类变量初始值的阶段。

     // 在准备阶段只是为value赋了默认值0,需要等到初试化阶段的时候才会把123赋值给value
     public static int value  = 123;
     // 对于静态常量在准备阶段将123赋值给 value
     public static final value = 123;
    
  • 解析:将常量池内的符号引用替换为直接引用

  • 初始化:执行类构造器的clinit()方法,并不是程序员代码中的编写的方法

    • Java虚拟机必须保证一个类的clinit方法在多线程的环境中被正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的clinit方法,其他线程都需要阻塞等待。 Java中单例对象实现的一直方式就是通过静态内部类的方式获取单例对象

      public class SingleClass {
          private SingleClass() {
          }
      
          static class SingleClassHandler {
              protected static SingleClass singleClass;
      
              static {
                  singleClass = new SingleClass();
                  try {
                      Thread.sleep(5000L);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }
      
          public static SingleClass getInstance() {
              return SingleClassHandler.singleClass;
          }
      
          public static void main(String[] args) {
              new Thread(new Runnable() {
                  @Override
                  public void run() {
                      SingleClass instance = SingleClass.getInstance();
                      System.out.println(instance);
                  }
              }).start();
              new Thread(new Runnable() {
                  @Override
                  public void run() {
                      SingleClass instance = SingleClass.getInstance();
                      System.out.println(instance);
                  }
              }).start();
          }
       }
      

类加载器

"通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)

  • 对于一个任意的类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,没一个类加载器,都拥有一个独立的类名称空间。比较两个类是否相等,只有在这两个类是有同一个类加载加载的前提下才有意义。

双亲委派模型

  • 启动类加载器(BootStrap类加载器):负责加载存放在JAVA_HOME\lib目录的类
  • 扩折类加载器(Extension Class Loader): 负责加载JAVA_HOME\lib\ext目录中的类
  • 应用程序类加载器(Application Class Loader):负责加载用户类路径上的所有类库
  • 用户自动以类加载器(User Class Loader)

双亲委派模型.png

双亲委派的工作流程

  • 流程: 如果一个类加载器收到了一个类加载请求,它不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的BootStrap Class Loader中,只有当父类加载器不能完成这个类的加载的时候,子类加载器才会自己去加载。