Java虚拟机——JVM

1.What is Jvm?

        我们大家知道jvm,全程:Java Virtual Machine。我们知道java具有跨平台,一次编译到处执行。每一种操作系统,执行相关程序的时候,因为操作环境不同,会造成代码不能跨平台执行。而java可以做到,原因在哪里?就在于不同操作系统有不同版本jvm。       

语言的执行过程:

源代码(.java)----->编译(字节码 .class)----->解释(成为机器码,01010100110)---->机器码

源代码(.js,.sh)--->解释执行(网页能够认识)

1.1. jvm的内存划分区域

1.1.1运行时内存区域划分

jvm运行时内存区域分为两种:线程隔离,线程共享

线程隔离分为:栈(虚拟机栈,本地方法栈),程序计数器

线程共享:堆,方法区(jdk1.8版本以后方法区被称为Metaspace)

1.1.2. 各个区域介绍

1.1.2.1. 程序计数器

       程序计数器(Program Counter Register)是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

        为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,所以程序计数器这类内存区域为“线程私有”的内存。
1.1.2.2. 栈

        所谓“栈”包括:java虚拟机栈、本地方法栈;他们作用相似,区别只是:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。程序员人为的分为“堆栈”中的“栈”。 ​ 栈里存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和指向了一条字节码指令的地址。 ​ 每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

1.2.2.3. 堆

    Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块,此内存区域就是存放对象实例,几乎所有的对象实例都在这里分配内存。 ​ Java堆是垃圾收集器管理的主要区域;内存回收的角度来看Java堆中还可以细分为:新生代和老年代;新生代细致一点的有Eden空间From Survivor空间To Survivor空间。这两块survivor空间大小一致。 ​ 在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的

(通过-Xmx设置最大内存和-Xms设置初始内存) ​ java -Xms10m -Xmx100m Hello

1.1.2.4. 方法区

       方法区又叫静态区:用于存储已被虚拟机加载的类信息、常量池、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆); ​ 对于HotSpot虚拟机是使用永久代来实现方法区; ​ Java虚拟机规范对方法区的限制非常宽松,除了不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,条件相当苛刻。

在jdk1.7中永久代的配置参数-XX:PermSize5m(初始化永久代内存大小),-XX:MaxPermSize10m(最大永久代内存大小)

在jdk1.8中Metaspace的配置参数:-XX:MetaspaceSize=10m(初始化大小),-XX:MaxMetaspaceSize=10m(最大大小)

java中的常量池技术,是为了方便快捷地创建某些对象而出现的,当需要一个对象时,就可以从池中取一个出来(如果池中没有则创建一个),在需要重复创建相等变量时节省了很多时间。

1.1.2.5. 异常--OOM

1)java虚拟机栈\本地方法栈区域 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常

public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            throw e;
        } finally {
            System.out.println("stack length:" + oom.stackLength);
        }
    }
}

2)

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

报错后dump出信息: --XX:+HeapDumpOnOutOfMemoryError ,举例如下:

public class HeapOOM {

    static class OOMObject {
    }

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();

        while (true) {
            list.add(new OOMObject());
        }
    }
}

2. 对象的引用和可达性分析

2.1. 对象的四类引用

java中的引用分为:强引用、软引用、弱引用、虚引用(幽灵引用或者幻影引用),这4种引用强度依次逐渐减弱。

  1. 强引用:在程序代码之中正常的类似于“Person p = new Person()”这类的引用;垃圾收集器不会回收掉被强引用的对象。
  2. 软引用:有用但非必须的对象,jdk中提供了SoftReference类来实现软引用;系统在发生内存溢出异常之前,会把只被软引用的对象进行回收。 ​ 用途?可以做缓存。
  3. ​ 弱引用:非必须的对象,jdk中提供了WeakReference类来实现软引用,比软引用弱一些;垃圾回收不论内存是否不足都会回收只被弱引用关联的对象。 ​
  4. 虚引用:对被引用对象的生存时间不影响;无法通过虚引用来取得一个对象实例;为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知;jdk提供PhantomReference类来实现虚引用。

2.2. 引用的可达性分析

       垃圾回收的对象,是哪些不被引用的对象,所以我们就得需要判定,知道哪些对象不被引用。判定一个对象不被引用的方法就是可达性分析,在可达性分析出现之前还有一种方式——引用计数器法

2.2.1. 引用计数器法

        Person p1 = new Person();
        System.out.println(p1);
        Person p2 = p1;
        System.out.println(p2);
        p1 = null;
        System.out.println(p2);

      Person p1 指向了对象new Person();所以当前对象new Person();对应的引用计数器+1,Person p2 = p1;自然当前对象new Person();对应的引用计数器在+1,为2。当p1 = null;当前对象new Person();失去一个引用,计数结果-1,当计数结果为0的话,则证明当前对象不被引用,则可以被垃圾回收掉。

2.2.2. 可达性分析

      通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(即不可达)时,则证明此对象是不可用的。

常见的GC Root对象:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。

  2. 方法区中类静态属性引用的对象。

  3. 方法区中常量引用的对象。

  4. 本地方法栈中JNI(Java Native Interface即一般说的Native方法)引用的对象。

2.2.3. 垃圾回收前的垂死挣扎

       如果我们判定一个对象不可达,就应该将该对象进行gc垃圾回收掉,但是jvm在进行垃圾回收之前会对这些对象进行一轮的筛 选,如果相关对象此时重新和引用链的对象建立起了关联,那么是可以逃脱被gc掉的命运,但是不是所有的对象都有着特权,只有我们在编写类的时候,复写Object类中的一个方法finalize(),也就是说在该方法重重新建立了引用,就可以起死回生。

2.2.3.1. 两次标记回收的过程:

     不可达的对象真正死亡需要两次标记: ​

     当不可达时标记第一次标记,当对象覆盖finalize()方法并且finalize()方法没有被虚拟机调用过,此对象将会放置在一个叫做F-Queue的队列之中,稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去触发这个方法,但并不承诺会等待它运行结束再执行垃圾回收。 ​

     finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中重新与引用链上的任何一个对象建立关联那么他被移除出“即将回收”的集合,否则就被回收了。

2.2.3.2. 代码示例

public class FinalizeObj {
    public static FinalizeObj obj;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("FinalizeObj finalize called !!!");
        obj = this;//在finalize方法中复活对象
    }

    @Override
    public String toString() {
        return "I am FinalizeObj";
    }

    public static void main(String[] args) throws InterruptedException {
        obj = new FinalizeObj();
        obj = null; //将obj设为null
        System.gc();//垃圾回收
        System.out.println(":-------------------");
        Thread.sleep(1000);//
        if(obj == null) {
            System.out.println("obj is null");
        } else {
            System.out.println("obj is alive");
        }

        System.out.println("第2次调用gc后");
        obj = null;//由于obj被复活,此处再次将obj设为null
        System.gc();//再次gc
        Thread.sleep(1000);
        if(obj == null) {
            //对象的finalize方法仅仅会被调用一次,所以可以预见再次设置obj为null后,obj会被垃圾回收,该语句会被调用
            System.out.println("obj is null");
        } else {
            System.out.println("obj is alive");
        }
    }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值