jvm学习笔记
一、初识JVM
什么是JVM?JAVA Virtual Machine,是对物理机器的软件实现。值得注意的是,jvm并不是仅仅支持java语言,所有可以编译成class文件的语言,理论上来说都是支持的(源文件编译成 .class 文件,JVM将 .class 文件 load 到JVM内存中,再解释成机器码执行),不同的平台有其对应的JVM(Windows、Linux、McOs,每个平台有自己对应的JVM),就是因为java语言可以编译成class文件,而不同平台对应的JVM都可以加载并运行class文件,因此java语言可移植,可以实现一处编译到处运行,执行效率也比传统的解释型语言要高。JVM屏蔽了硬件与操作系统的差异。
java是解释与编译并存的语言。
参考链接:https://chenxiao.blog.csdn.net/article/details/88038677
为什么有java编译器和java编译器还要JIT编译器?
解释器特点
1、逐行解释字节码,逐行生成机器指令执行;
2、效率低;
3、响应速度块,拿到字节码,就开始执行;
JIT特点
1、先进行编译,将字节码编译成本地机器指令;
2、响应速度慢,编译占用了时间,编译完成后才开始执行;
3、因为先编译,编译过程做了优化,执行效率比较高;
两者互补
1、对于一般代码,用解释器,这样响应快,给人的感觉就是速度快;
2、对于热点代码,用JIT,编译后,放在缓存区,下次可以直接使用,效率高;
下图是从网上看到的,详细描述了class文件从加载到JVM中,加载-链接-初始化,以及JVM内存模型与交互关系。 原文链接
二、JVM内存模型
以上是java内存模型的几种常见图形,直接内存指的是机器内存。
Java内存模型是Java语言在多线程并发情况下对于共享变量读写(实际是共享变量对应的内存操作)的规范,主要是为了解决多线程可见性、原子性的问题,解决共享变量的多线程操作冲突问题。
JVM运行时数据区,是Java虚拟机在运行时对该Java进程占用的内存进行的一种逻辑上的划分,包括方法区、堆内存、虚拟机栈、本地方法栈、程序计数器。这些区块实际都是Java进程在Java虚拟机的运作下通过不同数据结构来对申请到的内存进行不同使用。
1、线程共享(所有线程共享的)
1.1 堆
1)在虚拟机启动的时候创建,用于存放对象实例。
对可以按照可扩展来实现(通过-Xmx 和-Xms 来指定)
2)当堆中没有内存可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
3)堆内存划分
- 堆内存空间分为 年轻代 和 老年代 ,年轻代分为 Eden、From、To 三块,他们的大小比例约为Eden:From:To = 8:1:1,这个比例可以通过参数调整 [
-XX:InitialSurvivorRatio ]。- 年轻代与老年代的大小比例为 [ 年轻代:老年代 = 1 : 2]。
- 年轻代(minorGC)GC时会有大量对象被回收,用复制算法,将年轻代分为Eden、fromSurvivor、toSurvivor三个区域,对象生成时在Eden区存储,当垃圾回收后,Eden区经过回收后仍然存活的对象和其中一个survivor区的对象全部复制到另一个survivor区,以此往复,每一次垃圾回收,存活的对象年纪+1,当这些存活对象的年纪到15时,就会被移动到老年代中存储。;
- 老年代(majorGC)采用标记压缩算法;
- 年轻代装不下时minorGC,对象就会装到老年代,老年代不够时 majorGC,老年代也装不下会报OOM;
1.2 方法区
(1)Java虚拟机规范中定义了方法区的概念和作用,并没有具体的实现,不同的虚拟机有不同的实现;
(2)hotspot 虚拟机在jdk1.8之前,通过永久代来实现,在jdk1.8及以后,是在本地内存上取了一块空间,叫“元空间”来实现的。永久代和元空间都是对方法区的实现;
(3) 永久代物理是是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存;存储内容不同,元空间只存储类和类加载器的元数据信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。
1)用于存储已经被虚拟机加载的类信息,常量,静态变量等。
2)这个区域的内存回收目标主要针对常量池的回收和堆类型的卸载。
即时编译器编译后的代码存在方法区
2、线程私有(每条线程都拥有的)
-
程序计数器
是当前线程所执行字节码的行号指示器,这类内存也称为“线程私有”的内存。正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果是Natice方法,则为空。 -
虚拟机栈:
1)用于存储栈帧,每个方法执行时候生成栈帧,在栈帧里边存储了局部变量、操作数、动态链接、方法返回地址。
2) 每个方法从调用到执行完毕,对应一个栈帧在虚拟机栈中的入栈和出栈。通常所说的栈,一般是指在虚拟机栈中的局部变量部分。
3)局部变量所需内存在编译期间完成分配,
4)如果线程请求的栈深度大于虚拟机所允许的深度,则StackOverflowError。
5)如果虚拟机栈可以动态扩展,扩展到无法申请足够的内存,则OutOfMemoryError。
6) jvm栈一般分配的是8大基本类型和引用类型。但是编译器会做逃逸分析,如果一个局部对象不会被方法体以外的代码引用,那么分配内存空间的时候就直接栈上分配了,这样可以节约GC的性能。
- 本地方法栈
和虚拟机栈类似,主要为虚拟机使用到的Native方法服务。也会抛出StackOverflowError 和OutOfMemoryError。
3、内存区域划分
三、垃圾回收器
1. 垃圾回收器原理
1.1 垃圾的判断
1)可达性分析
通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。
jvm会将以下的对象定义为GC Roots:
- Java虚拟机栈中引用的对象:比如方法里面定义这种局部变量 User user= new User();
- 方法区中的静态属性引用的对象:比如 private static User user = new User();
- 常量引用的对象:比如 private static final User user = new User();
- 本地方法栈(JNI)中引用的对象;
2)引用计数法:
如果一个对象的引用计数为0,即:该对象没有被引用,说明它不太可能被引用到,可以回收。
【特点】:效率较高,但是它无法解决循环引用的问题
1.2 垃圾回收算法
1)标记清除(Mark-Sweep)
分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。首先从根开始将可能被引用的对象用递归的方式进行标记,然后将没有标记到的对象作为垃圾进行回收。
优点是容易实现,缺点是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
2)复制算法(copying)
为了解决Mark-Sweep算法的缺陷而提出的。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,实现简单,运行高效且不容易产生内存碎片,但可用内存缩减到原来的一半。Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。如图:
3)标记整理(标记压缩)Mark-Compact
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。如图:
2. 垃圾回收器介绍
Java 堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法;
年老代主要使用标记-整理垃圾回收算法,因此 java 虚拟中针对新生代和年老代分别提供了多种不
同的垃圾收集器,JDK1.6 中 Sun HotSpot 虚拟机的垃圾收集器如下:
(1) 年轻代
-
Serial:单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。采用的是Copying算法,它的优点是实现简单高效,但是缺点是会给用户带来停顿。
-
parNew :是Serial收集器的多线程版本,使用多个线程进行垃圾收集,使用复制算法,垃圾回收时必须暂停其他工作线程。
ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。【Parallel:平行的】;
ParNew虽然是除了多线程外和Serial 收集器几乎完全一样,但是ParNew垃圾收集器是很多 java虚拟机运行在 Server 模式下新生代的默认垃圾收集器。 -
Parallel Scavenge收集器: (并行收集器),它在回收期间不需要暂停其他用户线程,采用的是Copying算法,它主要是为了达到一个可控的吞吐量(吞吐量优先)。
高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而
不需要太多交互的任务。
(2) 老年代
- CMS(Current Mark Sweep)收集器: 是一种以获取最短回收停顿时间为目标的收集器,多线程,采用的是Mark-Sweep算法。
- 初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
- 并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
- 重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记 记录,仍然需要暂停所有的工作线程。
- 并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并
发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看 CMS 收集器的内存回收和用户线程是一起并发地执行。
-
SerialOld:单线程收集器,采用 Mark-Compact 算法。进行垃圾收集时,必须暂停所有用户线程。优点是实现简单高效,缺点是会给用户带来停顿.
这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。
-
Parallel Old收集器 :是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。
-
G1:它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。
相比于 CMS 收集器,G1 收集器两个最突出的改进是:
- 基于标记-整理算法,不产生内存碎片。
- 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。 G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾 最多的区域。区域划分和优先级区域回收机制,确保 G1收集器可以在有限时间获得最高的垃圾收 集效率。
- 常用组合
young | Tenured | JVM options |
---|---|---|
Serial | Serial | -XX:+UseSerialGC |
Parallel Scavenge | Serial | -XX:+UseParallelGC -XX:+UseSerialGC |
Parallel Scavenge | Parallel Old | -XX:+UseParallelGC -XX:+UseParallelOldGC |
Parallel New或Serial | CMS | -XX:+UseParNewGC -XX:+UseConcMarkSweepGC |
G1 | G1 | -XX:+UseG1GC |