一 . JVM架构图
JVM是Java Virtual Machine的简称,意为Java虚拟机。JVM有很多种,使用最为广泛的JVM为HotSpot。
如上面架构图所示,JVM分为三个主要子系统:
类加载器子系统(Class Loader Subsystem)
运行时数据区(Runtime Data Area)
执行引擎(Execution Engine)
一个对象的执行过程
1. 类加载器子系统
1.1 加载
jvm运行的文件是class文件,如何加载这个文件呢就用到了我们的类加载器,类加载器分为3种,
通过类加载器的加载和初始化将小class文件转化为大Class文件。
(1) Boot Strap类加载器:负责从引导类路径加载类,除了rt.jar,它具有最高优先级;
(2) Extension 类加载器:负责加载ext文件夹(jre \ lib)中的类;
(3) Application类加载器:负责加载应用程序级类路径,环境变量中指定的路径等信息。
当一个类被加载进来以后,首先会访问这个家在的父类加载器中是否加载了这个类,
父类加载器又会向上查看,他的父类加载器是否加载了这个类,知道父类加载器为null(Boot Strap),
我们把这种机制机制叫做双亲委派机制,使用双亲委派机制实现了java的沙箱安全,
是的不同的程序在加载相同的类是得到的结果是一致的。
比如我们自定义了一个String类,此时我们是无法使用到这个类定义的方法,
它被Application加载器加载后会往上找直到在boot strap加载器中找到它。
MyClass myClass = new MyClass();
System.out.println("maruis------>" + myClass.getClass().getClassLoader());
System.out.println("maruis------>" + myClass.getClass().getClassLoader().getParent());
System.out.println("maruis------>" + myClass.getClass().getClassLoader().getParent().getParent());
1.2 链接
(1) 验证(Verify):字节码验证器将验证生成的字节码是否正确,如果验证失败,将提示验证错误;
(2) 准备(Prepare):对于所有静态变量,内存将会以默认值进行分配;
(3) 解释(Resolve):有符号存储器引用都将替换为来自方法区(Method Area)的原始引用。
1.3 初始化
这是类加载的最后阶段,所有的静态变量都将被赋予原始值,并且静态区块将被执行。
-
运行时数据区
运行时数据区主要我分为5个部分:方法区域,堆区,java栈,java本地方法栈,程序计数器
其中最重要的部分是 堆和java栈
2.1 方法区(Method Area)
所有的类级数据,也就是类的结构信息将存储在这里,包括静态变量。每个JVM只有一个方法区,它是一个共享资源;
2.2 堆区域(Heap Area)
所有对象及其对应的实例变量和数组将存储在这里。每个JVM也只有一个堆区域。由于方法和堆区域共享多个线程的内存,所存储的数据不是线程安全的;
2.3 堆栈区(Stack Area)
栈管运行,堆管存储。
栈也叫栈内存,主管java程序的运行,是在线程创建时创建,它的生命周期随着线程的生命周期,线程结束内存也就释放了,对于栈来说
不存在垃圾回收的问题,因为线程结束,栈就会over,它是线程私有的,
8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
对于每个线程,将创建单独的运行时堆栈。对于每个方法调用,将在堆栈存储器中产生一个条目,称为堆栈帧。所有局部变量将在堆栈内存中创建。堆栈区域是线程安全的,因为它不共享资源。堆栈框架分为三个子元素:
局部变量数组(Local Variable Array):与方法相关,涉及局部变量,并在此存储相应的值
操作数堆栈(Operand stack):如果需要执行任何中间操作,操作数堆栈将充当运行时工作空间来执行操作
帧数据(Frame Data):对应于方法的所有符号存储在此处。在任何异常的情况下,捕获的区块信息将被保持在帧数据中;
2.4 PC寄存器(PC Registers)
每个线程都有单独的PC寄存器,用于保存当前执行指令的地址。一旦执行指令,PC寄存器将被下一条指令更新
2.5 本地方法堆栈(Native Method stacks)
本地方法堆栈保存本地方法信息。对于每个线程,将创建一个单独的本地方法堆栈。 -
执行引擎
分配给运行时数据区的字节码将由执行引擎执行,执行引擎读取字节码并逐个执行。
(1) 解释器:解释器更快地解释字节码,但执行缓慢。解释器的缺点是当一个方法被调用多次时,每次都需要一个新的解释;
(2) JIT编译器:JIT编译器消除了解释器的缺点。执行引擎将在转换字节码时使用解释器的帮助,但是当它发现重复的代码时,将使用JIT编译器,它编译整个字节码并将其更改为本地代码。这个本地代码将直接用于重复的方法调用,这提高了系统的性能。JIT的构成组件为:
中间代码生成器(Intermediate Code Generator):生成中间代码
代码优化器(Code Optimizer):负责优化上面生成的中间代码
目标代码生成器(Target Code Generator):负责生成机器代码或本地代码
分析器(Profiler):一个特殊组件,负责查找热点,即该方法是否被多次调用;
(3) 垃圾收集器(Garbage Collector):收集和删除未引用的对象。可以通过调用“System.gc()”触发垃圾收集,但不能保证执行。JVM的垃圾回收对象是已创建的对象。
Java本机接口(JNI):JNI将与本机方法库进行交互,并提供执行引擎所需的本机库。
二. 栈+堆+方法的交互关系
我们目前主要使用的jvm是HotSpot™
栈中存放的是8中数据类型,引用类型的变量还有方法地址
。
举例:
String s = new String(“hello”)和String s = “hello”;的区别?
==与equals()的区别:
==:比较引用类型比较的是地址值是否相同
equals:比较引用类型默认也是比较地址值是否相同,而String类重写了equals()方法,比较的是内容是否相同。
三. heap堆结构简介
存储逻辑
* * 新生去是累的诞生,成长,消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命,新生区又分为两个部分
* * 伊甸区和幸存者区,所有的类都是从伊甸区new出来的,幸存者区分为2个,幸存者0区和幸存者1区。
* * 当伊甸区的空间用完时,程序有需要创建对象,JVM的垃圾回收器就会对伊甸区中的垃圾进行回收(Minor GC),将伊甸区中不被使用的队形销毁。
* * 然后将伊甸区中的剩余对象存放到幸存者0区,幸存者0区也满了再对该区域进行垃圾回收
* * 把剩余的对象移动到1区,如果1区也满了就会进行gc,然后把数据移动到养老区,
* * 养老区满了,就会执行gc(FullGC)
* * 如果Fullgc之后,发现依然无法进行对象存储,就会产生OutofMemoryError异常
* MinoGC的全过程(复制->清空->换换)
* 1.eden、survivorFrom 复制到 survivorTo
* 首先,当Eden区满的时候就会触发第一次gc,把还活着大的对象复制到survivorFrom 区,并且清空Eden区,
* 当Eden区再次出发gc时,会清空eden和survivorFrom 区,并把依然存活的对象复制到survivorTo区(如果有对象的年龄已经达到了老年的标默认15次,就会被复制到老年区)同时吧这些对象的年龄+1,
* 2.清空eden,survivorFrom
* 然后,清空eden区和survivorFrom中的对象
* 3.survivorFrom和urvivorTo区交换
* 最后survivorFrom和survivorTo区进行交换,即谁空谁是to,survivorTo就会作为下一个survivorFrom
* 元空间(java7 叫永久存储区)
元空间(java7 叫永久存储区)
* 对应于方法区
* 是常驻内存的区域,用于存放jdk自身所携带的Class,Interface 的元(结构)数据,也就是说他存储的是运行环境不的类信息,被装载进此区域的数据是不会被垃圾回收期回收的,关闭JVM才会释放。
* 如: rt.jar 中的类结构就存储在元空间中。
四. JVM参数
4.1 JVM参数分类
jvm的参数可以分为3大类
-
标准参数,一直有的,比如:
-help
-server -client
-version -showversion
-cp -classpath -
X参数
-Xint : 解释执行
-Xcomp :第一次使用就编译成本地代码(java不是严格的解释性或者编译性执行的语言)
-Xmixed : 上面两种混合模式,有编译器自己去优化选择。 -
XX参数
1)Boolean类型格式:-XX:[+/-]<name> 表示启动或者禁用了name参数 比如:-XX:+UseConcMarkSweepGC表示启用了CMS垃圾回收器 -XX:+UseG1GC 表示启用了G1垃圾回收器
2)非布尔 kv型
格式:-XX:<name>=<value> 表示name参数的值是value 比如:-XX:MaxGCPauseMills=500表示GC最大的停顿时间是500 -XX:GCTimeRatio=19
4.2 堆参数的调整
实际情况中,对的最大内存和初始内存必须设置的一样,为了避免jvm内存忽高忽低,可能会有卡顿。
- 首先查看堆内存:默认最大内存是物理内存的1/4 最小内存是物理内存的1/64
long jvm_Xmx = Runtime.getRuntime().maxMemory();
long totleMemory = Runtime.getRuntime().totalMemory();
// 返回java虚拟机试图启用对的最大内存
System.out.println("maruis--JVM最大内存---->" + jvm_Xmx/1024/1024 +"M");
// 默认的堆内存的初始大小
System.out.println("maruis-- 默认的堆内存的初始大小---->" + totleMemory/1024/1024 +"M");
- 设置堆内存
-Xms4096m -Xmx4096m -XX:+PrintGCDetails
返回结果
maruis--JVM最大内存---->3925M
maruis-- 默认的堆内存的初始大小---->3925M
Heap
PSYoungGen total 1223168K, used 83886K [0x000000076ab00000, 0x00000007c0000000, 0x00000007c0000000)
eden space 1048576K, 8% used [0x000000076ab00000,0x000000076fceba88,0x00000007aab00000)
from space 174592K, 0% used [0x00000007b5580000,0x00000007b5580000,0x00000007c0000000)
to space 174592K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007b5580000)
ParOldGen total 2796544K, used 0K [0x00000006c0000000, 0x000000076ab00000, 0x000000076ab00000)
object space 2796544K, 0% used [0x00000006c0000000,0x00000006c0000000,0x000000076ab00000)
Metaspace used 3105K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 337K, capacity 388K, committed 512K, reserved 1048576K
结论:
逻辑上堆内存由:PSYoungGen+ ParOldGen+Metaspace
物理上堆内存由:PSYoungGen+ ParOldGen
因为从上面的数据可以看出:(1223168K+2796544K) / 1024 = 3925M
演示堆溢出异常java.lang.OutOfMemoryError: Java heap space
设置堆的内存大小为10M,用程序添加一个100M的数据到堆中
public class ClassLoaderDemo {
public static void main(String[] args) {
MyClass myClass = new MyClass();
System.out.println("maruis------>" + myClass.getClass().getClassLoader());
System.out.println("maruis------>" + myClass.getClass().getClassLoader().getParent());
System.out.println("maruis------>" + myClass.getClass().getClassLoader().getParent().getParent());
// m1();
long jvm_Xmx = Runtime.getRuntime().maxMemory();
long totleMemory = Runtime.getRuntime().totalMemory();
// 返回java虚拟机试图启用对的最大内存
System.out.println("maruis--JVM最大内存---->" + jvm_Xmx/1024/1024 +"M");
// 默认的堆内存的初始大小
System.out.println("maruis-- 默认的堆内存的初始大小---->" + totleMemory/1024/1024 +"M");
MyClass my = new MyClass();
}
private static void m1() {
m1();
}
}
class MyClass{
// 100M的数据
Byte[]aaa = new Byte[1*1024*1024*100];
}
GC回收参数的解读
[GC (Allocation Failure) [PSYoungGen: 2046K->504K(2560K)] 2046K->968K(9728K), 0.0018712 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1009K->488K(2560K)] 1473K->1016K(9728K), 0.0010144 secs] [Times: user=0.05 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 488K->504K(2560K)] 1016K->1040K(9728K), 0.0005699 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 536K->862K(7168K)] 1040K->862K(9728K), [Metaspace: 3292K->3292K(1056768K)], 0.0062774 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 862K->862K(9728K), 0.0005373 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 862K->834K(7168K)] 862K->834K(9728K), [Metaspace: 3292K->3292K(1056768K)], 0.0075727 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
[GC (Allocation Failure)发生在YoungGen区
为什么产生GC(YoungGen区),因为分配失败Allocation Failure
此时我们设置的最大堆内出和初始堆内存是10M,由于默认YoungGen占9728*1/3 = 3,242.6K 但是 由于有损耗,所以可定不到这个值 ,OldGen区占2/3 所以此时9728*2/3
[PSYoungGen: 2046K->504K(2560K)] 2046K->968K(9728K), 0.0018712 secs]
PSYoungGen: 2046K->504K(2560K)]:PSYoungGen 表示新生代(YoungGen区),2046KGC前占用,504KGC后占用(2560K)YoungGen区总空间。
2046K->968K(9728K),与上面意思类似,这些数据是整个堆的说明
0.0018712 secs 耗时
[Times: user=0.00 sys=0.00, real=0.00 secs]
耗时的细节:用户耗时,系统耗时,真实耗时
Full GC发生在 OldGen区
[Full GC (Allocation Failure) [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 536K->862K(7168K)] 1040K->862K(9728K), [Metaspace: 3292K->3292K(1056768K)], 0.0062774 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Full GC (Allocation Failure):gc类型
[PSYoungGen: 504K->0K(2560K)] YoungGen区的gc前后和占该去的总内存
1040K->862K(9728K) 堆的gc前后和占该去的总内存
[Metaspace: 3292K->3292K(1056768K)] 元空间的gc前后和占该去的总内存
0.0062774 secs gc耗时
[Times: user=0.00 sys=0.00, real=0.01 secs] gc耗时细节
五. GC的四大算法
1. 引用计算法
* 2. 复制算法
* 年轻代的垃圾回收算法使用的是复制算法,复制算法不会产生内存碎片
* 优点:没有碎片,缺点:占空间
* 3. 标记清除 针对的是 老年区
* 分两步,先标记出要回收的对象,然后统一回收这些对象。
* 优点:不需要额外空间,缺点:两次扫描(一次标记,一次清除),耗时严重,会产生内存碎片
* 4. 标记压缩(也叫标记整理)算法
* 分三步:标记,清楚,压缩(使得空间内存连续)
* 优点:没有碎片,缺点:耗时严重
* 哪一种算法最好?
* 答:不知道,分代收集算法,没有最好的算法,只有根据它的每一代,利用垃圾回收的特性利用最适合的算法。
* 新生代用复制算法,old区用标记清除,标记整理
* 内存效率:复制算法>标记清除算法>标记压缩算法
* 内存整齐度(碎片化):复制算法>标记压缩算法>标记清除算法
* 内存利用率:标记压缩算法>标记清除算法>复制算法
* 年轻代特点是区域相对老年代较小,对象存活率低。这种用复制算法回收,速度最快。
* 老年代的特点是区域较大,对象存活率高
* 这种情况就不太适合复制算法,一般使用标记清除和标记压缩算法的混合模式。
六. VMM java内存模型
七、最佳实战
7.1 jinfo查看当前运行程序的配置
## 第一步,通过jps -l 查找到java进程
jps -l
## 第二步:通过jinfo 获取jvm参数 22372 是进程编号 获取PrintGCDetails 是一个布尔型,+表示true,-表示false
jinfo -flag PrintGCDetails 22372
## 获取MetaspaceSize 参数,kv型,=
jinfo -flag MetaspaceSize 22372
## 获取所有jvm参数
jinfo -flags 22372
7.2 如何参数盘点家底
## 查看jvm的初始值
java -XX:+PrintFlagsInitial
## 查看对jvm修改更新的值, = 表示没改过; := 表示被修改过
java -XX:+PrintFlagsFinal -version
## 查看命令行参数 主要看-XX:+UseParallelGC 查看用的哪个垃圾回收器
java -XX:+PrintCommandLineFlags -version
7.3 常用的jvm基本配置参数有哪些?
- -Xms 初使内存默认物理内存的1/64
- -Xmx 最大内存默认物理内存的1/4
- -Xss 设置单个线程栈的大小,一般512K~1024K,等价于:-XX:ThreadStackSize
如果这个值是0表示是默认的出场设置,java在linux下默认出场时1024KB - -Xmn 新生代的大小,一般不用调
- -XX:MetaspaceSize 元空间的大小,元空间不在虚拟机中,而是用的物理内存。默认时21M
这个值可以稍微改大一点,以免频繁的fullgc,一般512m足以。 - -XX:+PrintGCDetails 打印出垃圾回收的细节情况,如果有gc过程回打印出来。
-XX:SurvivoRatio 默认8
,表示伊甸园区和幸存者的比例时8:1:1 如果改成4,那就是4:1:1 这个值一般不需要改。-XX:NewRatio 默认2
,设置新生代与老年代的比例,
表示新生代占1,老年代占2,新生代占整个堆的1/3。注意如果时4 表示新生代占1,老年代占4,新生代占整个堆的1/5。这个值一般不需要改- -XX:MaxTenuringThreshold 设置垃圾的最大年龄,默认进入养老代时15次gc,如果设置为0,就不经过幸存者区,而直接进入养老去
实例:
-Xms128m -Xmx4096m -Xss1024k -XX:MetaspaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC
7.4 强软弱虚4中引用
强引用不会被垃圾回收期回收。
软引用:内存够用不会受,不够用回收
弱引用:被回收
虚引用:被回收,先放在队列中
7.5 oom的认识
- java.lang.StackOverFlowError
jvm配置
-Xms10m -Xmx10m
实例代码
public class StackOverFlowErrorDemo {
public static void main(String[] args) {
stackoverflowerror();
}
private static void stackoverflowerror() {
stackoverflowerror();
}
}
结果:
- java.lang.OutOfMemoryError:Java heap space
jvm参数:
-Xms2m -Xmx2m -XX:+PrintGCDetails
实例代码:
public class JavaHeapSpaceErrorDemo {
public static void main(String[] args) {
byte[] aaa = new byte[5*1020*1024];
}
}
结果:
- java.lang.OutOfMemoryError:GC overhead limit exceeded
jvm参数:
-Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails
代码实例:
public class OverHeadlimitExceededDemo {
public static void main(String[] args) {
int i =0;
List<String> stringList = new ArrayList<>();
try {
while (true){
stringList.add(String.valueOf(++i).intern());
}
} catch (Exception e) {
System.out.println("★★★★★★---》"+i);
e.printStackTrace();
throw e;
}
}
}
结果:
-
【★】java.lang.OutOfMemoryError:Direct buffer memory
原因:Java8出现了NIO,缓存,通道,选择器。在 写NIO程序的时候,经常使用ByteBuffer来读或者写入数据,这是一种基于通道( Channel)与缓冲区( Buffer)的I/0方式.它可以使用 Native函数库直接分配堆外内存,然后通过一个存做在Java里面的 DirectByteBuffer对作为这块内存的引用进行操作。可以提高性能,因为避免了在Java堆和 Native堆中来回复制数据。
ByteBuffer. allocate(capability)第一种方式是分配java堆内存,属于GC管理范围,由于需要进行拷贝,所以比较慢。
ByteBuffer. allocteDirect (capability)第二种方式是分配操作系统的本地内存默认是物理内存的1/4
,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。但如果不断分配本地内存,堆内存很少使用,那么java虚拟机就不需要进行GC, DirectByteBuffer对象们就不会被回收,这个时候堆内存充足,但本地内存可能已经使用光了,在尝试分配本地内存就会出0ut0fMemory Error,那程序就直接奔溃了。
jvm参数:
-Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails
代码实例:
public class DirectBufferMemoryError {
public static void main(String[] args) {
// 默认是物理内存的1/4
System.out.println("maxDirectMemorym默认大小*************"+ VM.maxDirectMemory()/1024/1024+"MB");
try {
Thread.sleep(3000);
// 此处我们通过jvm分配的直接内存是5m 但是此处要使用6m的堆外内存是6m所以会抱错
ByteBuffer bb = ByteBuffer.allocateDirect(6*1024*1024);
} catch (Exception e) {
e.printStackTrace();
}
}
}
结果:
- 【★】java.lang.OutOfMemoryError:unable to create new native thread
原因:创建的线程数已经到了上限,linux系统默认允许的单个进程的最大线程数时1024个,其实到不了可能9百多就报错误了。
解决办法:
1.想办法降低线程的数量。2.如果再linux平台需要线程超过1024个,修改配置来扩大最大线程数
vim /etc/security/limits.d/20-nproc.conf
- java.lang.OutOfMemoryError:Metaspace
原因:元空间不足,
元空间存放:虚拟机加载的类信息,常量池,静态变量,即时编译后的代码
没有出现效果
public class MetaspaceError {
static class OOMTest {
}
static javassist.ClassPool cp = javassist.ClassPool.getDefault();
public static void main(String[] args) {
int i = 0;
try {
while (++i > 0) {
Class c = cp.makeClass("com.atguigu.deallock.MetaspaceError.OOMTest"+i).toClass();
}
} catch (Throwable e) {
System.out.println("★★★★★★---》" + i);
e.printStackTrace();
}
}
}
7.6 垃圾回收算法和垃圾回收器的关系,分别请你谈谈
引用计数器/复制/标记清除/标记整理 是四种算法,它是内存回收的方法论,垃圾回收器就是算法的具体落地实现。目前为止没有完美的垃圾收集器出现,更没有万能的垃圾收集器,只是针对具体应用找到最合适应用的垃圾收集器,进行分代收集。
4中主要的垃圾收集器:
Serial:串行回收
为单线程环境设置,并且只使用1个线程进行垃圾回收,会暂停所有的用户线程。不适合服务器环境。
Parallel: 并行回收
多个垃圾回收器并行工作,此时用户线程是暂停的,使用与科学计算/大数据处理首台处理等若交互场景。它的回收时间会比穿行要短。
CMS:并发标记清除
java8 以前基本都是上面的3钟方式。
用户线程和垃圾回收线程可以同时执行(不一定是并行,可能交替执行),不需要暂停用户线程,互联网公司多用它,适用于对相应时间有要求的场景。
G1:
将堆内存分割成不同的区域,然后并发的对其进行垃圾回收。
7.7 怎么查看服务器默认的垃圾回收器时哪个?
-XX:+UseParallelGC
java -XX:+PrintCommandLineFlags -version
7.8 ★7种经典的垃圾收集器
串行回收器:Serial、Serial old(在底层代码中已经被废弃)
并行回收器:ParNew、Parallel Scavenge、Parallel old
并发回收器:CMS、G1
参数说明
DefNew ----------------------》Default New Generation 新生代串行gc
Tenured ----------------------》Old 老年代并行gc
PaNew ----------------------》Parallel New Generation 新生代并行gc
PSYoungGen ----------------------》Paralel Scavenge 新生代和老年代都并行
ParOldGen ----------------------》Parallel Old Generation 老年代并行
适用范围
只需要掌握Server模式即可,Client模式基本不会用
操作系统:
32为Windows系统,不论硬件如何默认都是用Client的JVM模式
32为其他操作系统,2G内存同时又2个cpu以上用server模式,对于这个配置用client模式
64位操作系统只有server模式。
7种配置:
#使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。等价于新生代用Serial GC,且老年代用Serial old GC
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC
## XX: +UseParNewGC"手动指定使用.ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代Serial old GC。
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC -XX:ParallelGCThreads=8
## -XX:+UseParallelGC 自动与 -XX:+UseParallelOldGC 组合, 互相开启的。在Java8中,默认是此垃圾收集器。
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC
## XX:+UseConcMarkSweepGC 自动与 -XX:+UseParNewGC 组合
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC
## java8已经被废弃了用不了了,配置会出错。
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldGC
## -XX:+UseParallelGC 自动与 -XX:+UseParallelOldGC 组合, 互相开启的。
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC
7.8.1 Serial回收器:串行回收
#使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。等价于新生代用Serial GC,且老年代用Serial old GC
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC
7.8.2 ParNew回收器:并行回收
## XX: +UseParNewGC"手动指定使用.ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代Serial old GC。
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC -XX:ParallelGCThreads=8
7.8.3 ★Parallel回收器(java8默认的
)
## -XX:+UseParallelGC Parallel收集器和Parallel old收集器的组合,在server模式下的内存回收性能很不错。在Java8中,默认是此垃圾收集器。
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC
-XX:ParallelGcrhreads设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
在默认情况下,当CPU数量小于8个,ParallelGcThreads的值等于CPU数量。
当CPU数量大于8个,ParallelGCThreads的值等于3+[5*CPU Count]/8]
-XX:+UseParallelGC 手动指定年轻代使用Paralle1并行收集器执行内存回收任务。
-XX:+UseParalleloldcc 手动指定老年代都是使用并行回收收集器。
上面两个参数,默认开启一个,另一个也会被开启。(互相激活)
7.8.4 CMS(并发标记清除)回收器:低延迟
## XX:+UseConcMarkSweepGC 自动与 -XX:+UseParNewGC 组合
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC
CMS的垃圾 收集算法采用标记一清除算法,并且也会" stop一the一world
"
四个阶段:
- 初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“
stop-the-world”
机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GCRoots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。 - 并发标记(Concurrent-Mark)阶段:从Gc Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
- 重新标记(Remark)阶段:由于在并发标记阶段中,“
stop-the-world”
程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。 - 并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
打印结果:
-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:MaxNewSize=3497984 -XX:MaxTenuringThreshold=6 -XX:NewSize=3497984 -XX:OldPLABSize=16 -XX:OldSize=6987776 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC
★★★★★★--(-Xms)-》10158080Byte 9.6875MB
★★★★★★--(-Xmx)-》10158080Byte 9.6875MB
应用范围:
目前很大一部分的Java应用集中在互联网站或者B/s系统的服务端上,
这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
缺点:产生内存碎片
7.9 G1垃圾回收器
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC
G1垃圾收集器的重要参数:
- -XX:+UseG1GC:手动指定使用G1垃圾收集器执行内存回收任务
- -XX:G1HeapRegionSize设置每个Region的大小。值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。
- -XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms
- -XX:+ParallelGcThread 设置STW工作线程数的值。最多设置为8
- -XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGcThreads)的1/4左右。
- -XX:InitiatingHeapoccupancyPercent 设置触发并发Gc周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
G1垃圾收集器与CMS收集器比较的优点
1)空间整合
CMS:“标记-清除”算法、内存碎片、若干次Gc后进行一次碎片整理
G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
2)可预测的停顿时间模型(即:软实时soft real-time)
这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
相比于CMSGC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。
G1收集器的常见操作步骤
G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:
第一步:开启G1垃圾收集器
第二步:设置堆的最大内存
第三步:设置最大的停顿时间
G1中提供了三种垃圾回收模式:YoungGC、Mixed GC和Fu11GC,在不同的条件下被触发。
G1垃圾收集器的缺点
相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(overload)都要比CMS要高。
从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间。
G1收集器的适用场景
- 面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)
- 最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案;
- 如:在堆大小约6GB或更大时,可预测的暂停时间可以低于e.5秒;(G1通过每次只清理一部分而不是全部的Region的增量式清理来保证每次Gc停顿时间不会过长)。用来替换掉JDK1.5中的CMS收集器;
在下面的情况时,使用G1可能比CMS好: 超过5e%的Java堆被活动数据占用; - 对象分配频率或年代提升频率变化很大;
- GC停顿时间过长(长于e.5至1秒);
- HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。
G1回收的优化建议
-
年轻代大小
避免使用-Xmn或-XX:NewRatio等相关选项显式设置年轻代大小 固定年轻代的大小会覆盖
-
暂停时间目标暂停时间目标不要太过严苛
G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间 评估G1GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示你愿意承受更多的垃圾回收开销,而这些会直接影响到吞吐量。
7.10 如何启动jar包或war包时设置jvm参数
公式:java -server
7.10 如何选择垃圾回收器
不同厂商、不同版本的虚拟机实现差距比较大。HotSpot虚拟机在JDK7/8后所有收集器及组合如下图
怎么选择垃圾收集器?
优先调整堆的大小让JVM自适应完成。
如果内存小于100M,使用串行收集器
如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择
如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器
官方推荐G1,性能高。现在互联网的项目,基本都是使用G1。
最后需要明确一一个观点:没有最好的收集器,更没有万能的收集,调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器
7.11 日志分析
通过阅读Gc日志,我们可以了解Java虚拟机内存分配与回收策略。 内存分配与垃圾回收的参数列表
-Xloggc:…/logs/gc.1og日志文件的输出路径
-XX:+PrintGc输出GC日志。类似:-verbose:gc
-XX:+PrintGcDetails输出Gc的详细日志
-XX:+PrintGcTimestamps 输出Gc的时间戳(以基准时间的形式)
-XX:+PrintGCDatestamps 输出Gc的时间戳(以日期的形式,如2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC在进行Gc的前后打印出堆的信息
-Xloggc:…/logs/gc.1og日志文件的输出路径
补充说明
[GC"和"[FullGC"说明了这次垃圾收集的停顿类型,如果有"Full"则说明GC发生了"stop The World"
使用Seria1收集器在新生代的名字是Default New Generation,因此显示的是"[DefNew"
使用ParNew收集器在新生代的名字会变成"[ParNew",意思是"Parallel New Generation"
使用Parallel scavenge收集器在新生代的名字是”[PSYoungGen"
老年代的收集和新生代道理一样,名字也是收集器决定的
使用G1收集器的话,会显示为"garbage-first heap"
Allocation Failure表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了。
[PSYoungGen:5986K->696K(8704K)]5986K->704K(9216K)中括号内:GC回收前年轻代大小,回收后大小,(年轻代总大小)括号外:GC回收前年轻代和老年代大小,回收后大小,(年轻代和老年代总大小)
user代表用户态回收耗时,sys内核态回收耗时,rea实际耗时。由于多核的原因,时间总和可能会超过rea1时间
分析这些GC日志工具
常用的日志分析工具有:GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat等