内容简介:垃圾:内存中已经不再被使用到的空间就是垃圾。可达性分析算法:基本思路是通过一系列名为 GC Roots 的对象作为起始点,从这些对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连,说明此对象不可用。GC Roots 是一组必须活跃的引用。
垃圾:内存中已经不再被使用到的空间就是垃圾。
可达性分析算法:基本思路是通过一系列名为 GC Roots 的对象作为起始点,从这些对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连,说明此对象不可用。
GC Roots 是一组必须活跃的引用。
可以作为 GC Roots 的对象的有:
- 虚拟机栈(栈帧中的局部变量区,也称局部变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 JNI ( Native 方法 )引用的对象。
JVM 参数
JVM参数类型
1. 标配参数:在 jdk 各个版本之间保持稳定
- java -version - java -help - java -showversion 复制代码
2. X参数
-Xint:解释执行 -Xcomp: 第一次使用就编译成本地代码 -Xmixed: 默认,先编译后执行 复制代码
3. XX参数
-
Boolean 类型
-
-XX:+/- 属性
- +表示开启,-表示关闭
- 示例 以是否打印GC收集细节为例:
-
首先使用
jps -l
查看当前运行进程 id -
然后使用
jinfo -flag PrintGCDetails 进程id
来查看 -
如果开启,结果应该是
-XX:+PrintGCDetails
,否则-XX:-PrintGCDetails
-
-
-
KV类型
-
-XX:key=value
- 以元空间大小 MetaspaceSize为例:同样使用上述的方法,使用
jinfo -flag MetaspaceSize 进程id
来查看。 -
-Xms
等价于-XX:InitialHeapSize
,-Xmx
等价于-XX:MaxHeapSize
-
如何查看JVM参数默认值
- 使用
java -XX:+PrintFlagsInital
查看jvm初始参数 - 使用
java -XX:+PrintFlagsFinal -version
查看修改更新的参数。参数中使用=
说明是未修改的参数,使用:=
说明是人为或jvm修改过的参数。 - 使用
java -XX:+PrintCommandLineFlags
JVM常用参数
JDK 1.8 之后永久代取消了,由元空间取代。新生代由 Eden + SurvivorFrom + SurvivorTo 组成,大小用 -Xmn
设置。JVM 堆由新生代和老年代共同组成,由 -Xms
和 -Xmx
设置大小。元空间不再属于堆的一部分。
元空间和永久代的区别在于:永久代使用 JVM 的堆内存,元空间不在虚拟机中而是使用 本机物理内存 。
默认情况下,元空间的大小仅受本地内存限制,类的元数据放入 native memory,字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由 MaxPermSize 控制,而由系统的实际可用空间来控制。
-
-Xms
-XX:InitialHeapSize
-
-Xmx
-XX:MaxHeapSize
-
-Xss
-XX:ThreadStackSize
-
-Xmn
- 设置新生代大小:默认是堆空间1/3
-
-XX:MetaspaceSize
- 元空间和永久代类似,都是对方法区的实现。元空间不在虚拟机中,而是使用本地内存。默认情况下,元空间的大小仅受本地内存限制。
- 但是这并不意味元空间不会有OOM,因为其默认内存大小是有限的
-
-XX:+PrintGCDetails
- 输出详细GC手机日志信息
[GC [PSYoungGen: 2048K -> 496K(2560K)] 2048K->777K(9728k)] 复制代码
[GC [PSYoungGen: 2048K 496K (2560K) 2048K 777K 9728K
[Full GC (System) [PSYoungGen:3408K->0K(296688k)] [PSOldGen:0K->3363K(682688K)]3408K->3363K(981376K)[Metaspace:10638K->10638K(131072K)]] 复制代码
可以看到 Full GC 中,分别有 新生代、老年代、堆总内存以及元空间各自的GC前、GC后以及总大小。
-
-XX:SurvivorRatio
- 设置新生代中 Eden 和 S0/S1 的比例
- 默认为8,表示三者比例为 8:1:1
-
-XX:NewRatio
- 设置新生代和老年代在堆中的占比
- 设置的值就是设置老年代比新生代的比值
-
-XX:MaxTenuringThreshold
- 设置垃圾最大年龄,默认是15
- java8 中能设置的阈值是 0 ~ 15
- 设置为较小值,适用于老年代比较多的应用。
- 设置为较大值,可以增加对象在新生代存活的时间,增加在新生代回收的概率。
四种引用
SoftReference, WeakReference 和 PhantomReference 三个类都继承自 Reference 类,而 Reference 类又是 Object 类的子类。
强引用
当内存不足,JVM 开始垃圾回收,但是强引用的对象,即使出现了 OOM 也不会对该对象进行回收。把一个对象赋给一个引用变量,这个引用变量就是一个强引用,只要还有强引用指向一个对象,该对象就处于可达状态,不会被 JVM 回收。
软引用
对于只有软引用的对象来说,系统充足时不会被回收,系统内存不足时会被回收。软引用通常用在 对内存敏感 的程序中,比如 高速缓存 就用到软引用。
适用场景( 缓存 ):假设有一个应用大量读取本地图片,每次读取都会IO影响性能,一次性全部加载到内存可能会内存溢出。
可以使用 HashMap 保存图片的路径和图片对象关联的软引用之间的映射关系,内存不足时, JVM 会自动回收这些缓存图片对象所占用的空间,有效避免了OOM的问题。
Map<String, SoftReference<Bitmap>> imageCache = new HashMap<>(); 复制代码
弱引用
只要GC一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
WeakHashMap
相比于 HashMap, WeakHashMap 中的元素,当 key 被回收后,Map 中相应的键值对将不再存在。
public class WeakHashMapDemo { public static void main(String[] args) { myHashMap(); System.out.println("======================"); myWeakHashMap(); } private static void myWeakHashMap() { WeakHashMap<Integer,String> map=new WeakHashMap<>(); Integer k=new Integer(1); String v="str"; map.put(k,v); System.out.println(map); k=null; System.out.println(map); System.gc(); System.out.println(map); } private static void myHashMap() { HashMap<Integer,String> map=new HashMap<>(); Integer k=new Integer(1); String v="str"; map.put(k,v); System.out.println(map); k=null; System.out.println(map); System.gc(); System.out.println(map); } } 复制代码
运行结果
{1=str} {1=str} {1=str} ====================== {1=str} {1=str} {} 复制代码
引用队列
public class ReferenceQueueDemo { public static void main(String[] args) throws InterruptedException { Object o1=new Object(); ReferenceQueue<Object> referenceQueue=new ReferenceQueue<>(); WeakReference<Object> weakReference=new WeakReference<>(o1,referenceQueue); System.out.println(o1); System.out.println(weakReference.get()); System.out.println(referenceQueue.poll()); System.out.println(); o1=null; System.gc(); Thread.sleep(1000); System.out.println(o1); System.out.println(weakReference.get()); System.out.println(referenceQueue.poll()); } } 复制代码
java.lang.Object@1b6d3586 java.lang.Object@1b6d3586 null null null java.lang.ref.WeakReference@4554617c 复制代码
弱引用、软引用、虚引用在 gc 之前,会被放到引用队列中。
虚引用
虚引用顾名思义就是形同虚设,它并不会决定对象的声明周期。如果一个对象仅仅持有虚引用,就和没有引用一样,随时可能被回收。虚引用不能单独使用,也不能通过它访问对象,必须和引用队列联合使用。
虚引用的主要作用是跟踪对象被垃圾回收的状态,仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。其意义在于说明一个对象已经进入 finalization 阶段,可以被回收,用来实现比 finalization 机制更加灵活的回收操作。
设置虚引用的唯一目的,就是在这个对象被回收的时候收到一个系统通知或者后序添加进一步的处理。
public class PhantomReferenceDemo { public static void main(String[] args) throws InterruptedException { Object o1=new Object(); ReferenceQueue<Object> referenceQueue=new ReferenceQueue<>(); PhantomReference<Object> phantomReference=new PhantomReference<>(o1,referenceQueue); System.out.println(o1); System.out.println(phantomReference.get()); System.out.println(referenceQueue.poll()); System.out.println(); o1=null; System.gc(); Thread.sleep(1000); System.out.println(o1); System.out.println(phantomReference.get()); System.out.println(referenceQueue.poll()); } } 复制代码
运行结果:
java.lang.Object@1b6d3586 null null null null java.lang.ref.PhantomReference@4554617c 复制代码
可以看到,虚引用的 get() 返回值永远是 null。
总结
在创建引用的时候可以指定关联的队列,当 GC 释放对象内存的时候,会将引用加入到引用队列。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要行动,相当于一种通知机制。通过这种方式,JVM允许我们在对象被销毁后,做一些我们想做的事情。
OOM
java.lang.StackOverflowError
当递归调用中,栈的调用深度过深,导致栈溢出错误。 继承关系如下:
Throwable -> Error -> VirtualMachineError -> StackoverflowError 复制代码
java.lang.OutOfMemoryError: Java heap space
当对象过多,或者存在大对象等情况下,导致堆内容超过堆的最大尺寸,导致堆溢出。
java.lang.OutOfMemoryError: GC overhead limit exceeded
GC回收时间过程会抛出此类异常。过长的定义是超过 98% 的时间用来做 GC 并且回收了不到 2% 的堆内存。连续多次 GC 都只回收了不到 2% 的极端情况下才会抛出。
假如不抛出 GC overhead limit 错误会发生的情况: GC 清理出的内存很快再次被填满,破事 GC 再次执行,形成恶性循环,CPU 使用率一直是 100%,但是 GC 却没有任何成果。
public class GCOverheadDemo { public static void main(String[] args) { int i = 0; List<String> list = new ArrayList<>(); while (true) { list.add(String.valueOf(++i).intern()); } } } 复制代码
java.lang.OutOfMemoryError: Direct buffer memory
写 NIO 程序经常使用 ByteBuffer 来读取或者写入数据。它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里边的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样在一些场景能显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
ByteBuffer.allocate(capability)
是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢。
ByteBuffer.allocateDirect(capability)
是分配 OS 本地内存,不属于 GC 管辖范围,由于不需要内存拷贝所以速度相对较快。
如果不断分配本地内存,堆内存很少使用,那么 JVM 就不需要执行 GC,DirectByteBuffer 对象就不会被回收,导致堆内存充足,但是本地内存已经被用光了。
java.lang.OutOfMemoryError: Unable to create new native thread
在高并发请求服务器时,会出现该异常。
导致的原因:
- 应用创建了太多的线程,一个应用进程创建多个线程,超过了系统承载的极限
- 服务器不允许应用程序创建这么多线程,linux 系统默认单个进程可以创建的线程数是 1024。
解决办法:
- 想办法降低创建线程的数量
- 确实需要创建很多线程的应用,可以修改 linux 服务器配置,扩大其限制
java.lang.OutOfMemoryError: metaspace
MetaSpace 是方法区在 HotSpot 中的实现,它并不在虚拟机内存中而是使用本地内存,也就是类的元数据被存储在 MetaSpace 的 native memory 中。
其中存放了:虚拟机加载的类的信息,常量池,静态变量和即时编译的代码。
垃圾回收器
垃圾回收器的四种思想
Serial
串行垃圾回收器,为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程,所以不适合服务器环境。
过程就是:程序运行->单GC线程->程序运行
Parallel
并行垃圾回收器,多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算、大数据处理等弱交互场景。
过程:程序运行->多GC线程并行运行->程序运行
CMS
并发垃圾回收器,用户线程和GC线程同时执行,不需要停顿用户线程,适用于对响应时间有要求的场景(强交互)。
过程:初始标记(暂停,单线程)->并发标记(GC线程和用户线程同时执行)->最终标记(暂停,多GC线程)->清除
G1
G1垃圾回收器将堆内存分割成不同的区域(Region)然后并发的对其进行垃圾回收。
查看默认的垃圾回收器
使用 java -XX:+PrintCommandLineFlags -version
查看默认的垃圾回收器。
在 java8 中,默认的是 -XX:+UseParallelGC
。
默认垃圾回收器有哪些
java 的 gc 类型有以下几种:
- UseSerialGC
- UseParallelGC
- UseConcMarkSweepGC
- UseParNewGC
- UseParallelOldGC
- UseG1GC
在 jvm 中的 7 种垃圾回收器中,Serial Old 已经被废弃,所以在 gc 的源代码中,有如上六种。
七种垃圾回收器
在新生代的有:
- Serial
- Parallel Scavenge
- ParNew 在老年代的有:
- Serial Old
- Parallel Compacting
- CMS 二者都可以使用的是 G1。
DefNew: Default New Generation Tenured: Old ParNew: Parallel New Generation PSYoungGen: Parallel Scavenge ParOldGen: Parallel Old Generation 复制代码
新生代串行收集器: Serial
一个单线程的收集器,在GC的时候,必须暂停其他所有的工作线程直到 GC 结束。
在单CPU环境下,没有线程交互的开销可以获得最高的GC效率,因此 Serial 垃圾收集器是 JVM 在 Client模式下默认的新生代垃圾收集器。
JVM参数: -XX:+UseSerialGC
开启后,新生代使用 Serial 老年代使用 Serial Old,新生代和老年代都使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法。
GC 日志中,新生代使用 DefNew
,老年代使用 Tenured
。
新生代并行收集器: ParNew
使用多线程进行垃圾回收,垃圾收集时,会 STW 暂停其他线程直到 GC 结束。
ParNew 收集器是 Serial 的并行多线程版本,常见的应用场景是配合老年代的 CMS 工作。是很多 jvm 运行在 Server 模式下的 新生代的默认垃圾收集器。
JVM 参数: -XX:+UseParNewGC
。启用后,新生代使用 ParNew ,老年代使用 Serial Old,新生代使用复制算法,老年代使用标记-整理算法。 要注意的是, ParNew + Serial Old 已经不再被推荐。
GC 日志中,新生代使用 ParNew
,老年代使用 Tenured
。
新生代并行收集器: Parallel Scavenge
Parallel Scavenge 收集器类似于 ParNew 也是一个新生代垃圾收集器,使用复制算法,是并行的多线程的垃圾收集器,是 吞吐量优先 的收集器。
它重点关注的是吞吐量(运行用户代码时间/(运行用户代码时间+垃圾收集时间))。高吞吐量意味着高效利用CPU的时间, 适用于在后台运算而不需要太多交互的任务 。
Parallel Scavenge 和 ParNew 收集器的一个重要区别是 自适应调节策略 。自适应调节策略是指虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量。
JVM 参数: -XX:+UseParallelGC
,与使用 -XX:+UseParallelOldGC
效果相同,这两个收集器默认搭配使用,所以会互相激活。开启后,新生代使用复制算法,老年代使用标记-整理算法。
启用该收集器后,GC 日志中,新生代输出 PSYoungGen
,老年代输出 ParOldGen
。
在 Java 8 中,该收集器为默认的收集器。
老年代并行收集器 : Parallel Old
Parallel Old 收集器是 Parallel Scavenge 的老年代版本,使用多线程的标记-整理算法,在 JDK 1.6开始提供。
在 JDK 1.6 之前,新生代使用 Parallel Scavenge 只能搭配老年代的 Serial Old,只能保证新生代的吞吐量优先。
在 JDK 1.8 及以后,默认搭配为 Parallel Scavenge + Parallel Old ,保证了整体的吞吐量优先。
JVM 参数: -XX:+UseParallelOldGC
,开始后,新生代使用 Parallel Scavenge,老年代使用 Parallel Old。
启用该收集器后,GC 日志中,新生代输出 PSYoungGen
,老年代输出 ParOldGen
。
老年代并发标记清除收集器:CMS
CMS(Councurrent Mark Sweep)是一种以获取 最短回收停顿时间 为目标的收集器,适合应用在互联网或B/S系统的服务器上,这类应用中是服务器的相应速度,希望系统停顿时间最短。CMS 适合堆内存大、CPU 核数多的服务器端应用。
JVM 参数: -XX:+UseConcMarkSweepGC
,开启该参数后会自动开启 -XX:+UseParNewGC
。使用 ParNew (新生代) + CMS + Serail Old 的收集器组合,其中 Serial Old 作为 CMS 出错的后备收集器。
- 初始标记:标记 GC Roots 直接关联的对象,速度很快,需要 STW。
- 并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程,标记全部对象。
- 重新标记:修正在并发标记期间,因为用户程序继续运行导致标记产生变动的对象的记录,需要STW。
- 并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。
由于耗时最长的并发标记和并发清除过程中,GC 线程和用户一起并发工作,所以总体上看 CMS 的 GC 和用户线程一起并发执行。
优点:并发收集、停顿低。
缺点:
- 并发执行,对CPU资源压力大: 并发执行, CMS 在收集时会增加对堆内存的占用。因此 CMS 必须要在老年代堆内存用尽之前完成垃圾回收,否则 CMS 回收失败时,将触发担保机制, Serial Old 会以 STW 的方式进行 GC ,造成较大停顿时间
- 使用标记清除算法导致大量碎片
老年代串行收集器:Serial Old
Serial Old 是 Serial 收集器的老年代版本,是个单线程收集器,使用标记-整理算法,是 Client 默认的老年代收集器。
在 Server 模式下:
- 在 JDK 1.5 之前与新生代 Parallel Scavenge 搭配。
- 作为老年代使用 CMS 收集器的后备垃圾收集方案。
如何选择垃圾收集器
- 单CPU或小内存、单机程序:
-XX:+UseSerailGC
- 多CPU,需要最大吞吐量,如后台计算型应用:
-XX:+UseParallelGC
或-XX:+UseParallelOldGC
- 多CPU,追求低停顿时间,需要快速响应的应用:
-XX:+UseConcMarkSweepGC
参数 | 新生代垃圾收集器 | 新生代算法 | 老年代垃圾收集器 | 老年代算法 |
---|---|---|---|---|
-XX:+UseSerialGC |
SerialGC | 复制 | SerailOldGC | 标记整理 |
-XX:+UseParNewGC |
ParNew | 复制 | SerailOldGC | 标记整理 |
-XX:+UseParallelGC / -XX:+UseParallelOldGC |
Parallel Scavenge | 复制 | Parallel Old | 标记整理 |
-XX:+UseConcMarkSweepGC |
ParNew | 复制 | CMS+Serial Old | 标记清除 |
G1 收集器
garbage-first heap+metaspace
G1以前收集器的特点
- 年轻代和老年代是 各自独立且连续 的内存块
- 年轻代使用 eden + s0 + s1 的复制算法
- 老年代收集必须扫描整个老年代区域
- 都以尽可能少而快速执行 GC 为设计原则
G1 介绍
G1 是一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。并且具有如下特性:
- 像 CMS 收集器一样,能与应用程序线程并发执行
- 整理空闲空间更快
- 需要更多的时间预测 GC 停顿时间
- 不希望牺牲大量的吞吐性能
- 不需要更大的 Java Heap
G1 的设计目标是取代 CMS 收集器,同 CMS 相比:
- G1 有整理内存过程,不会产生很多内存碎片
- G1 的 STW 更可控,在停顿时间上添加了预测机制,用户可以指定期望停顿时间
G1 的主要改变是 Eden, Survivor 和 Tenured 等内存区域不再是连续的,而是变成了一个个大小一样的 region。
G1 的特点
- G1 能充分利用多 CPU 的硬件优势,尽量缩短 STW
- G1 整体采用标记整理算法,局部通过复制算法,不会产生内存碎片
- G1 把内存划分为多个独立的 Region
- G1 逻辑上保留了新生代和老年代,但是不再是物理隔离的,而是一部分 Region 的集合(并且不要求 Region 是连续的),会采用不同的 GC 方式处理不同的区域。
- G1 只有逻辑上的分代概念,每个分区都可能随着 G1 的运行在不同代之间切换。
Region
将堆划分成了 Region,避免了全内存区域的 GC 操作。G1 并不要求对象的存储是物理上连续的,只需要逻辑上连续即可,每个分区可以按需在新生代和老年代之间切换。
每个 Region 大小范围在 1MB-32MB,最多可以设置 2048 个区域(默认也是2048),因此能支持的最大内存为 64 GB。
G1 仍然属于分代收集器。这些 Region 的一部分包含新生代,新生代的垃圾(Eden)收集采用 STW 的方式,将存活对象拷贝到老年代或者 Survivor 空间。Region 的另一部分属于老年代, G1 通过将对象从一个区域复制到另外一个区域,完成清理工作,因此不会有 CMS 内存碎片问题的存在了。
在 G1 中,还有一种特殊的区域,称为 Humongous 区域,如果一个对象占用的空间超过了 Region 的 50% 以上,就被认为是巨型对象,会直接分配在老年代。G1 划分了一个 Humongous 区,用来存放巨型对象。如果一个 Humongous Region 装不下一个巨型对象,G1会寻找连续的 Humongous Region 来存储,为了能找到连续的 H 区,有时候不得不启动 Full GC。
回收步骤
Young GC: 针对 Eden 区进行收集, Eden 区耗尽后被处罚,主要是 小区域收集 + 形成连续的内存块 ,避免内存碎片。
- Eden 区的数据移动到 Survivor 区,如果 Survivor 区空间不够,晋升到 Old 区
- Survivor 区数据移动到新的 Survivor 区,部分晋升到 Old 区
- GC 结束,应用程序继续执行
步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收:根据时间来进行价值最大化的回收
微服务生产部署和调参优化
生产环境服务器变慢
- 查看整机情况
top uptime
- 查看 CPU 情况
- 使用
vmstat -n 2 3
代表每2秒采样一次,一共采样3次 - procs 中
- r代表运行和等待 CPU 时间片的进程数,整个系统的运行队列不应超过总核数的2倍,否则代表系统压力过大
- b代表等待资源如磁盘 I/O,网络 I/O 的进程数
- cpu 中
- us 代表用户进程消耗 CPU 时间百分比,如果长期大于 50% 要优化程序
- sy 代表内核进程消耗 CPU 时间百分比
- us + sy 参考值为 80% ,如果和大于 80% ,可能存在 CPU 不足
- 使用
mpstat -P ALL 2
查看所有 CPU 核信息 - 使用
pidstat -u 1 -p pid
查看每个进程使用 CPU 的用量分解信息
- 使用
- 查看内存信息
free -m pidstat -p pid -r 采样间隔秒数
- 查看硬盘信息
- 使用
df -h
- 使用
- 查看磁盘IO
- 使用
iostat -xdk 2 3
- rkB/s 每秒读取数据量
- wkB/s 每秒写入数据量
- svctm I/O请求的平均服务时间,单位为ms
- util 一秒有百分之多少的时间用于 I/O 操作
- 使用
pidstat -d 采样间隔秒 -p pid
- 使用
- 查看网络IO
- 使用
ifstat 采样间隔秒
- 使用
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Python面试经验总结,面试一时爽,一直面试一直爽!
- 算法面试:数组编码面试问题
- 【面试虐菜】—— JAVA面试题(1)
- 如何面试-作为面试官得到的经验
- PHP面试之网络协议面试题
- 如何克服面试紧张心理 ?(面试答题篇Ⅲ)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。