简介
JVM是JavaVirtualMachine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java是一门抽象程度特别高的语言,提供了自动内存管理等一系列的特性。这些特性直接在操作系统上实现是不太可能的,所以就需要JVM进行一番转换。
从下图中可以看到,有了JVM这个抽象层之后,Java就可以实现跨平台了。JVM只需要保证能够正确执行.class文件,就可以运行在诸如Linux、Windows、MacOS等平台上了。
1.Java内存区域
Java内存区域由下图中组件组成。
1.1.程序计数器
程序计数器是一块较小的内存空间,它可以看作是当前线程执行的字节码的行号指示器。
在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等功能都需要依赖计数器来完成。
1.2.Java虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。Java虚拟机栈描述的是Java方法执行的线程模型: 每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表,操作数栈,动态连接,方法出口等信息,每一个方法被调用直至执行完毕的过程,就对应着一个虚拟机栈中从入栈到出栈的过程。
局部变量表存储了编译期可知的各种Java虚拟机基本数据类型(boolean等八大基础类型),对象引用(指向对象起始地址的引用指针)
1.3.本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别是本地方法栈是为虚拟机执行本地(Native)方法服务。
1.4.Java堆
Java堆是虚拟机管理内存中最大的一块,Java堆是被所以线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,像年轻代和老年代都是在堆中。
1.5.方法区
方法区与Java堆一样,是各个线程共享的内存区域,他用于存储被虚拟机加载的类型信息,常量,静态变量,即时编辑器编译后的代码缓存等数据,别名叫非堆(Non-Heap)。
- JDK1.8之前方法区的实现是永久代,使用的堆内内存。
- JDK1.8后方法区的实现是元空间,使用的堆外内存。
1.6.运行时常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池。
1.7.直接内存
直接内存并不是虚拟机运行时数据区的一部分,但是这部分内存页被频繁使用。
在JDK1.4中加入了了NIO类,引入了一种基础通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据(对应NIO的术语叫做零拷贝)。
2.HotSpot虚拟机
2.1.对象的创建
创建对象通常(复制,反序列化)仅仅是一个new关键字而已。
-
1.当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用 ,并检查这个符号引用代表的类是否已经加载,解析和初始化过。如果没有,那必须先执行相应的类加载过程。
-
2.在类加载检查通过后,接下来虚拟机将为新生对象在Java堆上分配一块内存。
-
3.new指定之后会执行类的构造函数方法,这样一个真正可用的对象才算完全被构造。
2.2.对象的内存布局
对象在堆内存的存储布局可分为对象头、实例数据和对齐填充。
-
对象头: 占 12B,包括对象标记和类型指针。
-
对象标记: 存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、偏向线程 ID 和时间戳,这部分占 8B,称为 Mark Word。Mark Word 被设计为动态数据结构,以便在极小的空间存储更多数据,根据对象状态复用存储空间。
-
类型指针: 即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例.
-
实例数据: 对象真正存储的有效信息,即本类对象的实例成员变量和所有可见的父类成员变量。存储顺序会受到虚拟机分配策略参数和字段在源码中定义顺序的影响。相同宽度的字段总是被分配到一起存放,在满足该前提条件的情况下父类中定义的变量会出现在子类之前。
-
对齐填充: 不是必然存在的,仅起占位符作用。虚拟机的自动内存管理系统要求任何对象的大小必须是 8B 的倍数,对象头已被设为 8B 的 1 或 2 倍,如果对象实例数据部分没有对齐,需要对齐填充补全。
2.3.对象的访问定位
Java程序会通过栈上的reference数据来操作堆上的具体对象。HostSpot使用的直接指针访问,reference存储的是对象地址。
2.4.OutOfMemoryError异常
除了程序计数器外,虚拟机内存的其他几个运行时区域都有OutOfMemoryError(OOM)异常的可能。
2.4.1.Java堆溢出
Java堆用于存储对象实体,只要不停创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,随着对象数量增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。
以下设置jvm参数堆最大内存为10M然后一直通过while死循环制造对象模拟异常。
-Xms10M -Xmx10M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\java\JavaProject\jvm_frist\src\test\jvm.dump
public class OOMTest {
public static void main(String[] args) {
List<Object> list=new ArrayList<>();
int i=0;
while (true){
list.add(new User(i++, UUID.randomUUID().toString()));
//new User(i--, UUID.randomUUID().toString());
}
}
}
class User{
private Integer id;
private String name;
public User(Integer id,String name){
this.id=id;
this.name=name;
}
}
运行结果: java.lang.OutOfMemoryError: Java heap space:
- 通过设置jvm参数XX:+HeapDumpOnOutOfMemoryError + XX:HeapDumpPath可以在程序出现OOM异常的时候保存程序堆快照文件。
- 可使用jdk自带的jvisualvm工具对堆快照文件进行分析。
确定是否为内存泄露,如果是通过工具查看查看泄露对象到GC Roots的引用链,找到泄露对象是通过怎样的引用路径,与哪些GC Roots相关联导致垃圾收集器无法回收。
2.4.2.虚拟机栈和本地方法描述
关于虚拟机栈和本地方法栈有两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
- 如果虚拟机栈内存运行动态扩展,当扩展栈容量无法申请到足够的内存将抛出OutOfMemoryError异常。
设置jvm参数栈内存为128k然后无限递归调用模拟异常。
-Xss128k
public class StackSOFTest {
private int stackLength=1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
StackSOFTest stackSOFTest=new StackSOFTest();
try {
stackSOFTest.stackLeak();
}catch (Throwable e){
System.out.println("stack Length:"+stackSOFTest.stackLength);
throw e;
}
}
}
运行结果如下
Exception in thread “main” java.lang.StackOverflowError
2.4.3.方法区和运行时常量池区溢出
设置jvm参数堆最大内存为6M
-Xmx6MB
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
Set<String> set=new HashSet<>();
short i=0;
while (true){
set.add(String.valueOf(i++).intern());
}
}
}
运行结果:java.lang.OutOfMemoryError: Java heap space
2.4.4.直接内存溢出
直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize来指定,如果不去指定,则默认与Java堆最大值一致。
设置jvm参数最大直接内存为6M
-Xmx20M -XX:MaxDirectMemorySize=10M
public class DirectMemoryOOMTest {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field field = Unsafe.class.getDeclaredFields()[0];
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}
运行结果如下:
Exception in thread “main” java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at test.DirectMemoryOOMTest.main(DirectMemoryOOMTest.java:19)
3.垃圾收集器与内存分配策略
垃圾回收器需要完成的三件事情:
3.1.判断对象是否死亡?
在堆里面存放着Java中几乎所有的对象实例,垃圾收集器在对堆进行回收前需要判断哪些对象存活和死亡(既不能被任何地方引用到)。
3.1.1.引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的Java虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,如下面代码所示:除了对象objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知 GC 回收器回收他们。
设置jvm参数打印gc日志
-XX:+PrintGCDetails
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
private byte[] bytes = new byte[2 * _1MB];
public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
//objA=null;
//objB=null;
System.gc();
}
}
3.1.2.可达性分析算法
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
GC Roots根节点:类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等等
3.2.引用类型
无论是通过引用计数器判断对象的引用数量还是通过可达性分析算法判断对象是否引用链可达,判断对象存活都和引用离不开关系。
-
强引用:如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。强引用的特性是只要有强引用存在,被引用的对象就不会被垃圾回收。
例如: Object obj = new Object(); 创建了一个Object对象,并将其赋值给obj,这个obj就是new Object()的强引用。
-
软引用:在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。
软引用是由java.lang.ref.SoftReference所提供的功能,意思是只有在内存不足的情况下,被引用的对象才会被回收。
-
弱引用:具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象。
弱引用是由java.lang.ref.WeekReference所提供的功能,不同的是weekReference引用的对象只要垃圾回收执行,就会被回收,而不管是否内存不足。
-
虚引用:顾名思义,就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。
虚引用是由java.lang.ref.PhantomReference所提供的关联功能。
3.3.finalize延迟回收
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
- 第一次标记并进行一次筛选:
筛选的条件是此对象是否有必要执行finalize()方法。
当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。
- 第二次标记:
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。
finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己----只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
/**
* 重写Object类的finalize方法 在该对象被gc回收之前会调用该方法
*/
@Override
protected void finalize() throws Exception{
System.out.println("关闭资源,user"+id+"即将被回收");
}
3.4.垃圾回收算法
3.4.1.标记清除算法
算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,效率也很高,但是会带来两个明显的问题:
- 效率问题
- 空间问题(标记清除后会产生大量不连续的碎片)
3.4.2.复制算法
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
3.4.3.标记整理算法
根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一段移动,然后直接清理掉端边界以外的内存。
3.4.4.分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
3.6.垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
虽然我们对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的HotSpot虚拟机就不会实现那么多不同的垃圾收集器了。
3.6.1.Serial收集器
垃圾回收执行流程如下:
Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
Serial作用于新生代,对应的老年代回收器为Serial Old,新生代采用复制算法,老年代采用标记-整理算法。
虚拟机的设计者们当然知道Stop The World带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
3.6.2.ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。
新生代采用复制算法,老年代采用标记整理算法。
它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
并行和并发概念补充:
- 并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。适合科学计算、后台处理等弱交互场景。
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个CPU上。适合Web应用。
3.6.3.Parallel Scavenge收集器
Parallel Scavenge 收集器类似于ParNew 收集器,是Server模式(内存大于2G,2个cpu)下的默认收集器。
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
Parallel Scavenge作用于新生代,对应的老年代回收器为Serial Old,
新生代采用复制算法,老年代采用标记-整理算法。
3.6.4.Serial Old收集器
Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。
3.6.5.Parallel Old收集器
Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。
3.6.6.CMS收集器
垃圾回收执行流程如下:
CMS在jdk5中诞生,在jdk14中,已经移除CMS垃圾回收器,使用G1全面替代CMS。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
-
初始标记:暂停所有的其他线程(STW),并记录下直接与root相连的对象,速度很快。
-
并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
-
重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
-
并发清除:开启用户线程,同时GC线程开始对未标记的区域做清扫。
从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
- 对CPU资源敏感(会和服务抢资源);
- 无法处理浮动垃圾(在java业务程序线程与垃圾收集线程并发执行过程中又产生的垃圾,这种浮动垃圾只能等到下一次gc再清理了);
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
CMS的相关jvm参数 |
描述 |
-XX:+UseConcMarkSweepGC |
使用cms回收器,启用后会自动将-XX: +UseParNewGc打开。即: ParNew (新生代) + CMS (老年代) |
-XX:ConcGCThreads |
并发的GC线程数(并非stop the world时间,而是和服务一起执行的线程数) |
-XX:+UseCMSCompactAtFullCollection |
FullGC之后做压缩(减少碎片) |
-XX:CMSFullGCsBeforeCompaction |
多少次FullGC之后压缩一次(因压缩非常的消耗时间,所以不能每次FullGC都做) |
-XX:CMSInitiatingOccupancyFraction |
触发FulGC条件(默认是92) |
-XX:+UseCMSInitiatingOccupancyOnly |
是否动态调节 |
-XX:+CMSScavengeBeforeRemark |
FullGC之前先做YGC(一般这个参数是打开的) |
-XX:+CMSClassUnloadingEnabled |
启用回收Perm区(jdk1.7及以前) |
3.6.7.G1收集器
G1收集器在jdk7 u9以上版本可以使用,在jdk9的时候成为默认的垃圾回收器。jdk8所以是需要设置-XX:+UseG1GC参数指定的回收器为G1。
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.
G1将Java堆划分为多个大小相等的独立区域(Region),虽保留新生代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。
分配大对象(直接进Humongous区,专门存放短期巨型对象,不用直接进老年代,避免Full GC的大量开销)不会因为无法找到连续空间而提前触发下一次GC。
G1具备以下特点:
-
并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
-
分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
-
空间整合:与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内完成垃圾收集。
G1收集器的运作大致分为以下几个步骤:
-
初始标记(initial mark,stop the world):在此阶段,G1 GC 对根进行标记。该阶段与常规的 (stop the world) 年轻代垃圾回收密切相关。
-
并发标记(Concurrent Marking):G1 GC 在整个堆中查找可访问的(存活的)对象。
-
最终标记(Remark,stop the world):该阶段是 stop the world 回收,帮助完成标记周期。
-
筛选回收(Cleanup,stop the world):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率。
G1的相关jvm参数 |
含义 |
-XX:+UseG1GC |
启动G1垃圾回收器 |
-XX:G1HeapRegionSize |
设置Region大小,并非最终值 |
-XX:MaxGCPauseMillis |
设置G1收集过程目标时间,默认值200ms,不是硬性条件 |
-XX:G1NewSizePercent |
新生代最小值,默认值5% |
-XX:G1MaxNewSizePercent |
新生代最大值,默认值60% |
-XX:ParallelGCThreads |
stop the world期间,并行GC线程数 |
-XX:ConcGCThreads |
设置并发标记阶段,并行执行的线程数 |
-XX:InitiatingHeapOccupancyPercent |
设置触发标记周期的 Java 堆占用率阈值。默认值是45%。 |
3.6.8.Shenandoah和ZGC
&emspJDK11以上新增了Shenandoah和ZGC更为先进的垃圾回收器,有需要的自行了解。
3.6.9.回收器使用场景
垃圾收集器 |
分类 |
作用位置 |
使用算法 |
特点 |
适用场景 |
Serial |
串行 |
新生代 |
复制算法 |
响应速度优先 |
适用于单CPU环境下的client模式 |
ParNew |
并行 |
新生代 |
复制算法 |
响应速度优先 |
多CPU环境Server模式下与CMS配合使用 |
Parallel Scavenge |
并行 |
新生代 |
复制算法 |
吞吐量优先 |
适用于后台运算而不需要太多交互的场景 |
Serial Old |
串行 |
老年代 |
标记-整理算法 |
响应速度优先 |
适用于单CPU环境下的client模式 |
Parallel Old |
并行 |
老年代 |
标记-整理算法 |
吞吐量优先 |
适用于后台运算而不需要太多交互的场景 |
CMS |
并发 |
老年代 |
标记-清除算法 |
响应速度优先 |
适用于互联网或B/S业务 |
G1 |
并发、并行 |
新生代、老年代 |
标记整理算法、复制算法 |
响应速度优先 |
面向服务端应用 |
GC发展阶段:Serial(串行) ->Parallel(并行)->CMS(并发)->G1 -> ZGC
3.6.10.指定垃圾回收器
通过 java -XX:+PrintCommandLineFlags可以查看jdk默认的垃圾回收器,重执行结果可以看出使用的参数是-XX:+UseParallelGC对应:新生代Parallel ,老年代Parallel old。
jvm参数 |
描述 |
-XX:+UseSerialGC |
新生代使用Serial GC回收器 ,老年代使用Serial Old GC回收器。 |
-XX:+UseParallelGC |
新生代使用Parallel Scavenge回收器 ,老年代使用Parallel old回收器。 |
-XX:+UseConcMarkSweepGC |
启用后会自动将-XX:+UseParNewGC打开。即新生代使用ParNew回收器 ,老年代CMS回收器。 |
-XX:+UseG1GC |
使用G1作为新生代和老年代回收器 |
3.7.内存分配和回收
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。我们来进行实际测试一下。
在测试之前我们先来看看 Minor Gc和Full GC 有什么不同呢?
新生代GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。
测试:
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。
为什么要这样呢?
为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别那些对象应放在新生代,那些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
4.类加载过程和机制
双亲委托模式优势:
- 避免类的重复加载,确保一个类的全局唯一性
Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种关系可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
- 保护程序安全,防止核心API被随意篡改
5.常用的jvm参数
参数 |
描述 |
-Xss |
设置栈大小,决定了函数调用的最大深度 |
-Xms |
设置堆的最小空间大小,生产环境一般都是和-Xmx参数设置的值一样,避免因内存不够而扩容的问题 |
-Xmx |
设置堆的最大空间大小 |
-Xmn |
设置新生代大小 |
-XX:PermSize/-XX:MaxPermSize |
设置永久代最小/最大空间大小, jdk8之前 |
-XX:MetaspaceSize/-XX:MaxMetaspaceSize |
设置元空间最小/最大空间大小,jdk8及以后 |
-XX:MaxDirectMemorySize |
最大直接内存(不和堆共享内存),如果不设置默认和-Xmx参数设置的值一致。 |
-XX:+OmitStackTraceInFastThrow |
(默认)当打印同样错误日志到一定次数就会被jvm默认优化掉不打印。 |
-XX:-OmitStackTraceInFastThrow |
省略异常栈信息从而快速抛出。 |
-XX:+HeapDumpOnOutOfMemoryError |
设置堆出现OOM异常的保存堆内存快照文件,需要和-XX:HeapDumpPath配和使用。 |
-XX:HeapDumpPath |
堆内存快照文件保存的路径 |
-XX:+PrintGCDetails / -XX:+PrintGCTimeStamps / -XX:+PrintGCDateStamps |
打印垃圾回收信息和时间戳 |
-Xloggc |
指定gc垃圾回收日志存储路径 |
由上参数可以看到java进程占用的最大内存主要由以下参数决定
- 最大堆内存:-Xmx
- 最大元空间内存:-XX:MaxMetaspaceSize
- 最大直接内存: -XX:MaxDirectMemorySize
- 栈大小: -Xss (太小可以忽略不记)
- native方法区: jvm运行环境依赖的c++库需要使用到一定的额外内存
以下是一个比较常用的jvm参数设置。
#日志文件的路径
BASE_DIR=/home/soft
#设置G1为垃圾回收器
JAVA_OPT='-XX:+UseG1GC'
#设置最小堆内存 最大堆内存 年轻代堆内存 初始化元空间内存
JAVA_OPT='${JAVA_OPT} -Xms2g -Xmx2g -Xmn1n -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=256m'
#设置内存溢出打印堆日志
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${BASE_DIR}/logs/heapdump.hprof"
#设置GC输出日志 ,测试环境调优可以打印,生产环境一般需要注释掉
JAVA_OPT="${JAVA_OPT} -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:${BASE_DIR}/logs/gc.log"
此文部分内容参考于周志明老师写的【深入理解Java虚拟机第3版】。