一.Java内存模型
首先,我们回顾一下java的基本开发模式,我们知道我们写的所有的Java程序都保存在*.java文件中,即我们的源代码,但是呢,这些源代码,必须经过javac.exe命令将其编译成*.class文件,而后利用java.exe命令在JVM进程中解释此程序。
但是在这里流程中,又有自己的过程,如下图
实际上,当JVM将所需要的的.class文件加载到JVM进程之中,我们将需要一个类加载器 (ClassLoad),类加载器的好处在于:可以随便指定*.class文件所在的路径。
JVM:Java虚拟机,所有的程序都要求运行在JVM上,是因为考虑到了可移植性问题,但如果真在去执行程序,无法离开操作系统的支持。
在Java这种可以使用native实现本地C函数的调用(Native Interface),但是这些都是属于程序的辅助手段,真在的程序运行都在“运行时数据区”之中。
在整个的运行时数据区中,分为如下几个内存空间:
- 堆内存,保存所有引用数据的真实信息
- 栈内存,基本类型、运算、指向堆内的指针
- 方法区,所定义的方法信息都保存在方法区中,属于共享区;
- 程序计数器,是一个非常小的内存空间,用来保证程序依次指向
- 本地方法栈,每一次指向递归方法的时候,都会将上一个方法入栈;
例如:依次执行 A()->B()->C()->D()方法,那么进行本地方法栈的结构为:
- A->A先入栈
- BA->B入栈
- CBA->C入栈
- DCBA->D入栈
如果栈一直被占用到某种程度后,程序无法执行,抛出栈溢出错误 StackOverflowError
那么栈中我们村的是什么呢?
Java虚拟机栈(Java Virtual Machine Stacks)
- 栈内存时私有的,其生命周期和线程相同
- 虚拟机描述的是Java方法执行的内存模型,执行一个方法时产生一个帧,随后将其保存到栈(后进先出)的顶部,方法执行完毕后会自动将此栈帧进行出栈。栈顶部的栈帧就表示的是当前方法。如果群殴敏感期的栈深度过大,虚拟机可能会抛出StackOverflowError异常;如果虚拟机的实现中允许栈动态扩展,当内存不足以扩展栈时,会抛出OutOfMemoryError异常。
每个线程都有自己独立的空间,所以每个栈内存都是线程私有的。我们在JVM中用栈帧(Stack Frame)来定义栈的数据,每一个栈帧表示每个可能执行的方法。
而栈帧中包含了:局部变量表,操作树栈,指向运行时常量池的引用,方法返回地址和动态链接。
- 局部变量表(Local Variables):方法的局部变量或形参,其变量槽(solt)为最小单位,只允许保存32位长度的变量,如果超过32位则会开辟两个连续的solt(64位长度,long和double);
- 操作树栈(Operand Stack):表达式计算在栈中完成;
- 指向当前方法所属类的运行时常量池的引用(Reference to runtime constant pool):引用其他类的常量或使用String池中的字符串;
- 方法返回地址(Return Address):方法执行完毕后需要返回调用此方法的位置,所以需要再栈帧中保存方法返回地址;
在整个Java中存在对象池的概念,对象池是对整个常量池的一个规则破坏,因为在JVM启动时,所有的常量都已经分配好内存空间了,但是String中的 intern() 方法会打破这种限制,动态地进行常量池的内容设置;
当产生一个方法调用的时候,原本的方法会入栈,当方法执行完毕之后,方法将会进行栈帧的出栈,这样就能定义每个栈的详细信息。
Java内存管理:
a.JVM中的运行时数据区包括:
- 程序计数器
- Java栈
- 本地方法栈
- 方法区
- 堆
b.栈是运行时的单位,而堆是存储的单元。
- 栈因为是运行时的单位,里面存储的信息都是跟当前线程相关的信息。包括局部变量、程序运行状态、方法返回值等等;
- 堆只是保存对象信息
运行时数据区就是我们的Java内存管理,我们java能管理的地方只是在Java运行时数据区,其他我们无法控制,而Java运行时数据区的大小,我们可以根据自己的需求自行更改,但在其中,有些数据区是数据共享,有些数据区是对象独享,在整个操作中,对于运行时数据区直接和java的线程对象关联,所以,我们所说的Java内存调优都是在运行时数据区进行的,即共享的数据区越大越好,所以关键是在堆内存中,如果我们要真正做到对程序的理解,就需要对堆内存进行一定的控制。
二.Java对象访问模式
我们已经知道了java内存模型,而只靠内存模型,无法进行调优,因为jvm中充满了各种算法,其中就包括了java对象访问模式。
Java的引用类型是最为重要的数据处理模型,而整个数据引用类型数据处理之中会牵扯到:堆内存、栈内存、方法区。
以一个最简单的程序代码为主:
“Object obj = new Object()”,实例化了一个Object类对象。
“Object obj”,描述的是保存在栈内存中,这个数据还会保存在本地变量表中。
“new Object()”,一个真正的对象,保存在堆内存之中;
直观的思考整个引用的操作:
- 新定义的对象的名称保存在本地变量表
- 而后在这块区域里面需要确定好与之对应的栈内存
- 通过变量表中的栈地址可以找到堆内存
- 利用堆内存的对象进行本地方法的调用(方法区)
对于所有的引用数据类型的访问实际是有两种模式的。
Java中是没有句柄的,但这种模式的准确度很高,不足的是其过程较为繁琐。
所以,Java中直接利用对象保存模式,也就是说堆内存中,不需要构造句柄,而直接保存具体的对象。就相当于省略了句柄到对象之间的查找。而后这个对象可以直接进行Java方法区的调用。
当今实际上有三种JVM:
- SUN公司最早改良的HotSpot
- BEA公司的:JRockit(最初三个SUN公司的老员工创立)
- IBM的JVM S
而Oracle在收购SUN和BEA公司后,得到了两个虚拟机的版本。便将其合二为一,在JDK1.8开始,HotSpot和JRockit两者结合成现在的HotSpot。
范例:取得当前的JVM 版本(java -version)
java version “1.8.0_45”
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)
mixed mode:混合模式,指适合于编译和执行。
范例:使用纯解释模式启动(java -Xint -version)’
java version “1.8.0_45”
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, interpreted mode)
interpreted mode: 纯解释模式,不进行编译
范例:使用纯编译模式启动(java -Xcomp -version)
java version “1.8.0_45”
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, compiled mode)
compiled mode:纯编译模式,不进行解释
实际上现在JDK的设计都已经是开始为服务器准备的,因为对于JVM的启动模式有两种:
- -server:服务器模式,占用的内存大、启动速度慢,模式模式
- -client:本地单机运行模式,启动速度快
查询启动模式:
打开 jdk\jre\lib\amd64\jvm.cfg文件
-client KNOWN
-server KNOWN
对象的引用数据类型在 HotSpot中都是直接进行的引用处理,没有句柄池的概念。因为它能更快的进行对象的操作。
三.JVM垃圾收集
我们已经知道了Java的执行流程和java对象的访问模式,现在开始垃圾处理了。
首先,Java中最大的特点在于其具备良好的垃圾收集特性,也就是说GC是Java最重要的保证,它能让再愚蠢的开发者也能写出合理的代码来。
整个JVM的GC处理机制:对于不需要的对象进行标记,而后进行清除。
首先,我们来看一下JDK之前的内存结构图(非常重要):
然后,我们在看下JDK 1.8之后的内存结构(非常重要):
一定要记住,在JDK 1.8之后将最初的永久代内存空间取消了(取消永久代的目的:是为了将HotSpot与JRockit两个虚拟机标准合成一个)。
在整个JVM堆内存之中实际上将内存分为三块:
年轻代:新对象和没达到一定年龄的对象都在年轻代
老年代:被长时间使用的对象,老年代的内存空间比年轻代更大
元空间:像一些方法中的操作临时对象等,直接使用物理内存
最初的永久代是需要在JVM堆内存里面进行划分;
四.JVM垃圾回收流程
我们所有的数据都会保存在JVM堆内存之中,但是实际的开发中会经常创建很多的临时对象和常驻对象。所以,为了保证GC的性能问题,对于GC的处理流程如下图所示
对于整个GC流程里,最需要处理的就是年轻代和老年代的内存清理,而元空间都不在GC范围内;
- 当有一个新的对象产生,那么对象一定需要内存空间,于是现在就需要为对象进行内存空间的身躯。
- 首先会判断伊甸园区是否有空间,如果此时有内存空间,则将新对象保存在伊甸园区;
- 但是如果伊甸园区的内存空间不足,那么会自动执行一个Minor GC操作,将伊甸园区无用的内存空间进行清理,当清理之后会继续判断伊甸园区的内存空间是否充足?充足则将新的对象进行空间分配。
- 如果执行的Minor GC之后发现伊甸园区的内存亦然不足,那么这个时候会进行存活区判断,如果存活区有剩余空间,则将伊甸园区的部分对象保存在存活区。随后继续判断伊甸园区的内存空间是否充足,如果充足,则在伊甸园区进行空间分配;
- 如果此时存活区也已经没有内存空间了,则开始判断老年区,如果此时老年区的空间充足,则将存活区中的活跃对象保存在老年代。而后存活区就会有空余空间,随后,伊甸园区将活跃对象保存在存活区之中。而后在伊甸园区里为新对象开辟内存空间。
- 如果这个时候老年代也满了,那么这个时候将产生Major GC(Full GC),进行老年代的内存清理;
- 如果老年代执行了Full GC之后,依然无法进行对象的保存,就会产生OOM()异常“OutOfMemoryError”。
面试题:请解释“StackOverflowError”和“OutOfMemoryError”的区别:
1.StackOverflowError:每当Java程序启动一个新的线程时,Java虚拟机会为他分配一个栈,Java栈以帧为单位保持线程的运行状态;当线程调用一个方法时,jvm压入一个新的栈帧到这个线程的栈中,只要这个方法还没有返回,这个栈帧就存在。如果方法的嵌套调用层次太多(如递归调用),随着java栈中的帧的多,最终导致这个线程的栈中所有的栈帧大小的总和大于-Xss设置的值,会产生StackOverflowError移除异常。
2.OutOfMemoryError:如上流程图和流程图描述。
五.Java堆内存调整参数(调优关键)
堆内存的参数调整
通过之前的分析可以发现,实际上每一块子内存中都会存在有一部分的可变伸缩区,其基本流程:如果空间不足,在可变范围之内可扩大内存空间,当一段时间之后发现内存空间没有那么紧张的时候,再将可变空间进行释放。所以在整个调整过程之中:
参数名称 | 描述 |
-Xms | 设置初始分配大小,默认为物理内存的"1/64" |
-Xmx | 最大分配内存,默认为物理内存的“1/4” |
-XX:+PrintGCDetails | 输出详细的GC处理日志 |
-XX:+PrintGCTimeStamps | 输出GC的时间戳信息 |
-XX:+PrintGCDateStamps | 输出GC的时间戳信息(以日期的形式,如2018-08-15T16:53:16.155+0800;) |
-XX:+PrintHeapAtGC | 在GC进行处理的前后打印堆内存信息 |
-Xloggc:保存路径 | 设置日志信息保存文件 |
在整个堆内存的调整策略之中,有经验的人基本只会调整两个参数:“-Xmx”(最大内存)、“-Xms”(初始化内存)。如果要取得这些内存的整体信息,直接利用Runtime类即可:
System.out.println("Max_memory="+Runtime.getRuntime().maxMemory()/(double)1024/1024+"M");
System.out.println("Total_memory="+Runtime.getRuntime().totalMemory()/(double)1024/1024+"M");
本机运行输出:
Max_memory=4040.0M(默认最大内存)
Total_memory=254.0M(初始化内存)
发现默认的情况下分配的内存是总内存的"1/4";而初始化为内存的“1/64”;那么也就是说整个内存的可变范围(伸缩区):254~4040M之间,那么现在可能造成程序性能下降。所以,最好能让伸缩区的大小为0;即让Max_memory和Total_memory保持一致。
Max_memory=2048.0M
Total_memory=2048.0M
那么这个时候就避免了伸缩区的可调整策略,从而提升了整个程序的性能;
范例:观察GC的详解日志(java -Xms2G -Xmx2G -XX:+PrintGCDetails)
Max_memory=1963.0M
Total_memory=1963.0M
Heap
PSYoungGen total 611840K, used 31488K [0x00000000d5580000, 0x0000000100000000, 0x0000000100000000)
eden space 524800K, 6% used [0x00000000d5580000,0x00000000d74401a0,0x00000000f5600000)
from space 87040K, 0% used [0x00000000fab00000,0x00000000fab00000,0x0000000100000000)
to space 87040K, 0% used [0x00000000f5600000,0x00000000f5600000,0x00000000fab00000)
ParOldGen total 1398272K, used 0K [0x0000000080000000, 0x00000000d5580000, 0x00000000d5580000)
object space 1398272K, 0% used [0x0000000080000000,0x0000000080000000,0x00000000d5580000)
Metaspace used 3179K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 344K, capacity 388K, committed 512K, reserved 1048576K
下面再编写一个代码,观察GC的触发操作:
范例:测试GC处理(请保存内存空间小)(-Xms10M -Xmx10M -XX:+PrintGCDetails)
Random random = new Random();
String str = "123";
while (true) {
str = str + random.nextInt(8888888) + random.nextInt(8888888);
str.intern();//强制产生垃圾
}
如果在开发之中你发现程序执行速度变慢,那么就需要对程序内存进行分析:
可视化工具: jvisualvm (命令行执行此命令) 或 jconsole
命令查看:jmap(jmap -heap PID)
他会将整个内存空间的情况进行取得;即日常开发下可用这两种方法进行查看调整堆内存;
如果不会调内存的话,可直接将 -Xms 和 -Xmx 调成一样大小即可。
六.年轻代
所有的新对象都会在年轻代产生,如果年轻代的空间不足,无法产生对象,则会引发小GC和主GC(全GC)。
存活区会分为两个大小相等的存活区,所有使用关键字new实例化的对象,一定会在伊甸园区进行保存。而对于存活区保存的一定是在伊甸园区保存好久,并经过好几次小GC还保存下来的活跃对象。那么这个将晋升到存活区中,存活区一定会有两块大小相等的空间。目的是一块存活区未来晋升,另外一块存活区为了对象回收。这两块内存空间一定有一块是空的。
在年轻代中使用的是MinorGC,这种GC采用的是复制算法;
年轻代GC实现算法——复制算法(Coping):复制采用的方式为从根集合扫描出存活的对象,并将找到的存活对象复制到一块新的完全未使用的空间中。
把一个空间的数据复制到另一个空间,然后进行清除,腾出空间。
根集合扫描:通过上述分析可以发现,伊甸园区中保存的对象,大部分都是临时对象,极可能频繁产生小GC,所以在HotSpot虚拟机中为了加快此空间的内存分配形式,产生了两种技术“Bump-The-Point”和“TLAB(Thread-Local-Allocation Buffers)”。
(1)Bump-The-Point:该技术的主要特点是跟踪在Eden区保存的最后一个对象,这个最后保存的对象一般会保存在Eden的顶部,这样在每次创建新对象时都只需要检查最后保存的对象是否足够的空间就可以很快的判断出Eden区中是否还有剩余空间,这种做法就可以极大的提高内存分配速度。
(2)TLAB:虽然“Bump-The-Point”算法可以提高内存分配的速度,但是这种做法并不适合多线程的操作情况。所以又采用了“TLAB”算法,将Eden区分为多个数据块,每个数据块分别使用“Bump-The-Point”进行对象保存于内存分配。
参数名称 | 描述 |
-Xmn | 设置年轻代堆内存大小,默认为物理内存的“1/64” |
-Xss | 设置每个线程栈大小,JDK 1.5之后没人为每个线程分配1M的栈大小,减少此数值可以产生更多的线程对象,但不能无限生成 |
-XX:SurvivorRation | 设置Eden与Survivor空间的大小比例,默认为“8:1:1”,不建议修改。 |
-XX:NewSize | 设置年轻代内存区大小 |
-XX:NewRatio | 设置年轻代与来年代的比率 |
范例:改变存活区的比例(-Xms10M -Xmx10M -XX:SURvivorRatio = 6 -XX:+ PrintGCDetails) ,大部分情况下无须改动。
年轻代的GC是小GC,小GC的算法是复制算法;总有一个存活区是空的,连个存活区的大小是相同的;伊甸园区和存活区的比例是 8:1:1,这个比例一般情况下无须改动。
七.老年代、永久代和元空间
7.1老年代
老年代主要接收由年轻代发送过来的对象,一般情况下,经过了数次Minor GC之后还保存下来的对象才会进入到老年代。当老年代内存不足时,将引发“major GC”,即“Full GC”。
在老年代里面会采用两种算法结合的模式实现GC的处理:整理-压缩。
7.1.1标记-清楚(Mark-Sweep)算法
从根集合开始扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未标记的对象,进行回收。
垃圾清理前:
垃圾清理后:
优缺点:在空间中存活的对象较多的情况下较为高效,但由于算法为直接回收不存货对象所占用的内存,因此会造成内存碎片。
7.1.2标记-压缩(Mark-Compact)算法
标记阶段与“标记-清楚”算法相同,但在清楚阶段有所不同。在回收不存活对象所占用的内存空间后,会将其他所有存活对象都往左端空闲的空间进行移动,并更新引用其对象的指针。
垃圾清理器:
垃圾清理后:
优缺点:在“标记-清楚”的基础上还需要进行对象移动,成本相对较高,好处则是不产生内存碎片。
以后在老年代存储的时候尽可能保存长期会被使用,并且不会被轻易回收的大对象。
参数名称 | 描述 |
-XX:NewRatio | 设置年轻代与老年代的比率 |
-XX:+UseAdaptiveSizePolicy | 控制是否采用动态控制策略,如果动态控制,则动态调整Java堆中各个区域的大小及进入老年代的年龄 |
-XX:PretenureSizeThreshold | 控制直接进入老年代的对象大小,大于这个值的对象会直接分配在老年代中。 |
范例:设置老年代参数 (java -Xms2G -Xmx2G -XX:+PrintGCDetails -XX:PretenureSizeThreshold=512k -XX:NewRatio=2 TestDemo)
7.2 永久代(JDK1.8 后消失)
虽然JAVA的版本是JDK1.8,但是JavaEE的版本还是JDK1.7,也就是说,在JavaEE里面必须对永久代进行设置。永久代也是在堆内存中保存的,但是永久代不会被回收,例如:intern()方法产生的对象不会被回收。如果操作不当,导致永久代中的数据量过大,那么这个程序会报00M问题。一般情况下不会出现这种问题;
参数名称 | 描述 |
-XX:MaxPermSize | 设置永久代的最大值 |
-XX:PermSize | 设置永久代的初始大小 |
范例:设置永久代参数(java -XX:MaxPermSize10M TestDemo)
在JDK1.8之中设置永久代会报出错误:
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize10M; support was removed in 8.0
7.3元空间
元空间时JDK 1.8之后才有的,功能和永久代类型。唯一的区别是,永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制。
参数名称 | 描述 |
-XX:MetaspaceSize | 设置源空间的初始大小 |
-XX:MaxMetaspaceSize | 设置源空间的最大容量,默认是没有限制的(受本机物理内存限制) |
-XX:MinMetaspaceFreeRatio | 执行GC之后,最小的剩余元空间的百分比,减少为分配空间所导致的垃圾收集 |
-XX:MaxMetaspaceFreeRatio | 执行GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集 |
范例:设置一些参数,让元空间出错(java -XX:MaxMetaspaceSize=1m XX:MetaspaceSize=1m TestDemo)
此时会报出“OutOfMemoryError:Metaspace”,元空间内存不足。
八.JVM垃圾回收策略
在此之前,我们已经知道了年轻代使用的Minor GC和老年代的Major GC(Full GC),但在JVM中还有六种可以GC的方式。
年轻代可用GC策略:
- 串行GC(Serial Copying)
- 并行回收GC(Parallel Scavenge)
- 并行GC(ParNew)
老年代可以GC策略:
- 串行GC(Serial MSC)
- 并行GC(Parallel MSC,Parallel Mark Sweep、Parallel Compacting)
- 并发GC(CMC,Concurrent Mark Sweep GC,CMS GC)
需要注意的是,这些回收策略都有固定的配置方法,但是需要对这些策略的算法进行了解;
8.1年轻代-串行GC(Serial Copying)
算法:复制(Copy)
过程:
- 扫描出新生代中存活的对象
- Minor GC 将存活的对象复制到名为“To Space”的“S0/S1”区
- To space/From Space 区对换角色
- 经历过几次Minor GC仍然存活的对象,放入老年代
8.2年轻代-并行回收GC(Parallel Scavenge)
算法:复制(Copying)清理算法
操作步骤:在扫描和复制时均参与多线程方式处理,并行回收GC为空间较大的年轻代回收提供许多优化。
优势:在多CPU的机器上其GC耗时会比串行方式端,适合多CPU、对暂停时间要求较短的应用。
在年轻代使用并行GC处理的时候会产生一个污水处理厂的暂停,在进行对象回收的时候暂停其他线程。
8.3年轻代-并行GC(ParNew)
算法:复制(Copying)清理算法
操作过程:并行GC(ParNew)必须结合老年代“CMS GC”一起使用。因为在年轻代如果发生了“Minor GC”时,老年代也需要使用“CMS GC”同时处理(并行回收GC并不会做这些)。
CMS(Concurrent Mark Sweep),是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收期。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。
8.4老年代-串行GC(Serial MSC)
算法:标记-清楚=压缩(Mark sweep Compact)
步骤:
- 扫码老年代中还存活的对象,并对这些对象进行标记
- 遍历整个老年代内存空间,回收所有为标记的对象内存
- 将所有存活对象都集中在一端,而后将所有回收对象的内存空间变为一块连续的内存空间
优缺点:串行执行的过程中为单线程,需要暂停营业并耗时较长。
8.5老年代-并行GC(Parallel MSC,Parallel Mark Sweep、Parallel Compacting)
算法:标记-压缩(Mark Compacting)
步骤:
- 将来年代内存空间按照线程个数划分为若干个子区域。
- 多个线程并行对各自子区域内的存活对象进行标记
- 多个线程并行清楚所有未标记的对象
- 多个线程并行将多个存活对象整理在一起,并将所有被回收的对象空间整合为一体。
优缺点:多个线程同时进行垃圾回收可以缩短应用暂停时间,但是由于老年代的空间一般较大,所以在扫描和标记存活对象上需要花费较长时间。
8.6老年代-并发GC(CMC,Concurrent Mark Sweep GC,CMS GC)
算法:标记-压缩(Mark Compacting)
步骤:
- 初始标记(STW Initial Mark):虚拟机暂停正在执行的任务(STW),由根对象扫描出所有的关联对象,并做出标记。此过程只会导致短暂的JVM暂停。
- 并发标记(Concurrent Marking):恢复所有暂停的线程对象,并且对之前标记过的对象进行扫描,取得所有跟标记对象有关联的对象。
- 并发预清理(Concurrent Precleaning):查找所有在并发标记阶段进入老年代的对象(一些对象可能从新生代晋升到老年代,或者有一些对象被分配到老年代),通过重新扫描,减少下一阶段的工作。
- 重新标记(STW Remark):此阶段会暂停虚拟机,对在“并发标记”阶段被改变引用或者新创建的对象进行标记;
- 并发清理(Concurrent Sweeping):恢复所有暂停的应用线程,对所有未标记的垃圾进行清理,并且会尽量将已回收对象的空间重新拼凑为一个整体。在此阶段收集器线程和应用程序线程并发执行。
- 并发重置(Concurrent Reset):重置CMS收集器,等待下一次垃圾回收。
8.7 常用GC策略
运行环境 | 年轻代GC | 老年代 |
单机程序(client) | 串行GC(Serial Copying) | 串行GC(Serial MSC) |
服务器程序(server) | 并行回收GC(Parallel Scavenge) | 并行GC(Parallel Mark Sweep、Parallel Compacting) |
九.JVM垃圾回收策略参数配置
清楚了解了整个可以使用的回收策略之后,如果想要对GC进行合理的回收策略控制,可通过如下的几个参数进行控制:
参数名称 | 年轻代GC效果 | 老年代与元空间GC效果 |
-XX:+UseSerialGC | 串行GC(Serial Copying) | 串行GC(Serial MSC) |
-XX:+UseParallelGC | 并行回收GC(Parallel Scavenge) | 并行GC(Parallel Mark Sweep、Parallel Compacting) |
-XX:+UseConcMarkSweepGC | 并行GC(ParNew) | 并行GC(Concurrent Mark-Sweep GC、CMS GC),当出现Concurrent Mode Failure时采用串行GC(Serial MSC) |
-XX:+UseParNewGC | 并行GC(ParNew) | 串行GC(Serial MSC) |
-XX:+UseParallelOldGC | 并行回收GC(Parallel Scavenge) | 并行GC(Parallel Mark Sweep、Parallel Compactinh) |
并行操作的时候可以设置使用的CPU的数量;
范例:查看默认的回收策略 (java -Xms2G -Xmx2G -XX:+PrintGCDetails TestDemo),年轻代使用的是并行回收策略,老年代使用的是并行GC策略
范例使用串行GC策略(java -Xms2G -Xmx2G -XX:+UseSerialGC -XX:+PrintGCDetails TestDemo)
范例使用并行GC策略(java -Xms2G -Xmx2G -XX:+UseParallelGC -XX:+PrintGCDetails TestDemo)
此时如果使用CMS的处理操作,则年轻代使用传统的并行GC策略,而老年代使用CMS,这样对整个程序的暂停时间会非常短暂,适用于响应速度快的程序。
如果程序没有特别的要求,建议使用默认,但以上所有的策略都是原始的GC策略,他们都需要扫描全部子内存空间。
十.G1收集器
G1收集器(Garbage First)是从JDK 1.7 u4 版本之后正式引入到Java中的垃圾收集器,此类垃圾收集器主要应用在多CPU以及大内存的服务器环境下,这样可以极大的减少垃圾收集的停顿时间,以提升服务器的操作性能。引入此收集器的主要目的是为了在将来的某一个时间内可以替换掉CMS(Concurrent Mark Sweep)收集器。
G1区域划分:G1垃圾收集器采用的是区域化、分布式的垃圾收集器。其核心思想为将整个堆内存区域划分为大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小(区域大小范围“1M~32M”,最多可以设置2048个区域,即支持的最大内存为:“32M*2048=65536M”、64G内存),这样Eden、Survivor、Tenured就变为了一系列不连续的内存区域,也就避免了全内存区的GC操作。
在G1收集器中不再区分所谓的年轻代、老年代内存空间。所有的内存空间就是一块,但是划分为不同的子区域。所以,相较于其他的策略,更加便利。
虽然G1收集器里面将整个内存区域都混合在了一起,但是其本身也是在小范围内进行年轻代和老年代的区分,就是说依然会采用不同的GC方式来处理不同的子区域。
不用的内存区将释放,有一些数据直接拷贝到老年代。
所有的垃圾内存的保存区域有可能会被清洁后重新分配;
老年代的处理流程不一样,因为任何时候如果想要标注老年代的不用内存空间,都需要进行一些暂停,而G1中最大好处是他不用全内存扫描,只是区域性扫描。
清楚了G1的基本运行之后,那么下面进行一些G1的配置。
但是目前可能还不成熟,所以谨慎使用。而最好是在80G内存的机器上使用。
参数名称 | 描述 |
-XX:G1HeapRegionSize=n | 设置G1区域的大小,每个区域大小可选范围:1M~32M。目标是根据最小的堆内存大小划分出约2048个区域。 |
-XX:MaxGCPauseMilis=n | 设置回收的最大时间 |
-XX:G1NewSizePercent=n | 设置年轻代最小使用的空间比率,默认为Java对内存的5% |
-XX:G1MaxNewSizePercent=n | 设置年轻代最大使用的空间比率,默认为Java对内存的6% |
-XX:ParallelGCThreads=n | 设置STW工作线程数的值,与使用的CPU数量有关,最大值为8,。如果CPU数量超过8个,则做多可以设置总CPU数量的“5/8” |
-XX:ConcGCThreads=n | 设置并行标记线程数 |
-XX:InitiatingHeapOccupancyPercent=n | 设置占用区域的百分比,超过此百分比将触发GC操作,默认为45% |
-XX:NewRatio=n | 设置年轻代与老年代的比率(Yong:Tenured),默认为2 |
-XX:SurvivorRatio=n | 设置Eden与Survivor的比率(Eden:Survivor),默认为8 |
-XX:MaxTenuringThreshold=n | 新生代保存到老年代的岁数 |
-XX:G1ReservePercent=n | 设置预留空间的空间百分比,以降低目标空间的溢出风险,默认为10% |
使用G1回收期范例
java -Xmx10m -Xms10m -XX:+UseG1GC -XX:+PrintGCDetails TestDemo
G1处理和传统的垃圾收集策略是不同的,关键的因素是它将所有的内存进行了区域性的划分。
十一.Java引用类型
引用类型可以说是整个Java开发的灵魂所在,如果没有合理的操作,那么就可能产生垃圾问题,但是对于引用也需要一些合理化的设计。
在很多的时候并不是所有的对象都需要我们一直使用,那我们就需要对引用的问题做进一步的思考。所以从JDK 1.2之后,关于引用提出了4种方案:
- 强引用:当内存不足的时候,JVM宁可出现OutOfMemory错误停止,也需要进行保存,并不会将此空间回收;如new一个对象;
- 软引用:当内存不足时,进行对象的回收处理,往往用于高速缓存中
- 弱引用:不管内存是否紧张,只要有垃圾,则立即回收
- 幽灵引用:和没有引用是一样的
11.1 强引用
JVM默认的引用模式。即:在引用期间内,如果该堆内存被指定的栈内存有联系,那么该对象无法被GC所回收,而一旦出现了内存空间不足,就会出现“OutOfMemoryError”错误信息;
范例:观察强引用
Object object = new Object();//强引用,默认支持
Object obj = object;//引用传递
object = null;//断开一个连接
System.gc();
System.out.println(obj);
如果此堆内存有一个栈内存指向,那么该对象将无法被该GC回收。
强引用是我们一直使用的引用的模式,并且也是以后常使用的引用模式,正因为强引用存在这种内存分配异常问题,所以尽量减少实例化对象。
11.2 软引用
在许多的开源组件中,往往使用软引用作为缓存组件出现,其最大特点在于:不足时回收,不充足时不回收。要想实现软引用,则需要一个单独的类来控制:java.lang.ref.SoftReference
构造: public SoftReference(T referent)
取出数据:public T get()
范例:观察软引用
如果此时内存空间充足,那么对象将不会回收,否则会进行回收处理
11.3 弱引用
本质含有是只要一进行GC,那么所引用的对象将会被立刻回收。弱引用需要使用的是Map接口的子类。
一旦出现GC,则必须进行回收处理。