对于中级程序猿或者高级程序猿,可以说jvm这块知识是必须掌握的了吧,毕竟在跳槽找工作面试时,都会提问关于jvm的相关知识,与其死记硬背,不如静下心来主动捅破这层窗户,深入了解下关于jvm的知识点;
jvm的内存结构主要分为 4部分,其中主要分析的是运行时数据区。运行时数据区包含 程序计数器,堆,栈,本地方法栈以及方法区,其中堆和方法区是线程共享的,其他的3种是线程私有的。jvm的优化主要发生在 堆和方法区。
程序计数器 是线程私有的,他是一块比较小的内存空间,可以将它看成是当前线程执行字节码的行号显示器。更加确切的说,一个线程的执行,是通过字节码解释器来改变当前线程的计数器的值,来获取到下一条需要执行字节码的指令,从而保证线程的正常执行;
如果执行的是java程序,那么显示的是正在执行的虚拟机字节码指令地址,如果执行的是Native 程序,那么展示的是undefined ;
了解虚拟机栈之前,首先了解下什么是栈?
栈 是一种仅能在表头进行插入和删除的线性表结构,及所说的栈是一种先进后出的;
栈是线程私有的,它的生命周期是和线程绑定在一起的,线程执行前,都会分配一个独立的栈空间;
栈的元素是栈帧,每个方法在执行时都会创建一个栈帧,栈帧里主要存储了局部变量表,操作数栈,方法出口以及动态连接等信息;其实每个方法在调用执行的时候,就对应了一个入栈和出栈的过程。
在栈帧中,是由局部变量表进行存储数据的,那么局部变量表主要存储了基本数据类型的局部变量,对象的引用,但是局部变量表中并不存储对象的内容。局部变量表在程序进行编译时,进行内存空间的分配。
操作数栈的元素是java 中的任意数据类型,在编译时,操作数栈是空的,在方法执行的过程中,通过字节码指令完成操作数栈的入栈和出栈过程。通常在进行算术运算的时候是通过操作数栈完成的。所以可以说操作数栈是用于计算的临时储存区;
栈的异常主要有两种:
StackOverflowError:栈溢出错误
线程所需要的栈内存大小> 配置允许的栈内存大小,那么java虚拟机会抛出 栈内存溢出溢出
OutOfMemoryError:内存不足
栈内存在进行扩展时,申请的内存大于剩余的内存时,会抛出内存不足的异常;
在面试过程中,会提问到,为什么使用递归会造成栈溢出? 原因是,递归每次调用时,都会创建一个栈帧,这样会导致,递归层数越深,所需要的栈的大小超过jvm设置的栈大小,所以会抛出StackOverflowError的异常;
堆是完全二叉树,也就是说除了树的最后一层节点不需要是满的,其他的每一层从左到右都必须是满的。
在java中,堆是java 虚拟机管理最大的内存区域,它是线程共享的,主要存放了new 关键字创建的对象。所有对象的实例都是在堆上进行分配的,它也是发生垃圾回收的主要区域;
java 堆又分为年轻代(Young Generation) ,年老代(Old Gereration,且年轻代和年老代的比例是1:2 的关系。在年轻代中又分为 Eden , to Survivor ,from Survivor 空间,他们的关系是8:1:1 的关系。
在年轻代中主要存储的是“新生对象”,当年轻代满了之后,会出发minor gc 来清理年轻代内存;
在年老代中主要存储的是长期存活的对象或者大对象,所以在新生代多次未清理掉的对象会进入年老代中。当年老代满了之后,会出发full gc;
需要注意的是,full gc不单单是只清理年老代,而是清理整个内存空间,包含年轻代和年老代。如果full gc 清理之后,还是无法存储对象,那么就会发生我们常见的内存溢出;
方法区是同java 的堆一样,是线程共享的,主要用来存储已经被加载的类的信息,常量,静态变量,以及及时编译器编译后的代码。可以更形象的说 静态变量,常量,类信息,常量池都是存在方法区中的;
需要注意的是,在jdk1.8 之后,取消了方法区,而取而代之的是MetaSpace 元空间,元空间不是存在jvm中的,而是存在于本地内存中的;
既然要进行垃圾回收,首先需要判断哪些是垃圾。在java 中,判断是否是垃圾有两种方式,分别是 引用计数法,可达性分析 。
引用计数法很简单,就是一个对象被引用时加一,去除引用时减一,这样我们就可以进行判断引用计数是否为零 来判断当前对象是否是垃圾了。
已用计数方法虽然简单,但是存在一个问题,在进行循环引用时,无法判断是否为垃圾对象。例如:a ,b两个对象,a引用了 b, b引用了a ,但是a ,b对象未被其他对象引用,理论上来讲,a ,b 两个对象是属于垃圾回收范围内的,但是他们的计数器并不为0 ,所以无法将a ,b 两个对象进行垃圾回收;
可达性分析的思路是通过 “GC Roots ” 的对象作为起点,从这个起点向下搜索,所搜索的路径称为“引用链” ,当一个对象没有任何引用链的时候,那么证明这个对象是不可用的,则会被垃圾回收掉;
在java 中,可以作为 “GC Roots” 的对象主要有几下几种:
垃圾回收算法主要有: 标记-清除,标记-整理,复制 ,分代收集算法。
标记清除算法主要包含了标记和清除两个阶段。标记阶段是由GC Roots 触发的可达对象。清除阶段是清除未被标记的对象。
缺点: 标记清除会产生内存碎片问题,如果内存碎片过多,会导致内存地址不连续,在后续内存分配是效率会降低;
标记整理和标记清除很相像,只不过标记整理是在标记完之后,会将存活的对象向一边移动,然后清理未标记的对象
缺点:整理时比较耗时
复制算法是每次都将内存划分两块,每次只使用其中的一块,当这块用完时,会将存活的对象复制到另外一块内存上,然后将已经使用过的进行一次内存清理。
缺点: 标记占用内存,当对象存活率较高时,需要频繁的去复制,效率变低;
分代收集算法是针对不同的分代,采取不同的收集方法,在“新生代” 中以为对象存活率低,所以采用“复制算法” 进行收集,在“年老代” 中,因为对象存活率高,所以采用“标记-清除”,“标记-整理”算法。
新生代分为Eden 和两个大小相同的Survivor 区,所有新创建的对象都存放下Eden区,当Eden 区满了之后,会触发Minor Gc,然后会将仍然存活的对象移到其中一个Survivor 区,并始终保证一个Survivor 为空。如果在Survivor 区存放不了,那么Gc 收集器会将这些对象直接存放在Old 区,如果在Old 区也满了之后,就会触发full gc 了。full gc 会进行触发清理整个堆内存;
新生代的回收器主要有:Serial、ParNew、Parallel Scavenge(这三种算法都采用了标记-复制算法)
老年代回收器主要有:CMS、Serial Old、Parallel Old
分代收集:G1
CMS收集的步骤为:
初始标记–>并发标记–>重新标记–>并发清除
初始标记:需要停止运行程序的,初始标记仅仅只是标记一下GC Roots能关联到的对象。
并发标记:进行GC Roots Tracing 的过程,就是追踪与GC Roots关联的对象是否还在使用中。
重新标记:在上一步并发标记时,程序是在运行的,会产生新的垃圾对象,对象地址也有所变动,所以需要重新标记一下,但是这样也会导致程序暂停。
并发清除:这个阶段就是清除垃圾对象,因为是并行运行的,所以这时也会产生新的垃圾,但是这些垃圾是清理不掉的,因为没有被标记,所以只有等下次GC回收。
CMS清除算法有2个缺点:
G1回收算法(慢慢要替代CMS回收算法)
为解决CMS算法产生空间碎片和其它一系列的问题缺陷.G1中提供了三种模式垃圾回收模式,young gc、mixed gc 和 full gc,在不同的条件下被触发。
发生在年轻代的GC算法,一般对象(除了巨型对象)都是在eden region中分配内存,当所有eden region被耗尽无法申请内存时,就会触发一次young gc.
当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器
缺点:G1垃圾回收器对内存的要求较高,一般建议4G以上使用
jps 命令可以列出所有的java 进程。
除此之外,还有一些参数可以自定义输出:
-q 指定jps只输出进程ID
-m 输出传递给Java进程的参数
-l 输出主函数的完整路径
-v 显示传递给Java虚拟机的参数
jstat 用于观察 Java 堆信息的详细情况
命令: jstat -option 进程id
参数 | 含义 |
---|---|
-class | 监视类装载、卸载数量、总空间以及类装载所耗费的时间 |
-gc | 监视Java堆状况,包括Eden区、两个Survivor区、老年代、永久代等的容量、已用空间、GC时间合计等信息 |
-gccapacity | 监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间 |
-gcutil | 监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比 |
-gccause | 与-gcutil功能一样,但是会额外输出导致上一次GC产生的原因 |
-gcnew | 监视新生代GC状况 |
-gcnewcapacity | 监视内容与-gcnew基本相同,输出主要关注使用到的最大、最小空间 |
例:
jstat -gcutil 2365
S0 | S1 | E | O | P | YGC | YGCT | FGC | FGCT | GCT |
---|---|---|---|---|---|---|---|---|---|
0.00 | 0.00 | 12.05 | 0.00 | 14.71 | 0 | 0.000 | 0 | 0.00 | 0.00 |
其中每个选项的意义如下:
S0、S1 表示Survivor0、Survivor1,还未使用。
E 表示Eden区使用了12.05%的空间。
O 表示老年代还未使用。
P 表示永久代使用了14.17%的空间
YUC、YGCT 表示从程序运行以来一共发生了0次Minor GC(YGC,Young GC),总共耗时0秒。
FGC、FGCT 表示从程序运行以来一共发生了0次Full GC(FGC,Full GC),总共耗时0秒。
命令: jinfo -option pid
jmap 是一个多功能命令,可以生成 Java 程序的 Dump 文件,也可以查看堆内对象实例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列。
命令: jmap -option pid
jhat 命令用于分析 Java 应用的对快照内存。Sun JDK 提供了 jhat 命令与 jmap 搭配使用,来分析 jmap 生成的堆转储快照。jhat 内置了一个微型的 HTTP/HTML 服务器,生成 dump 文件的分析结果后,可以在浏览器中查看( http://localhost:7000)