JVM

JVM虚拟机

Java 虚拟机具体是怎么运行 Java 字节码

从虚拟机视角来看,执行 Java 代码首先需要将它编译而成的 class 文件加载到 Java 虚拟机中。加载后的 Java 类会被存放于方法区(Method Area)中。实际运行时,虚拟机会执行方法区内的代码。

如果你熟悉 X86 的话,你会发现这和段式内存管理中的代码段类似。而且,Java 虚拟机同样也在内存中划分出堆和栈来存储运行时数据。

不同的是,Java 虚拟机会将栈细分为面向 Java 方法的 Java 方法栈,面向本地方法(用 C++ 写的 native 方法)的本地方法栈,以及存放各个线程执行位置的 PC 寄存器。

JVM的内存管理结构:

JVM内存分为线程私有区和线程共享区

线程私有区

  1. 程序计数器
    当同时进行的线程数超过CPU数或其内核数时,就要通过时间片轮询分派CPU的时间资源,不免发生线
    程切换。这时,每个线程就需要一个属于自己的计数器来记录下一条要运行的指令。如果执行的是JAVA
    方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为空。

  2. 虚拟机栈
    线程私有的,与线程在同一时间创建。管理JAVA方法执行的内存模型。每个方法执行时都会创建一个桢
    栈来存储方法的的局部变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法
    调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小)。

    局部变量表:存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象的引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)

    使用jclasslib工具可以查看class类文件的结构。下图为栈帧结构图:

  3. 本地方法栈
    与虚拟机栈作用相似。但它不是为Java方法服务的,而是本地方法native(C语言)。由于规范对这块没有强
    制要求,不同虚拟机实现方法不同

线程共享区

  1. 方法区
    线程共享的,用于存放被虚拟机加载的类信息,如常量、静态变量和即时编译器编译后的代码。若要分代,算是永久代(老年代),以前类大多“static”的,很少被卸载或收集,现回收废弃常量和无用的类。其中运行时常量池存放编译生成的各种常量。(如果hotspot虚拟机确定一个类的定义信息不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装载该类的ClassLoader被回收)

  2. java堆

    是Java虚拟机所管理的内存中最大的一块。java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建

    。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

    堆存放对象实例和数组,是垃圾回收的主要区域,分为新生代和老年代。刚创建的对象在新生代的Eden
    区中,经过GC后进入新生代的S0区中,再经过GC进入新生代的S1区中,15次GC后仍存在就进入老年
    代。这是按照一种回收机制进行划分的,不是固定的。若堆的空间不够实例分配,则
    OutOfMemoryError。

堆的结构

堆和栈的区别

栈是运行时单位,代表着逻辑,内含基本数据类型堆中对象引用,所在区域连续,没有碎片;堆是存
储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在区域不连
续,会有碎片。

  1. 功能不同
    栈内存用来存储局部变量方法调用,而堆内存用来存储Java中的对象无论是成员变量,局部变量,
    还是类变量,它们指向的对象都存储在堆内存中。
  2. 共享性不同
    栈内存是线程私有的。
    堆内存是所有线程共有的。
  3. 异常错误不同
    如果栈内存或者堆内存不足都会抛出异常。
    栈空间不足:java.lang.StackOverFlowError。
    堆空间不足:java.lang.OutOfMemoryError。
  4. 空间大小
    栈的空间大小远远小于堆的

JVM的垃圾回收机制

GC是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。垃圾回收主要针对的是堆内存(heap),

什么时候触发我们的GC机制

  ① 在程序空闲的时候。
  ② 手动调用system.gc()。不推荐,因为调用的时候开销很大会严重影响程序运行,更有甚者程序直接挂掉;如果你想跑路就大胆尝试吧!
  ③Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”(内存泄露)这是灰常严重的情况,这时Java应用将停止。
  出现内存泄露的集中情况:
  ①静态集合类像HashMap、Vector等
  ②各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。
  ③监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

  • 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;

  • 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

  顺便提一下 Minor GC、Full GC、OOM什么时候触发:
  创建对象是新生代的Eden空间调用Minor GC;当升到老年代的对象大于老年代剩余空间Full GC;GC与非GC时间耗时超过了GCTimeRatio的限制引发OOM。

如何判断对象是否需要回收?

有两种(采用第二种方式)

  1. 引用计数法(但是并没有采用)

    为每一个创建的对象分配一个引用计数器,用来存储该对象被引用的个数。当该个数为零,意味着没有人再使用这个对象,可以认为“对象死亡”。但是,这种方案存在严重的问题,就是无法检测“循环引用”:当两个对象互相引用,即时它俩都不被外界任何东西引用,它俩的计数都不为零,因此永远不会被回收。而实际上对于开发者而言,这两个对象已经完全没有用处了。

  2. 可达性分析(采用)

    这种方案是目前主流语言里采用的对象存活性判断方案。基本思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。其余的对象则被视为“死亡”的“不可达”对象,或称“垃圾”。

    GC ROOTS的对象包括下面几种:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(即一般说的Native方法)引用的对象

有哪些方式来回收这些垃圾呢?

通过java的分代回收机制,堆中的对象主要分为三种

  • 刚刚创建的对象。在代码运行时会持续不断地创造新的对象,这些新创建的对象会被统一放在一起。因为有很多局部变量等在新创建后很快会变成不可达的对象,快速死去,因此这块区域的特点是存活对象少,垃圾多。形象点描述这块区域为:新生代;
  • 存活了一段时间的对象。这些对象早早就被创建了,而且一直活了下来。我们把这些存活时间较长的对象放在一起,它们的特点是存活对象多,垃圾少。形象点描述这块区域为:老年代;
  • 永久存在的对象。比如一些静态文件,这些对象的特点是不需要垃圾回收,永远存活。形象点描述这块区域为:永久代。(不过在 Java 8 里已经把永久代删除了,把这块内存空间给了元空间,后续文章再讲解。)

也就是说,常规的 Java 堆至少包括了 新生代 和 老年代 两块内存区域,而且这两块区域有很明显的特征:

  • 新生代:存活对象少、垃圾多
  • 老年代:存活对象多、垃圾少

引用

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4中引用强度依次逐渐减弱。

  1. 强引用

    是指创建一个对象并把这个对象赋给一个引用变量。

    比如:

    Object object =``new` `Object();
    String str =``"hello"``;

    强引用有引用变量指向时永远不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。

    public static void main(String[] args) {  
        new Main().fun1();  
    }  
    
    public void fun1() {  
        Object object = new Object();  
        Object[] objArr = new Object[1000];  
    } 

    当运行至Object[] objArr = new Object[1000];这句时,如果内存不足,JVM会抛出OOM错误也不会回收object指向的对象。不过要注意的是,当fun1运行完之后,object和objArr都已经不存在了,所以它们指向的对象都会被JVM回收。

    如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。

    比如Vector类的clear方法中就是通过将引用赋值为null来实现清理工作的:

  2. 软引用(SoftReference)

    • 如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

    • 软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。

    • 在JDK1.2之后,提供了SoftReference类来实现软引用,SoftReference的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。

    • 也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对 这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。另外,一旦垃圾线程回收该Java对象之 后,get()方法将返回null。

  3. 弱引用(WeakReference)

    弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。下面是使用示例:

    public static void main(String[] args) {  
        WeakReference<People>reference=new WeakReference<People>(new People("zhouqian",20));  
        System.out.println(reference.get());  
        System.gc();//通知GVM回收资源  
        System.out.println(reference.get());  
      }  
    }  
    class People{  
        public String name;  
        public int age;  
        public People(String name,int age) {  
            this.name=name;  
            this.age=age;  
        }  
        @Override  
        public String toString() {  
            return "[name:"+name+",age:"+age+"]";  
        }  
    
    }
    
    

输出结果:
[name:zhouqian,age:20]
null

```

  1. 虚引用(PhantomReference)

      虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

      要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

JVM 调优参数