JVM--JVM简介,Java内存区域,垃圾回收器与内存分配

1.为什么Java可以跨平台?
在这里插入图片描述

  • Java源代码经javac编译后成为二进制字节码的class文件,JVM解释执行c lass文件
  • Java代码不是直接运行在CPU上,而是运行在Java虚拟机(JVM)
  • 正是因为运行在虚拟机,所以它的代码可以不用修改就能在不同的平台的JVM上运行,比如windows系统是windows的jvm,linux系统是linux的虚拟机。
  • 虚拟机必须支持解释字节码。

什么是虚拟机?
虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。常见虚拟机有JVM、VMware、Virtual Box。
JVM和VMware、Virtual Box这2个虚拟机区别有:1.VMware、Virtual Box是通过软件模式物理cpu的指令集,物理系统中会有很多的寄存器。
2.JVM是通过软件模拟Java字节码的指令集,jvm中主要保存了PC寄存器,JVM是一台被定制过的现实不存在的计算机。
1996 JDK1.0:Java推出(SUN公司)
2000JDK1.3:HotSpot作为默认虚拟机发布
2004JDK1.5 :泛型、注解、装箱、枚举。可变参数、for-each
2010年Oracle收购SUN 得到HotSpot。

运行时数据区域
JVM在运行Java程序时会将管理的内存分为不同的区域。
其中,线程私有区域:程序计数器、Java虚拟机栈、本地方法栈
线程共享区域:Java堆、方法区、运行时常量池


程序计数器(线程私有)
程序计数器是指一个较小的空间,记录当前线程执行字节码的地址,如果被阻塞唤醒后,可以从计数器找到要执行的地址,如果执行的是native方法,计数器值为空
程序计数器内存区域是唯一 一个没有内存溢出(OOM)的区域。

什么是线程私有?
由于JVM的多线程是通过线程轮流切换分配cpu处理时间的方式来实现,那么在任意时间,一个处理器(多核处理器是一个内核)只会执行一条线程的指令。为了当线程阻塞或者切换线程后可以恢复到正确的执行地址,每条线程需要独立的程序计数器,各个计数器之间互不影响,独立存储。把这种区域称为“线程私有”的内存。

Java虚拟机栈(线程私有)
虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时都会创建一个栈帧,栈帧里包含局部变量表、操作数栈、动态链接、方法出口等。每一个方法从调用到执行完成,相当于在虚拟机栈中入栈和出栈。 生命周期和线程相同。
Java虚拟机栈为什么是线程私有?
假如线程1有方法a,调用方法a后,a进入虚拟机栈,a方法里有方法b,b入虚拟机栈,此时栈顶为b,当方法b执行完成后,出栈,栈顶为a,假如虚拟机栈是共享的,又有线程2调用方法c,c入虚拟机栈,栈顶是c,当线程1的a方法执行完成后出栈,出的是线程2的c,此时就有问题。所以每个线程都要有各自的虚拟机栈。
局部变量表: 存放编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在栈中分配多大的局部变量表是完全确定的,在执行期间,不会发生变化。

虚拟机栈一般会产生两种异常:
1.栈溢出:线程请求的栈深度大于虚拟机所允许的深度,有StackOverFlowError异常
2.内存溢出:虚拟机在动态扩展时无法申请到足够的空间,有OutOfMemoryError(OOM) 异常。

本地方法栈(线程私有)
本地方法栈和虚拟机栈的作用一样,只是本地方法栈针对于native方法,而虚拟机栈针对的是Java方法。
在HotSpot虚拟机中,本地方法栈和虚拟机栈是同一块内存区域。

Java堆(线程共享)
Java堆是JVM所管理的最大内存区域。Java堆是所有线程共享的一块区域,在JVM启动时创建。Java堆存放的是对象实例。
所有对象的实例以及数组都在堆上分配。
Java堆是垃圾回收器管理的主要区域,又称“GC堆”,可以用-Xmx设置堆的最大值,-Xms设置堆的最小值。
当堆上没有足够内存完成实例分配并且堆无法再扩展,会有内部溢出 (OOM)异常。

方法区(线程共享)
方法区主要存储已被虚拟机加载的类信息、常量(public static final)、静态变量、即时编译器编译后的代码等数据。在JDK8以前的Hotspot虚拟机中方法区被称为“永久代”,在JDK8以后,被称为“元空间”(Meta Space)。
通过ClassLoader将class文件加载到虚拟机,回收常量池和对类型的卸载。
当方法区无法满足内存要求,会有OOM异常。 (一个for循环一直加载不同的类,可能会导致内存溢出)

运行时常量池(方法区一部分)
存放字面量与符号引用。
字面量:字符串(JDK1.7后移到堆上)、 final常量、基本数据类型的值。
符号引用:类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符。
在这里插入图片描述
Java堆溢出
只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免来GC清除这些对象,那么在对象数量达到最大堆容量后就会产生内存溢出异常。
内存溢出:内存对象存活;可以检查JVM堆内存与物理内存相比是否要将JVM堆内存进行调整或者检查对象生命周期是否过长。
内存泄漏:对象无法被回收;要求我们要即使关闭流,在对象使用时创建,销毁时置null。

垃圾回收器与内存分配策略
由于程序计数器、虚拟机栈、本地方法栈都是线程私有,生命周期随着线程,即这些私有区域是自动回收,而Java堆与方法区是线程共享,需要有一定的内存分配和回收。
如何判断对象已“死”
垃圾回收器在对堆进行垃圾回收时,需要判断堆上的对象是否存活。对象已死,简单理解是没有其他地方引用。但是判断对象是否已“死”有以下算法:
1.引用计数法:给对象增加一个引用计数器,有地方引用时,计数器+1,当计数器为0时,代表没有对象引用它,即对象已“死”。但是引用计数法无法解决循环引用问题。JVM并不采用引用计数来判断对象是否存活。
在这里插入图片描述
2.可达性分析算法

JVM采用可达性分析来判断对象是否存活。当一个对象到一系列"GC Roots"不可达时,称这个对象不可用,即可回收。
在这里插入图片描述
可作为GC Rootss的对象包含:
1.虚拟机栈(Java方法,本地变量表)中引用的对象;
2.本地方法栈(Native方法)引用的对象;
3.方法区中的静态属性引用的对象;
4.方法区常量引用的对象。

在JDK1.2前,只要认为栈上一个数据存放的是另一块内存地址,就认为这块内存代表一个引用;
在JDK1.2以后,引用分为强引用、软引用、弱引用、虚引用。

强引用
强引用是JDK1.2.以前认为的引用,即Object obj = new Object();只要强引用还存在,垃圾回收器永远不会回收堆上的被引用的对象。

软引用
软引用用来描述还有用但是不是必须的对象。对于软引用关联的对象,在系统发生内存溢出之前,会将这些对象列入回收范围进行二次回收, 如果这次回收还是没有足够内存,才会抛内存溢出异常。

弱引用
弱引用描述非必需对象,被弱引用关联的对象,只会存活到下一次垃圾回收前。当垃圾回收器开始工作时,不论内存是否够用,都会回收弱引用关联的对象。(一次回收)

虚引用
无法通过虚引用来取得一个对象实例,为一个对象设置虚引用的唯一目的在于这个对象被收集器回收时可以收到一个系统通知

覆写finalize()方法
真正判断一个对象是否“死”至少历经两次过程:如果对象在进行可达性分析之后没有到达GC Roots的路,会暂处在“缓刑”阶段,将会被第一次标记并且进行一次筛选,筛选的条件是该对象是否覆写finalize()方法。当一个对象没有被使用过,调用System.gc,如果覆写finalize(),将暂逃一劫;如果再次没有被使用,回收时将会直接死亡(finalize只会被JVM调用一次)。

模拟系统回收过程:

public class TestFinalize {
    private static TestFinalize testFinalize;
    public void isLive()
    {
        System.out.println("alive");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("覆写finalize,逃过一劫");
        testFinalize=this;
    }

    public static void main(String[] args) throws InterruptedException {
        testFinalize=new TestFinalize();
        testFinalize=null;
        System.gc();  //第一次回收
        Thread.sleep(1000);  //需要休眠,因为回收需要一定时间
        if(testFinalize!=null) //说明调用finalize()方法
        {
            testFinalize.isLive();
        }
        else
            System.out.println("died");

        testFinalize=null;
        System.gc();  //第二次回收
        if(testFinalize!=null) //说明调用finalize()方法
        {
            testFinalize.isLive();
        }
        else
            System.out.println("died");
    }
}

回收方法区
方法区主要回收两部分:废弃常量和无用的类。

回收废弃常量
比如字符串常量直接赋值时,"hello"入池后,但是系统没有任何一个String对象引用“hello”常量,如果发生GC有必要,这个“hello”常量将会清理出常量池。

回收无用的类
判断一个类是否无用需要同时满足下列三个条件:
1.该类的所有实例化对象被回收(堆上不存在任何该类的实例);
2.加载该类的ClassLoader已经被回收。
3.该类的class对象没有再被引用过,无法再通过反射访问该类。

垃圾回收算法

标记–清除算法
首先遍历标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。
缺点:
1.效率低:标记和清除都需要遍历,导致效率较低;
2.空间不连续:标记清除后会产生大量的不连续碎片,空间碎片太多导致空间利用率低,比如后续需要较大的空间而连续内存不够时需要触发另一次垃圾收集。

复制算法(新生代回收算法)
将内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活的对象复制到另一块上面,然后把已经使用过的内存区域一次清理掉。
优点:每次是对整个半区进行内存回收,不需要考虑内存碎片问题。
现在商用虚拟机(包括HotSpot采用复制算法来回收新生代)。
新生代中将内存区域划分为Eden(新生代内存),Survivor From(幸存者1),Survivor To(幸存者2),三者比例为8:1:1。当Survivor空间不够用时,需要依赖老年代进行分配担保。
HotSpot实现复制算法流程如下:
1.当Eden区满时,会触发第一次Minor gc(新生代回收),把还活着的对象拷贝到Survivor From区;当Eden再次触发新生代回收,会扫描Eden和From区,对两个区域进行垃圾回收,经过这次回收还存活的对象会直接复制到To区,并将Eden和From区清空。
2.当后续Eden又发生新生代回收时,会对Eden和To进行垃圾回收,存活的对象会复制到From区,并将Eden和To区清空。
3.部分对象在From和To区来回复制,如果一个对象来回复制15次,最终是存活状态,会存入老年代。

标记–整理算法(老年代回收算法)
首先遍历需要回收的对象进行标记,然后将所有存活对象都向一端移动(整理),最后清理掉边界以外的内存。

分代收集算法
一般将Java堆分为新生代和老年代。在新生代中,每次垃圾回收只有少量对象存活,采用复制算法;而老年代中存活率较高,没有额外空间对它进行分配担保,采用标记–清理或者标记–整理算法。

问:Minor GC(新生代回收)和Full GC(老年代回收)区别?
1.Minor GC:发生在新生代的垃圾回收,每次垃圾回收只有少量对象存活,采用复制算法,回收速度较快。
2.Full GC又称老年代GC (Major GC):发生在老年代的垃圾收集,
一般会伴随着Minor GC(并非绝对),老年代回收速度比新生代回收速度慢10倍以上。(标记–清理/标记–整理)。

垃圾收集器(JDK1.7的G1收集器之后的HotSpot虚拟机)

有7种作用不同的收集器,如果两个收集器之间存在连线,说明他们可以搭配使用。
在这里插入图片描述
并行(Parallel):并行在多核;指用户线程与垃圾收集线程同时执行(不一定并行,可能会交替执行),用户线程继续运行,而垃圾收集程序在另外一个cpu。
并发(Concurrent):并发在同一个cpu;指多条垃圾收集器并行工作,用户线程处于等待状态 。
吞吐量: 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

1.Serial 收集器(新生代收集器,串行GC)
串行指单线程执行。
特性:只会使用一个CPU或一条收集线程完成收集工作,在进行垃圾收集时,必须暂停其他所有的工作线程,知道它收集结束(Stop The World)。
应用场景:Serial收集器是虚拟机运行在Client模式下的新生代收集器。但是JDK8后无Client模式,有Server模式。

ParNew收集器(新生代收集器,并行GC)
特性: ParNew收集器是Serial 收集器的多线程版本。
应用场景: ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。

Parallel Scavenge收集器(新生代收集器,并行GC)
特性:新生代收集器,使用复制算法的收集器,并行的多线程收集器。

最大的垃圾收集器停顿时间越小,吞吐量越高,但是GC停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。比如原本10秒收集一次,每次停顿100ms,现在5秒收集一次,每次停顿70ms,停顿时间下降的同时,吞吐量也下降了。
对比分析

  • Parallel Scavenge收集器和CMS 收集器对比:Parallel Scavenge收集器的目标是达到一个可控制的吞吐量,而CMS等收集器的目的是尽可能缩短垃圾收集器时用户线程的停顿时间。
  • Parallel Scavenge收集器和ParNew收集器:Parallel Scavenge收集器具有自适应调节策略。

自适应调节策略:1.监控虚拟机状态,调整新生代和Survivor比例;
2.调整到合适的gc时间和吞吐量。

Serial Old收集器(老年代收集器,串行GC)
特性:Serial Old是Serial收集器的老年代版本,同样是单线程收集器,使用标记—整理算法。
应用场景

  • Client模式:Serial Old收集器在给Client模式下的虚拟机使用;
  • Server模式:在JDK 1.5以及之前的版本中
    与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

Parallel Old收集器(老年代收集器,并行GC)
特性:Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和标记—整理算法。Parallel Old收集器是JDK1.6开始提供的。
应用场景: 在注重吞吐量和CPU资源敏感的场合,可以优先考虑Parallel Old和Parallel Scavenge收集器的组合。

在JDK1.5及以前:Parallel Scavenge(新生代回收) 和 Serial Old(老年代回收);
在JDK1.6及以后:Parallel Scavenge(新生代回收) 和 Parallel Old(老年代回收)。

CMS收集器(老年代收集器,并发GC)
特性: 主要目的是获取最短停顿时间。基于“标记–清除”算法。

G1收集器(唯一 一个在老年代和新生代都可以使用的收集器)
G1收集器在清除实例所占用的内存空间后,会做内存压缩。整体是“标记–清理”算法,局部是复制算法。

新生代垃圾收集: 在G1垃圾收集中,新生代的垃圾收集过程使用复制算法,把Eden区和Survivor区的对象复制到新的Survivor区域。
老年代垃圾收集:对于老年代的垃圾收集,G1垃圾收集是“标记–清理”算法。

内部分配与回收策略

1.对象优先在新生代分配
大多数情况,对象在新生代Eden中分配,当Eden区没有足够的空间进行分配时,虚拟机将发生一次新生代回收(Minor GC).
2.大对象直接进入老年代
虚拟机提供了一个-XX:PretenureSize Threshold参数,令大于这个设置值的对象直接在老年代分配。目的在于避免Eden区以及两个Survivor区之间发生大量的内存复制(新生代采用内存–复制算法)。
3.长期存活的对象直接进入老年代
如果对象在Eden出生并经过一次新生代GC可以存活并且被Survivor容纳,年龄增加一次,当年龄增加到一定值(默认为15),就会晋升到老年代。可以通过参数-XX:MaxTenuringThreshold设置阈值年龄。
4.动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以进入老年代,无需等到Max Tenuring Threshold中要求的年龄。
5.空间分配担保
在发生新生代GC之前, 虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果大于,此次新生代GC是安全的;如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。 如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC(新生代GC),但这次Minor GC依然是有风险的;如果小于或者
HandlePromotionFailure=false,则改为进行一次Full GC(老年代GC)。

猜你喜欢

转载自blog.csdn.net/sophia__yu/article/details/87195046