目录
一、JVM的基本概念
1.JVM是Java虚拟机,用来保证Java语言跨平台。
2.JVM与Java语言并没有必然的联系,他只与特定的二进制文件格式(class文件格式相关联)
3.JVM就是一个字节码翻译器,它将字节码文件翻译成各个系统对应的机器码,确保字节码文件能在各个系统正确运行。
二、JVM存在的意义
先思考一个问题:电脑是怎么认识我们所编写的Java程序的?
首先要明白电脑是二进制的系统,它只认识 机器码(二进制编码),例如01010…
比如我们经常要编写 的文件名.java文件 电脑是怎么识别并运行的
文件名.java是我们程序员编写的,我们可以认识,但是电脑不认识,所以就需要以下的一系列操作,能让电脑识别它:
Java文件被处理的过程:
①程序员编写的.java文件
②由javac编译成字节码文件.class:(为什么编译成class文件,因为JVM只认识.class文件)
③在由JVM编译成电脑认识的文件 (对于电脑系统来说 文件代表一切)
如下图所示
在这里提一下,为什么说java是跨平台语言?
java有JVM从软件层面屏蔽了底层硬件、指令层面的细节让它兼容各种系统。
我们在java开发中何时考虑过内存管理
不像c和c++还要考虑什么时候释放资源
我们java只需要考虑业务实现就行了
三、JVM的体系结构
为了方便理解JVM的体系结构,可以先看看以下几张图
①JVM的体系结构
②JVM所处的位置
③ 官方给出的解释
类加载器
类装载器(ClassLoader)负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine(执行引擎)决定。
运行时数据区
1.方法区
也称”永久代” 、“非堆”, 它用于存储
虚拟机
加载的类信息、常量、静态变量、是各个线程共享的内存区域。默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。
运行时常量池:是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。
2.虚拟机栈(也叫栈)
描述的是java 方法执行的
内存
模型:每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。声明周期与线程相同,是线程私有的。
局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。
3.本地方法栈
与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务
4.堆
也叫做java 堆、GC堆是java虚拟机所管理的内存中最大的一块内存区域,
也是被各个线程共享的内存区域,在JVM启动时创建。该内存区域存放了对象实例及数组(所有new的对象)
。其大小通过-Xms(最小值)和-Xmx(最大值)参数设置,-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。由于现在收集器都是采用分代收集算法,堆被划分为新生代和老年代(
因为大部分对象是“朝生夕灭”的,一部分对象是长期驻留在内存中的
)。新生代主要存储新创建的对象和尚未进入老年代的对象。老年代存储经过多次新生代GC(Minor GC)任然存活的对象。
新生代
:程序新创建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成,可通过-Xmn参数来指定新生代的大小,也可以通过-XX:SurvivorRation来调整Eden Space及SurvivoSpace的大小。
老年代
:用于存放经过多次新生代GC任然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:①.大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。②.大的数组对象,切数组中无引用外部对象。
老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。
5.程序计数器
是最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。
在这里补充一下
线程私有区域包含
(
即生命周期与线程相同,依赖用户线程的启动/结束,而创建/销毁JVM内
):
程序计数器/PC
寄存器
、虚拟机栈、本地方法栈。
线程共享区域包含
(
即随虚拟机的启动/关闭而创建/销毁
):
方法区、堆。
执行引擎
1.执行引擎是Java虚拟机的核心组成部分之一
2.虚拟机是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的。
而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
四、运行时内存
Java堆从GC的角度还可以细分为: 1/3堆空间的新生代(Eden区、SurvivorFrom区和SurvivorTo区)和2/3堆空间的老年代。
新生代:存放新创建的对象
Eden区:存放新生对象(如果对象占用内存过大,直接分配到老年代),当此区域内存不够时触发MinorGC进行垃圾回收
SurvivorFrom区:上次MinorGC的幸存者,作为此次GC的被扫描者
SurvivorTo区:一次MinorGC的幸存者
MinorGC:过程为复制->清空->互换
1.复制:将Eden区和SurvivorFrom区的新生对象复制到SurvivorTo区,同时给这些对象年龄+1,如果对象年龄达到了老年标准,默认年龄15,则复制到老年代区,如果ServivorTo区内存不够则放到老年区;
2.清空:将Eden区和SurvivorFrom区中的对象清空;
3.互换:将SurvivorTo区和SurvivorFrom区互换,原ServicorTo成为下一次GC时的 ServicorFrom区。
老年代:存放应用程序中生命周期长的内存对象,老年代相对稳定MajorGC不会频繁执行,当MinorGC执行后有新生代的对象加入老年代之后导致内存不足时才会触发,或无法找到足够大的连续内存空间分配给较大的对象时也会触发永久代:存放class和Meta的信息,在加载时被放入,GC不会在主程序运行期间对永久代进行清理,所以如果class加载增多会导致抛出异常,在Java8中永久代被元数据区替代,元数据区并不在虚拟机中,直接使用本地内存,因此仅受限于本地内存。
五、垃圾回收
垃圾回收首先要确定三个方向:1、如何确定垃圾;2、如何收集垃圾;3、垃圾收集器
确定垃圾的方法
1. 引用计数法
:在Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,则说明对象不太可能再被用到,那么这个对象就是可回收对象。
2.可达性分析
:为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程,两次标记后仍然是可回收对象,则将面临回收。
收集垃圾
1.标记清除算法
:先标记,后清除,优点简单快捷,缺点内存碎片化严重不连续
2.复制算法
:将内存划分为等量的两块,每次将回收后存活的对象复制到另一块,
优点
不易产生碎片,
缺点
压缩内存空间,如果存活对象多则复制一次会降低效率。
3.标记整理算法
:整合上述标记清除和复制的优点,标记待回收对象空间,将存活对象移向内存一端,然后清除端边界外的对象
4.分代收集算法
:将内存划分为新生代老年代,新生代频繁回收,老年代较为稳定,新生代使用复制算法,老年代为标记整理法。
此处除了分代算法,还有一种分区算法,分区算法将整个堆空间划分为连续的不同小区间, 每个小区间独立使用,独立回收。这样做的好处是可以控制一次回收多少个小区间,根据目标停顿时间,每次合理地回收若干个小区间(而不是整个堆),从而减少一次 GC 所产生的停顿。
六、垃圾收集器
①Serial 垃圾收集器(单线程、 复制算法)
:Serial(英文连续)是最基本垃圾收集器,使用复制算法,曾经是JDK1.3.1之前新生代唯一的垃圾收集器。Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此 Serial垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器
②ParNew 垃圾收集器 ( Serial+多线程 多线程)
:ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。【Parallel:平行的】ParNew虽然是除了多线程外和Serial收集器几乎完全一样,但是ParNew垃圾收集器是很多java虚拟机运行在 Server 模式下新生代的默认垃圾收集器
③Parallel Scavenge 收集器(多线程复制算法、高效)
:Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别
④Serial Old 收集器 (单线程标记整理算法 )
:Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器
⑤Parallel Old 收集器 (多线程标记整理算法)
:Parallel Old收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在JDK1.6才开始提供。在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略
⑥CMS 收集器(多线程标记清除算法
):Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验
⑦G1 收集器
:G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。