Jvm概述和参数配置
浅谈 CPU 和线程
线程与进程
操作系统:实现管理进程、存储、设备、文件、交互等功能;说白了就是使硬件协调工作并能输入、处理、输出。
进程: 进程是操作系统为应用程序分配的资源;进程之间的资源相互独立;一个应用程序至少有一个进程;可理解为程序要跑起来所需要的内存空间;程序运行中处理某些指令会影响到占用空间,所以进程大小会变。
线程:每个进程至少有一个线程,同一进程中各线程资源共享;一个线程可能包含多条指令,CPU 执行的一段指令就是线程,CPU 根本不理解自己执行的指令属于哪个线程,CPU 也不需要理解这些,CPU 需要做的事情就是根据 PC 寄存器中的地址从内存中取出后执行,其它没了。Java 虚拟机会为每一个 Java 线程创建 PC 寄存器,在任意时刻,一个 Java 线程总是在执行一个方法。
线程数和 CPU 核心数
前面说到线程就相当于一段指令,单个核心上可以跑任意多个线程,只要内存够就行;线程数和 CPU 核心数没有直接关联。
BIO、NIO、AIO
BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
NIO: 同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理;Java 的 NIO 库允许 Java 程序使用直接内存(Java 堆外,直接向系统申请的内存)。
AIO: 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用。
Java 虚拟机的基本结构
虚拟机发展史
- 1996 年 1 月,JDK1.0 发布,使用一款叫做 Classic 的虚拟机解释执行 Java 字节码,此虚拟机边解释边运行代码,并非像现在先编译、后执行,速度上肯定快不起来,只能说够用。
- 1997 年 2 月,JDK1.1 发布,同年 Sun 公司通过收购途径获得了 Hotspot 虚拟机。
- 1998 年,JDK1.2 发布,Classic 依然作为默认的 Java 虚拟机。
- 2000 年,JDK1.3 发布,Hotspot 虚拟机成为默认的 Java 虚拟机,直到现在,依然是我们最常用的虚拟机。
Java 虚拟机架构
Java 虚拟机是一台执行 Java 字节码的虚拟计算机,它拥有独立运行的机制,其运行的 Java 字节码可以由 Java、Groovy、Scala 等语言编译而成。
- 基本结构

- 结构说明

总结
数据结构中的栈是一块私有的内存空间,每一次方法调用相当于开辟了一块新的栈空间,每一次方法调用结束都有相应的栈被弹出;Java 栈和栈有着相似的含义,只是 Java 栈中主要内容是栈帧,而每个栈帧对应着一个函数。
常用虚拟机参数
堆的参数配置
几乎所有对象都存放在堆中,并且 Java 堆通过垃圾回收机制可完全自动化管理,不需要显式释放对象。
根据垃圾回收机制的不同,Java 堆可能拥有不同的结构。最为常见的是将整个 Java 堆分为新生代和老年代。
最大堆和初始堆
Java 虚拟机会尽量使堆空间趋近于 Xms(初始堆空间),堆空间不足时会自动扩容,但最大容量不会超过 Xmx(最大对空间);实际工作中可以将 Xms 和 Xmx 的值设为相等,可以减少程序运行时进行的垃圾回收次数,从而提高程序的性能。
Java 堆中存放着对象实例,垃圾对象会被垃圾回收机制自动处理;堆空间不足对应的异常为:java.lang.OutOfMemoryError: Java heap space!
程序中可通过 Runtime.getRuntime.maxMemory()等方法获取堆信息。
通常建议-Xmx 不要超过物理内存的 80%。
-Xms1024m -Xmx1024m新生代、老年代配置
- -Xmn 用于配置新生代的大小;设置新生代大小会影响到老年代的大小,这个参数对性能和 GC 有很大影响,一般设置为整个堆空间的 1/3 到 1/4;
-Xmn340m- -XX:SurvivorRatio=Eden/Survivor 用于设置新生代中 Eden 区和 from/to 区(统称为 Survivor 区)的比例(from 和 to 区大小相等、可互换角色);
-XX:SurvivorRatio=2- -XX:NewRatio=老年代/新生代 用于设置老年代和新生代的比例;
## 除了-Xmn 指定新生区绝对大小,另一种指定大小的方式 -XX:NewRatio=2几个重要的堆分配参数含义

堆溢出处理
-XX:HeapDumpOnOutOfMemoryError 开启在内存溢出时导出整个堆信息【导出文件后缀为.dump,可用 MAT 工具打开查看】;
-XX:HeapDumpPath=保存路径/xxx.dump 指定堆信息导出路径,和导出堆信息配合使用;
非堆内存的参数配置
方法区配置
方法区和堆一样,所有线程共享;方法区用于保存类信息,如字段、方法、常量池等。
-XX:PermSize 与 -XX:MaxPermSize【JDK1.7 及以前】,永久区/永久代是方法区的主要实现,默认最大为 64M;
若程序包含大量的类或使用代理生成了大量的类导致方法区溢出,虚拟机抛 java.lang.OutOfMemoryError:PermGen space。
-XX:MaxMetaspaceSize 【JDK1.8+】,若不指定,会无上限耗用系统内存;
JDK1.8 及以上版本弃用了永久区,取而代之的是元数据区;若元数据区发生溢出,虚拟机会抛出 java.lang.OutOfMemoryError: Mataspace。
栈配置
-Xss 栈空间较小,栈分配速度快,出栈不会触发 GC;虚拟机可能将局部对象实例存入栈中,以提高效率;
栈空间直接决定了函数调用的最大深度;
每次函数调用都会生成对应的栈帧,从而占用一定的栈空间,当入栈的空间大于最大栈空间,就会抛出 java.lang.StackOverflowError 栈溢出错误;例如递归调用方法可能导致栈溢出。
直接内存配置
-XX:MaxDirectMemorySize 直接内存默认值为最大堆空间 Xmx 的值;直接内存适合申请内存空间次数少,读写频繁的场景,如频繁的 IO 操作、网络连接等。直接内存不一定能触发 GC(除非达到了 MaxDirectMemorySize 的设置值),分配不合理会使系统内存耗尽,导致内存溢出异常
Client 和 Server
Java 虚拟机支持 Client 和 Server 两种运行模式。使用参数 -client 和 -server 参数配置。使用 -version 可查看当前模式。两者相比,Client 模式启动较快,适用于开发机;Server 模式会收集更多的系统性能信息,使用复杂的优化算法优化程序,启动速度比较慢,但执行速度远远快于 Client 模式,对于长期运行的程序而言,Server 模式是不错的选择
其他参数配置
-XX:+PrintGC 【JDK1.8 及以前】和 -Xlog:gc 【JDK1.9+】
查看每次 GC 运行前后堆的大小变化;只有发生了 GC 才会打印;包含 GC 前堆空间、GC 后堆空间、可用堆空间、GC 耗时相关数据。
-XX:+PrintGCDetail 【JDK1.8 及以前】和 -Xlog:gc* 【JDK1.9+】
查看更加详细的 GC 处理情况。
-XX:+PrintClassHistogram
加上此启动参数,程序启动后,在 Java 的控制台中按下 Ctrl+Break 组合键,就会显示部分当前系统中占用空间最大的对象类名、以及它的实例数量和所占空间大小。
-XX:+PrintVMOptions
在程序运行时打印当前系统的实际运行显式参数;显式传递给虚拟机的参数是人为设置的,虚拟机运行自行设置的参数为隐式参数。
-XX:+PrintCommandLineFlags
在程序运行时打印传递给虚拟机的显示和隐式参数。
-XX:+PrintFlagsFinal
显示所有 Java 虚拟机参数及其当前取值;由于虚拟机参数众多,这个参数开启后,可能会产生 500 多行输出,有兴趣可以玩玩儿。
垃圾回收算法
引用计数法
引用计数是最古老最经典的一种垃圾收算法;对于一个对象 A,只要有任何对象引用了 A,则 A 的引用计数器(整型)就加一;当引用失效就减一;计数器为 0 时,对象被清除;
缺点:无法解决循环引用(如 A 引用 B,B 引用 A),计数器会消耗性能;Java 的垃圾收集器没有使用这种算法
标记清除法
分为两个阶段,标记阶段:可理解为标记有引用关系的对象(不包含循环引用);清除阶段:清除所有未标记的对象;
缺点:未标记对象可能地址不连续,会影响效率
复制算法
将内存分为两块,每次只使用其中一块,在进行垃圾回收时,将正在使用的内存中存活对象复制到未使用的内存块中;
优点:若垃圾对象多,则相应需要复制的存活对象就少,复制算法会很高效;这种情况在新生代经常发生;
缺点:要将系统内存折半;
Java 的新生代串行垃圾回收器中,使用了复制算法的思想;其中 from 和 to 区,他们地位、大小相等,用于存放未被回收的对象(幸存空间),因此他们也叫做 survivor 区;
间述:在进行垃圾回收时,eden 区的存活对象会被复制到 from 区(假设 from 区未使用),to 区(假设使用中)的年轻对象也会被复制到 from 区(大对象或老年对象会直接进入老年代,若 from 区满了,也直接进入老年代),此时 eden 区和 to 区的剩余对象就被当做垃圾对象进行回收;
标记压缩法
复制算法适用于新生代,但是在老年代,大部分对象都是存活对象,用复制算法显然复制成本会很高;标记压缩就是适用于老年代的一种算法,和标记清除法一样,只是多了一个压缩再清理的过程,它将所有存活对象压缩到内存一端,然后清理边界外所有内存,解决了标记清除法清理后,地址不连续的问题
分代算法
分代更像一种思想,在前面的算法中,没有一种完美的算法能适用于各种环境,分代算法将内存区间根据对象的特点分成几块,每块根据其特点使用不同的算法;
分区算法
将整个堆空间分成连续的不同小区间,每个小区间独立使用,独立回收,有点类似于并行的意思,分工处理以减少 GC 产生的停顿时间
对象的引用方式
Java 中提供了 4 个级别的引用:强引用、软引用、弱引用、虚引用;强引用相当于一般的对象赋值,其他三个引用可手动实现,对应类型的类可在 java.lang.ref 包中找到;这 4 个引用类型强度逐级减弱
强引用
程序中的一般引用类型;强引用可以直接访问目标对象、在任何时候都不会被系统回收、可能导致内存泄漏;
// 局部变量a被分配在栈上,指向A实例所在的堆空间
// 通过变量a可直接操作该实例,a就是A实例的强引用
A a = new A();
// 此时,A实例有两个引用( a 和 b ),他们都是强引用
A b = a;软引用
如果一个对象只持有软引用,当堆空间不足时会被回收;
使用参数-Xmx10m 运行下面代码:
A a = new A();
SoftReference<A> softRef = new SoftReference<>(a);
a = null;
System.gc(); // 此时堆空间充裕,弱引用不会被清理
System.out.println(softRef.get() == null); // softRef.get()可拿到对象A的实例;打印结果为false
// 模拟堆空间不足
byte[] b = new byte[1024 * 925 * 7]
System.gc(); // 此时堆空间不足,弱引用被清理
System.out.println(softRef.get() == null); // 打印结果为true弱引用
发现即回收;不管堆空间怎样,只要被 GC 发现就清理;若没有及时发现,它可能存在较长时间;
示例代码:
A a = new A();
WeakReference<A> weakRef = new WeakReference<>(a);
a = null;
System.out.println(weakRef.get() == null); // 打印结果为false
System.gc();
System.out.println(weakRef.get() == null); // 打印结果为true小结:软引用、弱引用都非常适合保存那些可有可无的缓存数据,可提高效率;
虚引用
一个持有虚引用的对象,和没有几乎是一样的,它的作用在于跟踪垃圾回收过程;
垃圾收集器和内存分配
HotSpot 虚拟机实现了很多不同的收集器,因为直到现在为止还没有万能的收集器,根据场景应用最合适的收集器才是最好的选择。
垃圾回收器的种类
串行回收器 Serial GC
最古老的一种回收器,仅仅使用单线程进行垃圾回收,且回收时线程阻塞;优点:久经考验,极为高效
- 新生代串行回收器 使用复制算法实现
- 老年代串行回收器 使用标记压缩法实现
# 新生代、老年代都使用串行回收器 -XX:+UseSerialGC新生代并行回收器 Parallel GC
在串行回收器基础上改进,以并行方式减少垃圾回收时间,使用复制算法实现。
Parallel 收集器更关注吞吐量(CPU 运行用户代码时间与 CPU 总消耗时比值),如虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%。
# 新生代使用 ParallelGC 回收器,老年代使用串行回收器 -XX:UseParallelGC以下参数只适用于 ParallelGC 回收器,可手动设置垃圾停顿时间和垃圾耗时比例;
- 垃圾停顿时间 -XX:MaxGCPauseMillis 其值是一个大于 0 的整数;值越小停损时间虽然小了,但是会导致回收变得更加频繁
- 垃圾耗时比例 -XX:GCTimeRatio 其值是一个 0 到 100 的整数;若值为 19(默认),耗时占用比例为 1/(1+19) = 5%,即一分钟内有 3 秒将花费在垃圾回收上面
- 自适应 GC 调节 -XX:+UseAdaptivesSizePolicy 手动设置比较困难的场景下,此参数可自动根据系统配置设置新生代大小、Eden 和 survivor 区的比例等等
新生代 ParNew 回收器(jdk9+已弃用,建议使用默认的 G1 回收器)
ParNew 回收器工作在新生代,相当于是 Serial 收集器的多线程版本,使用的是复制算法实现
ParNew 和 Parallel 的区别是 ParNew 能与老年代收集器 CMS 配合工作。
# 新生代使用 ParNew 回收器,老年代使用 CMS 回收器 # jdk9+已弃用CMS 回收器,建议使用默认的 G1 回收器 -XX:UseConcMarkSweepGC # 新生代使用 ParNew 回收器,老年代使用串行 Serial 回收器 # 此参数jdk9+已弃用,建议使用默认的 G1 回收器 -XX:UseParNewGC老年代 ParallelOldGC 回收器
和 ParallelGC 回收器类似,也是多线程并发回收器并关注吞吐量;但它应用于老年代;使用标记压缩法实现;
# 新生代使用 Parallel GC,老年代使用 ParallelOld GC -XX:UseParallelOldGCCMS 回收器(jdk9+已弃用)
CMS 是 concurrent mark sweep 的缩写,意为并发标记清除;这款收集器是 HotSpot 虚拟机中第一款真正意义上的并发(Concurrent)收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
CMS 收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间。
# 新生代使用 ParNew 回收器,老年代使用 CMS 回收器 # jdk9+已弃用CMS 回收器,建议使用默认的 G1 回收器 -XX:UseConcMarkSweepGC因为 CMS 基于标记清除算法实现,这意味着收集结束时会有大量空间碎片产生,会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配比较大的对象,导致提前触发 Full GC(针对整个堆内存的回收算法,速度比较慢,尽量减少 Full GC 的次数,以保证系统的性能)。
G1 回收器
G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一,JDK9 之后的默认回收器;使用了分区算法,将堆分为一个个区域,每次回收只回收其中几个;
回收器相关的其他参数
# 设置垃圾回收器线程数量,最好和 CPU 数量一致
-XX:ParallelGCThreads=16内存分配
垃圾收集器用于回收分配给对象的内存,对于给对象分配内存,往大方向讲,就是在堆上分配。对象主要分配在新生代的 Eden 区上,当 Eden 区没有足够空间进行分配时,虚拟机将发起一次新生代 GC(Minor GC,发生在新生代的垃圾收集动作)。
大对象直接进入老年代、长期存活的对象将进入老年代;
Eden 区对象经过一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,对象在 Survivor 区中每“熬过”一次 Minor GC,年龄(相当于计数器)就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就将会被晋升到老年代;
对象年龄不一定达到阈值才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代;
# 若对象大小大于1M (1024*1024),直接进入老年代
-XX:PretenureSizeThreshold=1048576
# 设置对象晋升老年代年龄阈值(默认为15)
-XX:MaxTenuringThreshold=10
