JVM虚拟机垃圾回收
JVM虚拟机的基本结构:
JDK1.8版本:
先说说与JDK1.7版本的细微区别:
1. 没有了方法区,取而代之的是元空间;
2. 原来方法区中的运行时常量池中的字符串常量池,静态变量都存在了堆中;
3. 方法区中的其它的(例如类加载信息)存放在元空间中,运行时元空间存在于直接内存中;
JVM内部各个组成介绍:
堆(Heap):
1. JVM中所管理内存中最大的一块,在虚拟机启动的时候创建;
2. 唯一的目的是存放对象实例,几乎所有的对象实例和数组都是在这里分配内存;
3. 堆是垃圾收集管理的主要区域,所以也被称为“GC”;
4. 原来方法区中的运行时常量池中的字符串常量池和静态变量,都存放在堆中;
方法区:
1. 在JDK1.7之前运行时常量池存放在方法区,此时的hotspot虚拟机堆方法区的实现为永久代;
2. 在JDK1.7时,字符串常量池被拿到了堆中,剩余的东西还是在方法区,也就是hotspot中的永久代;
3. 在JDK1.8,hotspot移除了永久代,使用元空间取代,字符串常量池还在堆中,运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间;
ps: 静态常量池(class文件常量池) 在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用; 运行时常量池: 在类加载完成后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是每个class都有一个运行时常量池,类在解析之后,将符号引用替换为直接引用,与全局常量池中的引用值保持一致; 字符串常量池: 全局字符串池中的内容是在类加载完成,经过验证,准备阶段之后再堆中生成字符串对象实例,然后将字符串对象实例的引用值存放到字符串常量池中,需要注意的是这里面存放的是引用值而不是具体的实例对象;
元空间(metaspace):
在JDK1.8中,永久代已经不存在,存储的类信息,编译后的代码数据已经移动到了元空间中,元空间并不在堆中,而是在本地内存中,元空间的本质和永久代类似,都是对JVM规范中的方法区的实现,不过元空间和永久代的区别是:元空间并不在虚拟机中,而是使用本地内存;
为什么要使用元空间,取消永久代? 1.字符串存在永久代中,容易出现性能问题和内存溢出; 2.不会再有java.lang.OutOfMemoryError:PermGen的问题; 3.类及方法的信息等比较难确定其大小,因此永久代的大小指定比较困难,太小容易出现永久代溢出,太大容易导致老年代溢出;
PC寄存器:(程序计数器)
PC寄存器是保存程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计算器中得到当前需要执行的指令所在的存储单元的地址,然后根据得到的地址获取指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直到执行完所有的指令;
Java栈(虚拟机栈):
java栈是java方法执行的内存模型,主要存放局部变量,操作数栈、动态链接、以及是方法的出口;
本地方法栈:
本地方法栈与java栈作用相似,区别是java栈是为java方法服务的,而本地方法栈是为执行本地方法服务的;
JVM堆空间布局:
JDK1.7版本:
在jdk1.8版本中,取消了永久代,使用了元空间取而代之;
ps:为什么要堆内存要分代? 1. 不同类型的对象的生命周期是不同的; 2.如果不分代,每次垃圾回收都需要遍历整个堆内存空间,花费时间较长,效率较低; 3.不同年代的对象采用不同的垃圾回收方式,以便提高回收效率;
年轻代:
1. 新生成的对象首先是分配在年轻代的;
2.年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象;
3.Eden:Eden是用来存放JVM刚分配的对象;
4.Survivor1和Survivor2:2个Survivor的空间一样大,当Eden中的对象经过垃圾回收没有被回收掉时,会在2个Survivor之间来回copy,当满足某个条件时,比如copy的次数,就会copy到Tenured;
老年代:
1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代;
2.老年代中存放的都是一些生命周期较长的对象,比如:Session对象、Socket连接等;
怎么判断对象为垃圾对象?
1.引用计数算法
给每一个创建的对象增加一个引用计数器,每当有一个地方引用它的时候,这个计数器就加1,而当引用失效的时候,计数器就减1,当这个计数器的值为0的时候,就是这个对象没有任何地方在使用它了,那就是一个无效的对象了,可以进行垃圾回收。
2.根搜索算法:
通过一系列名为“GC Root”的对象作为终点,当一个对象到GC Roots之间无法通过引用到达时,那么该对象便可以进行回收了。
如何进行垃圾回收(垃圾回收的算法)?
1.标记并清除
分为“标记”和“清除”两个阶段,首先根据上面的算法标记出所有需要回收的对象,在标记完成后,统一回收掉所有的 被标记的对象。
缺点:
①效率低:标记和清除这两个过程效率都不高。
②容易产生内存碎片:因为内存的申请通常是不连续的,那么清除一些对象后,那么就会产生大量不连续的内存碎片。
2.复制算法
将可用内存按容量划分为大小相等的两块区域,每次只使用其中一块,当这一块的内存用完了,就将活着的对象复制到另一块区域中,然后把已使用的内存空间一次性清理掉。这样的话,就不用考虑内存碎片的问题了,但是缺点就是一般的内存是闲置的,资源浪费严重,并且如果对象存活率较高的话,每次需要复制大量的对象,效率会变得很低。
3.标记整理算法
首先标记出所有存活对象,然后让所有存活对象向一端移动,直接清理到端边界以外的内存,这种方式的只有对象的存活率比较高时,该算法的效率才会高。
4.分代算法
根据对象的生命周期不同将堆内存分为新生代和老年代,每个代采用不同的垃圾回收算法。
垃圾收集器:
Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:Serial Old、Parallel Old、CMS
整堆收集器:G1
名词解释: Minor GC:又称为新生代GC,指发生在新生代的垃圾收集动作,因为java对象大多数都是朝生夕灭,所以Minor GC会非常频繁,而且速度很快; Full GC:又称为Major GC或者老年代GC,指发生在老年代的GC;出现Full GC经常会伴随至少一次的Minor GC(不是绝对),Major GC速度比Minor GC要慢10倍以上;
Serial收集器:
采用的算法为标记并清除,单线程;是虚拟机运行在Client模式下的默认新生代收集器;
ParNew收集器:
ParNew收集器是Serial收集器的多线程版本,是虚拟机在Server模式下的默认新生代收集器;
Parallel Scavenge收集器:
Parallel Scavenge收集器是一个新生代的收集器,采用的是复制算法,关注点是吞吐量;
Serial Old收集器:
是一个老年代的收集器,同样是一个单线程收集器,采用的算法是标记并整理算法;
Parallel Old收集器:
是Parallel Scavenge收集器的老年代版本,使用多线程,采用的是标记并整理算法;
CMS收集器:
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,使用的算法是标记并清除算法
涉及到的参数配置:
JVM基本参数:
参数名称
|
含义
|
默认值
|
-Xms
|
初始堆大小
|
物理内存的1/64(<1GB)
|
-Xmx
|
最大堆大小
|
物理内存的1/4(<1GB)
|
-Xmn
|
年轻代大小
|
|
-XX:NewSize
|
设置年轻代大小
|
|
-XX:MaxNewSize
|
年轻代最大值
|
|
-XX:PermSize
|
设置永久代(perm gen)初始值 |
物理内存的1/64
|
-XX:MaxPermSize
|
设置永久代最大值 |
物理内存的1/4
|
-Xss
|
每个线程的堆栈大小
|
|
-XX:NewRatio
|
年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) |
|
-XX:SurvivorRatio
|
Eden区与Survivor区的大小比值
|
|
-XX:MaxDirectMemorySize
|
设置最大可用直接内存大小 |
|
垃圾收集器参数:
参数名称 | 含义 |
-XX:+UseSerialGC | 串行垃圾回收器 |
-XX:+UseParallelGC | 并行垃圾回收器 |
-XX:ParallelGCThreads | 并行收集器的线程数 |
-XX:+UseParallelOldGC | 年老代垃圾收集方式为并行收集 |
-XX:+UseConcMarkSweepGC | 设置年老代为并发收集 |
-XX:+UseParNewGC | 设置年轻代为并行收集 |
-XX:+UseG1GC | 使用G1垃圾回收器 |
JVM日志参数:
参数名称 | 含义 |
-XX:+PrintGC | 输出GC日志 |
-XX:+PrintGCDetails | 输出GC的详细日志 |
-XX:+PrintGCTimeStamps | 输出GC的时间戳(以基准时间的形式) |
-XX:+PrintGCDateStamps | 输出GC的时间戳(以日期的形式输出) |
-XX:+PrintHeapAtGC | 在进行GC的前后打印出堆的信息 |