一、JVM垃圾回收的时候如何确定垃圾?是否知道什么是GC Roots?
1、JVM基础介绍
2、JVMGC串讲
(1)JVM内存结构
(2)JVM体系概述
3、常见的垃圾回收算法
(1)引用计数
(2)复制算法
Java堆从GC的角度还可以细分为: 新生代(Eden 区、From Survivor 区和To Survivor 区)和老年代。
MinorGC的过程(复制->清空->互换):
- Eden、SurvivorFrom复制到SurvivorTo,年龄+1。首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1。
- 清空eden-SurvivorErom。然后,清空Eden和Survivor From中的对象,也即复制之后有交换,谁空谁是To。
- Survivor To和 Survivor From互换。最后,Survivor To和Survivor From互换,原SurvivorTo成为下一次GC时的Survivor From区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
(3)标记清除
算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象。(4)标记压缩/标记整理
4、GCRoots介绍
(1)什么是垃圾?
简单的说就是内存中已经不再被使用到的空间就是垃圾。(2)垃圾回收(判断一个对象是否回收)
- 引用计数法
- 枚举根节点做可达性分析(根搜索路径)
引用计数法(目前无人用,解决不了循环引用的问题)
- Java中,引用和对象是有关联的,如果要操作对象则必须用引用进行。
- 因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,给对象中添加一个引用计数器。
- 每当有一个地方引用它,计数器值+1;
- 每当有一个引用失效时,计数器值-1;
- 任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象。
枚举根节点做可达性分析(根搜索路径)
- 为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。
- 所谓”GC Roots”或者说tracing GC 的”根集合”就是一组必须活跃的引用。
- 基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连,则说明此对象不可用,也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活;没有被遍历到的就自然被判定为死亡。
(3)Java中可以作为GC Roots的对象
- 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
- 方法区中的类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(Native方法)引用的对象。
二、JVM调优和参数配置、查看默认值
1、JVM的参数类型
- **数
- -version java -version
- -help
- **(了解)
- -Xint:解释执行
- -Xcomp:第一次使用就编译成本地代码
- -Xmixed:混合模式
- **参数
- 布尔类型
- 公式:-XX:+ 或者 - 某个属性值(+表示开启,- 表示关闭)
- jps -l 查看一个正在运行中的java程序,得到java程序号
- jinfo -flag PrintGCDetails (java程序号) 查看它的某个jvm参数(如PrintGCDetails)是否开启。
- jinfo -flags (java程序号)查看它的所有jvm参数
- 例子
- 是否打印GC收集细节
- -XX:-PrintGCDetails
- -XX:+PrintGCDetails
- 是否使用串行垃圾回收器
- -XX:-UseSerialGC
- -xx:+UseSerialGC
- 是否打印GC收集细节
- 设值类型
- 公式:-XX:属性key=属性值value
- 例子
- -XX:MetaspaceSize=128m
- -XX:MaxTenuringThreshold=15
- 布尔类型
注意:JVM的XX参数之XmsXmx坑题
- -Xms等价于-XX:InitialHeapSize,初始大小内存,默认物理内存的1/64
- -Xmx等价于 -XX:MaxHeapSize,最大分配内存,默认为物理内存1/4
2、JVM初始默认值
(1)查看初始默认参数值
-XX:+PrintFlagsInitial
公式:java -XX:+PrintFlagsInitial
C:\Users\abc>java -XX:+PrintFlagsInitial
[Global flags]
int ActiveProcessorCount = -1 {product} {default}
uintx AdaptiveSizeDecrementScaleFactor = 4 {product} {default}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product} {default}
uintx AdaptiveSizePolicyCollectionCostMargin = 50 {product} {default}
uintx AdaptiveSizePolicyInitializingSteps = 20 {product} {default}
uintx AdaptiveSizePolicyOutputInterval = 0 {product} {default}
uintx AdaptiveSizePolicyWeight = 10 {product} {default}
...
(2)查看修改更新参数值
-XX:+PrintFlagsFinal
公式:java -XX:+PrintFlagsFinal
C:\Users\abc>java -XX:+PrintFlagsFinal
...
size_t HeapBaseMinAddress = 2147483648 {pd product} {default}
bool HeapDumpAfterFullGC = false {manageable} {default}
bool HeapDumpBeforeFullGC = false {manageable} {default}
bool HeapDumpOnOutOfMemoryError = false {manageable} {default}
ccstr HeapDumpPath = {manageable} {default}
uintx HeapFirstMaximumCompactionCount = 3 {product} {default}
uintx HeapMaximumCompactionInterval = 20 {product} {default}
uintx HeapSearchSteps = 3 {product} {default}
size_t HeapSizePerGCThread = 43620760 {product} {default}
bool IgnoreEmptyClassPaths = false {product} {default}
bool IgnoreUnrecognizedVMOptions = false {product} {default}
uintx IncreaseFirstTierCompileThresholdAt = 50 {product} {default}
bool IncrementalInline = true {C2 product} {default}
size_t InitialBootClassLoaderMetaspaceSize = 4194304 {product} {default}
uintx InitialCodeCacheSize = 2555904 {pd product} {default}
size_t InitialHeapSize := 268435456 {product} {ergonomic}
...
(3)查看修改变更值
PrintFlagsFinal举例,运行java命令的同时打印出参数
java -XX:+PrintFlagsFinal -XX:MetaspaceSize=512m HelloWorld
...
size_t MetaspaceSize := 536870912 {pd product} {default}
...
打印命令行参数
-XX:+PrintCommandLineFlags
C:\Users\abc>java -XX:+PrintCommandLineFlags -version
-XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=266613056 -XX:MarkStackSize=4
194304 -XX:MaxHeapSize=4265808896 -XX:MinHeapSize=6815736 -XX:+PrintCommandLineFlags -XX:ReservedCodeCacheSize=251658240 -XX:+Seg
mentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
openjdk version "15.0.1" 2020-10-20
OpenJDK Runtime Environment (build 15.0.1+9-18)
OpenJDK 64-Bit Server VM (build 15.0.1+9-18, mixed mode)
3、堆内存初始大小快速复习
JDK1.8之后将最初的永久代取消了,由元空间取代。
在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。
空间(Java8)与永久代(Java7)之间最大的区别在于:永久代使用的JVM的堆内存,但是Java8以后的元空间并不在虚拟机中而是使用本机物理内存。
因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制
public class JVMMemorySizeDemo {
public static void main(String[] args) throws InterruptedException {
// 返回Java虚拟机中内存的总量
long totalMemory = Runtime.getRuntime().totalMemory();
// 返回Java虚拟机中试图使用的最大内存量
long maxMemory = Runtime.getRuntime().maxMemory();
System.out.println(String.format("TOTAL_MEMORY(-Xms): %d B, %.2f MB.", totalMemory, totalMemory / 1024.0 / 1024));
System.out.println(String.format("MAX_MEMORY(-Xmx): %d B, %.2f MB.", maxMemory, maxMemory / 1024.0 / 1024));
}
}
输出结果
TOTAL_MEMORY(-Xms): 257425408 B, 245.50 MB.
MAX_MEMORY(-Xmx): 3793747968 B, 3618.00 MB.
4、常用基础参数介绍
(1)栈内存Xss
设置单个线程栈的大小,一般默认为512k~1024K
等价于-XX:ThreadStackSize
(2)元空间MetaspaceSize
-Xmn:设置年轻代大小
-XX:MetaspaceSize 设置元空间大小
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制
典型设置案例
-Xms128m -Xmx4096m -Xss1024k -XX:MetaspaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails-XX:+UseSerialGC
(3)PrintGCDetails回收前后对比
-XX:+PrintGCDetails 输出详细GC收集日志信息
设置参数 -Xms10m -Xmx10m -XX:+PrintGCDetails 运行以下程序
import java.util.concurrent.TimeUnit;
public class PrintGCDetailsDemo {
public static void main(String[] args) throws InterruptedException {
byte[] byteArray = new byte[10 * 1024 * 1024];
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
}
}
输出结果
[GC (Allocation Failure) [PSYoungGen: 778K->480K(2560K)] 778K->608K(9728K), 0.0029909 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 480K->480K(2560K)] 608K->616K(9728K), 0.0007890 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 480K->0K(2560K)] [ParOldGen: 136K->518K(7168K)] 616K->518K(9728K), [Metaspace: 2644K->2644K(1056768K)], 0.0058272 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 518K->518K(9728K), 0.0002924 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 518K->506K(7168K)] 518K->506K(9728K), [Metaspace: 2644K->2644K(1056768K)], 0.0056906 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.lun.jvm.PrintGCDetailsDemo.main(PrintGCDetailsDemo.java:9)
Heap
PSYoungGen total 2560K, used 61K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 3% used [0x00000000ffd00000,0x00000000ffd0f748,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 7168K, used 506K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 7% used [0x00000000ff600000,0x00000000ff67ea58,0x00000000ffd00000)
Metaspace used 2676K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 285K, capacity 386K, committed 512K, reserved 1048576K
(4)SurvivorRatio
调节新生代中 eden 和 S0、S1的空间比例,默认为 -XX:SuriviorRatio=8,Eden:S0:S1 = 8:1:1
假如设置成 -XX:SurvivorRatio=4,则为 Eden:S0:S1 = 4:1:1
SurvivorRatio值就是设置eden区的比例占多少,S0和S1相同。
(5)NewRatio
配置年轻代new 和老年代old 在堆结构的占比
默认:-XX:NewRatio=2 新生代占1,老年代2,年轻代占整个堆的1/3
-XX:NewRatio=4:新生代占1,老年代占4,年轻代占整个堆的1/5,
NewRadio值就是设置老年代的占比,剩下的1个新生代。
新生代特别小,会造成频繁的进行GC收集。
(6)MaxTenuringThreshold
晋升到老年代的对象年龄。
SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区,部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认为15),最终如果还是存活,就存入老年代。
这里就是调整这个次数的,默认是15,并且设置的值 在 0~15之间。
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻对象不经过Survivor区,直接进入老年代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大的值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概念。
三、强引用、软引用、弱引用、虚引用
1、强引用Reference
Reference类以及继承派生的类
当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。
// 这样定义的默认就是强应用
Object obj1 = new Object();
强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。
2、软引用SoftReference
软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。
对于只有软引用的对象来说,
- 当系统内存充足时它不会被回收,
- 当系统内存不足时它会被回收。
软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!
当内存充足的时候,软引用不用回收:
回收后,内存依然不足的话,还是会抛异常。
public class SoftReferenceDemo {
/**
* 内存够用的时候
* -XX:+PrintGCDetails
*/
public static void softRefMemoryEnough() {
// 创建一个强应用
Object o1 = new Object();
// 创建一个软引用
SoftReference<Object> softReference = new SoftReference<>(o1);
System.out.println(o1);
System.out.println(softReference.get());
o1 = null;
// 手动GC
System.gc();
System.out.println(o1);
System.out.println(softReference.get());
}
/**
* JVM配置,故意产生大对象并配置小的内存,让它的内存不够用了导致OOM,看软引用的回收情况
* -Xms5m -Xmx5m -XX:+PrintGCDetails
*/
public static void softRefMemoryNoEnough() {
System.out.println("========================");
// 创建一个强应用
Object o1 = new Object();
// 创建一个软引用
SoftReference<Object> softReference = new SoftReference<>(o1);
System.out.println(o1);
System.out.println(softReference.get());
o1 = null;
// 模拟OOM自动GC
try {
// 创建30M的大对象
byte[] bytes = new byte[30 * 1024 * 1024];
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(o1);
System.out.println(softReference.get());
}
}
public static void main(String[] args) {
softRefMemoryEnough();
//softRefMemoryNoEnough();
}
}
1、内存充足输出结果
java.lang.Object@15db9742
java.lang.Object@15db9742
[GC (System.gc()) [PSYoungGen: 2621K->728K(76288K)] 2621K->736K(251392K), 0.0011732 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 728K->0K(76288K)] [ParOldGen: 8K->519K(175104K)] 736K->519K(251392K), [Metaspace: 2646K->2646K(1056768K)], 0.0048782 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
null
java.lang.Object@15db9742
Heap
PSYoungGen total 76288K, used 1966K [0x000000076b380000, 0x0000000770880000, 0x00000007c0000000)
eden space 65536K, 3% used [0x000000076b380000,0x000000076b56ba70,0x000000076f380000)
from space 10752K, 0% used [0x000000076f380000,0x000000076f380000,0x000000076fe00000)
to space 10752K, 0% used [0x000000076fe00000,0x000000076fe00000,0x0000000770880000)
ParOldGen total 175104K, used 519K [0x00000006c1a00000, 0x00000006cc500000, 0x000000076b380000)
object space 175104K, 0% used [0x00000006c1a00000,0x00000006c1a81e88,0x00000006cc500000)
Metaspace used 2653K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 282K, capacity 386K, committed 512K, reserved 1048576K
2、内存不充足,软引用关联对象会被回收
java.lang.Object@15db9742
java.lang.Object@15db9742
[GC (Allocation Failure) [PSYoungGen: 756K->496K(1536K)] 756K->600K(5632K), 0.0009017 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 496K->480K(1536K)] 600K->624K(5632K), 0.0006772 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 480K->0K(1536K)] [ParOldGen: 144K->519K(4096K)] 624K->519K(5632K), [Metaspace: 2646K->2646K(1056768K)], 0.0055489 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 519K->519K(5632K), 0.0002674 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 519K->507K(4096K)] 519K->507K(5632K), [Metaspace: 2646K->2646K(1056768K)], 0.0052951 secs] [Times: user=0.11 sys=0.00, real=0.01 secs]
null
null
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.lun.jvm.SoftReferenceDemo.softRefMemoryNotEnough(SoftReferenceDemo.java:44)
at com.lun.jvm.SoftReferenceDemo.main(SoftReferenceDemo.java:58)
Heap
PSYoungGen total 1536K, used 30K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
eden space 1024K, 2% used [0x00000000ffe00000,0x00000000ffe07ac8,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 4096K, used 507K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
object space 4096K, 12% used [0x00000000ffa00000,0x00000000ffa7edd0,0x00000000ffe00000)
Metaspace used 2678K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 285K, capacity 386K, committed 512K, reserved 1048576K
3、弱引用WeakReference
弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,
对于只有弱引用的对象来说,只要垃圾回收机制一运行不管JVM的内存空间是否足够,都会回收该对象占用的内存。
import java.lang.ref.WeakReference;
public class WeakReferenceDemo {
public static void main(String[] args) {
Object o1 = new Object();
WeakReference<Object> weakReference = new WeakReference<>(o1);
System.out.println(o1);
System.out.println(weakReference.get());
o1 = null;
System.gc();
System.out.println(o1);
System.out.println(weakReference.get());
}
}
输出结果:
java.lang.Object@15db9742
java.lang.Object@15db9742
null
null
4、软引用和弱引用的适用场景
场景:假如有一个应用需要读取大量的本地图片
如果每次读取图片都从硬盘读取则会严重影响性能
如果一次性全部加载到内存中,又可能造成内存溢出
此时使用软引用可以解决这个问题。
设计思路:使用HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占的空间,从而有效地避免了OOM的问题
Map<String, SoftReference<String>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
5、WeakHashMap案例
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
public class WeakHashMapDemo {
public static void main(String[] args) {
myHashMap();
System.out.println("==========");
myWeakHashMap();
}
private static void myHashMap() {
Map<Integer, String> map = new HashMap<>();
Integer key = new Integer(1);
String value = "HashMap";
map.put(key, value);
System.out.println(map);
key = null;
System.gc();
System.out.println(map);
}
private static void myWeakHashMap() {
Map<Integer, String> map = new WeakHashMap<>();
Integer key = new Integer(1);
String value = "WeakHashMap";
map.put(key, value);
System.out.println(map);
key = null;
System.gc();
System.out.println(map);
}
}
输出结果
{1=HashMap}
{1=HashMap}
==========
{1=WeakHashMap}
{}
6、虚引用简介
虚引用需要java.lang.ref.PhantomReference类来实现。
顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。
虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。
PhantomReference的gei方法总是返回null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比fihalization机制更灵活的回收操作。
换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
7、ReferenceQueue引用队列
回收前需要被引用的,用队列保存下。
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;
public class ReferenceQueueDemo {
public static void main(String[] args) {
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();
System.out.println("执行GC操作");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(o1);
System.out.println(weakReference.get());
// 取队列中的内容
System.out.println(referenceQueue.poll());
}
}
输出结果:
java.lang.Object@15db9742
java.lang.Object@15db9742
null
==================
执行GC操作
null
null
java.lang.ref.WeakReference@6d06d69c
8、虚引用PhantomReference
Java提供了4种引用类型,在垃圾回收的时候,都有自己的特点。
ReferenceQueue是用来配合引用工作的,没有ReferenceQueue一样可以运行。
创建引用的时候可以指定关联的队列,当GC释放对象内存的时候,会将引用加入到引用队列,如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动这相当于是一种通知机制。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
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(500) ;
System.out.println(o1);
System.out.println(phantomReference.get());
System.out.println(referenceQueue.poll());
}
}
输出结果:
java.lang.Object@15db9742
null
null
==================
null
null
java.lang.ref.PhantomReference@6d06d69c
9、GCRoots和四大引用小总结
四、JVM常见错误
1、StackoverFlowError
2、OutOfMemoryError
- java.lang.OutOfMemoryError:java heap space
- java.lang.OutOfMemoryError:GC overhead limit exceeeded
- java.lang.OutOfMemoryError:Direct buffer memory
- java.lang.OutOfMemoryError:unable to create new native thread
- java.lang.OutOfMemoryError:Metaspace
(1)OOM之Java heap space
public class OOMEJavaHeapSpaceDemo {
/**
*
* -Xms10m -Xmx10m
*
* @param args
*/
public static void main(String[] args) {
byte[] array = new byte[80 * 1024 * 1024];
}
}
(2)OOM之GC overhead limit exceeded
GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC 都只回收了不到2%的极端情况下才会抛出。
假如不抛出GC overhead limit错误会发生什么情况呢?那就是GC清理的这么点内存很快会再次填满,迫使cc再次执行。这样就形成恶性循环,CPU使用率一直是100%,而Gc却没有任何成果。
import java.util.ArrayList;
import java.util.List;
public class OOMEGCOverheadLimitExceededDemo {
/**
*
* -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m
*
* @param args
*/
public static void main(String[] args) {
int i = 0;
List<String> list = new ArrayList<>();
try {
while(true) {
list.add(String.valueOf(++i).intern());
}
} catch (Exception e) {
System.out.println("***************i:" + i);
e.printStackTrace();
throw e;
}
}
}
输出结果
[GC (Allocation Failure) [PSYoungGen: 2048K->498K(2560K)] 2048K->1658K(9728K), 0.0033090 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2323K->489K(2560K)] 3483K->3305K(9728K), 0.0020911 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2537K->496K(2560K)] 5353K->4864K(9728K), 0.0025591 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2410K->512K(2560K)] 6779K->6872K(9728K), 0.0058689 secs] [Times: user=0.09 sys=0.00, real=0.01 secs]
[Full GC (Ergonomics) [PSYoungGen: 512K->0K(2560K)] [ParOldGen: 6360K->6694K(7168K)] 6872K->6694K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0894928 secs] [Times: user=0.42 sys=0.00, real=0.09 secs]
[Full GC (Ergonomics) [PSYoungGen: 2048K->1421K(2560K)] [ParOldGen: 6694K->6902K(7168K)] 8742K->8324K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0514932 secs] [Times: user=0.34 sys=0.00, real=0.05 secs]
[Full GC (Ergonomics) [PSYoungGen: 2048K->2047K(2560K)] [ParOldGen: 6902K->6902K(7168K)] 8950K->8950K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0381615 secs] [Times: user=0.13 sys=0.00, real=0.04 secs]
...省略89行...
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7044K->7044K(7168K)] 9092K->9092K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0360935 secs] [Times: user=0.25 sys=0.00, real=0.04 secs]
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7046K->7046K(7168K)] 9094K->9094K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0360458 secs] [Times: user=0.38 sys=0.00, real=0.04 secs]
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7048K->7048K(7168K)] 9096K->9096K(9728K), [Metaspace: 2651K->2651K(1056768K)], 0.0353033 secs] [Times: user=0.11 sys=0.00, real=0.04 secs]
***************i:147041
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7050K->7048K(7168K)] 9098K->9096K(9728K), [Metaspace: 2670K->2670K(1056768K)], 0.0371397 secs] [Times: user=0.22 sys=0.00, real=0.04 secs]
java.lang.OutOfMemoryError: GC overhead limit exceeded
[Full GC (Ergonomics) at java.lang.Integer.toString(Integer.java:401)
[PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7051K->7050K(7168K)] 9099K->9097K(9728K), [Metaspace: 2676K->2676K(1056768K)], 0.0434184 secs] [Times: user=0.38 sys=0.00, real=0.04 secs]
at java.lang.String.valueOf(String.java:3099)
at com.lun.jvm.OOMEGCOverheadLimitExceededDemo.main(OOMEGCOverheadLimitExceededDemo.java:19)
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
[Full GC (Ergonomics) [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: 7054K->513K(7168K)] 9102K->513K(9728K), [Metaspace: 2677K->2677K(1056768K)], 0.0056578 secs] [Times: user=0.11 sys=0.00, real=0.01 secs]
at java.lang.Integer.toString(Integer.java:401)
at java.lang.String.valueOf(String.java:3099)
at com.lun.jvm.OOMEGCOverheadLimitExceededDemo.main(OOMEGCOverheadLimitExceededDemo.java:19)
Heap
PSYoungGen total 2560K, used 46K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 2% used [0x00000000ffd00000,0x00000000ffd0bb90,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 513K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 7% used [0x00000000ff600000,0x00000000ff6807f0,0x00000000ffd00000)
Metaspace used 2683K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 285K, capacity 386K, committed 512K, reserved 1048576K
(3)OOM之Direct buffer memory
导致原因:
写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避兔了在Java堆和Native堆中来回复制数据。
- ByteBuffer.allocate(capability) 第一种方式是分配VM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢。
- ByteBuffer.allocateDirect(capability) 第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。
但如果不断分配本地内存,堆内存很少使用,那么JV就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序就直接崩溃了。
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
public class OOMEDirectBufferMemoryDemo {
/**
* -Xms5m -Xmx5m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
*
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
System.out.println(String.format("配置的maxDirectMemory: %.2f MB",//
sun.misc.VM.maxDirectMemory() / 1024.0 / 1024));
TimeUnit.SECONDS.sleep(3);
ByteBuffer bb = ByteBuffer.allocateDirect(6 * 1024 * 1024);
}
}
输出结果
[GC (Allocation Failure) [PSYoungGen: 1024K->504K(1536K)] 1024K->772K(5632K), 0.0014568 secs] [Times: user=0.09 sys=0.00, real=0.00 secs]
配置的maxDirectMemory: 5.00 MB
[GC (System.gc()) [PSYoungGen: 622K->504K(1536K)] 890K->820K(5632K), 0.0009753 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 316K->725K(4096K)] 820K->725K(5632K), [Metaspace: 3477K->3477K(1056768K)], 0.0072268 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Exception in thread "main" Heap
PSYoungGen total 1536K, used 40K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
eden space 1024K, 4% used [0x00000000ffe00000,0x00000000ffe0a3e0,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 4096K, used 725K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
object space 4096K, 17% used [0x00000000ffa00000,0x00000000ffab5660,0x00000000ffe00000)
Metaspace used 3508K, capacity 4566K, committed 4864K, reserved 1056768K
class space used 391K, capacity 394K, committed 512K, reserved 1048576K
java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:694)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.lun.jvm.OOMEDirectBufferMemoryDemo.main(OOMEDirectBufferMemoryDemo.java:20)
(4)OOM之unable to create new native thread
【1】故障演示
[1] 概念
不能够创建更多的新的线程了,也就是说创建线程的上限到达了。
高并发请求服务器时,经常会出现异常java.lang.OutOfMemoryError:unable to create new native thread,准确的说该native thread异常与对应的平台有关
[2] 导致原因
- 应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
- 服务器并不允许你的应用程序创建那么多线程,linux系统默认运行单个进程可以创建的线程为1024个,如果应用创建超过这个数量,就会报java.lang.OutOfMemoryError:unable to create new native thread
[3] 解决方法
- 想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建那么多线程,如果不是,改代码将线程数降到最低
- 对于有的应用,确实需要创建很多线程,远超过系统默认1024个线程限制,可以通过修改linux服务器配置,扩大linux默认限制。
上面程序在Linux OS(CentOS)运行,会出现下列的错误,线程数大概在900多个public class OOMEUnableCreateNewThreadDemo {
public static void main(String[] args) {
for (int i = 0; ; i++) {
System.out.println("************** i = " + i);
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError: unable to cerate new native thread
【2】上限调整
非root用户登录linux系统测试
服务器级别调参调优
查看系统线程限制数目
ulimit -u
修改系统线程限制数据
vim /etc/security/limits.d/90-nproc.conf
打开后发现除了root,其他账户都限制在1024个
假如我们想要张三这个用户运行,希望他生成的线程多一些,我们可以如下配置
(5)OOM之Metaspace元空间
使用java -XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:MetaspaceSize为21810376B(大约20.8M)
Java 8及之后的版本使用Metaspace来替代永久代。
Metaspace是方法区在Hotspot 中的实现,它与持久代最大的区别在于:Metaspace并不在虚拟机内存中而是使用本地内存也即在Java8中, classe metadata(the virtual machines internal presentation of Java class),被存储在叫做Metaspace native memory。
永久代(Java8后被原空向Metaspace取代了)存放了以下信息:
- 虚拟机加载的类信息
- 常量池
- 静态变量
- 即时编译后的代码
案例:模拟Metaspace空间溢出,我们借助CGlib直接操作字节码运行时不断生成类往元空间里面灌,类占据的空间总是会超过Metaspace指定的空间的大小的。
1、添加CGlib依赖
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.10</version>
</dependency>
2、
import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class OOMEMetaspaceDemo {
// 静态类
static class OOMObject {}
/**
* -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*
* @param args
*/
public static void main(final String[] args) {
// 模拟计数多少次以后发生异常
int i =0;
try {
while (true) {
i++;
// 使用Spring的动态字节码技术
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, args);
}
});
enhancer.create();
}
} catch (Throwable e) {
System.out.println("发生异常的次数:" + i);
e.printStackTrace();
} finally {
}
}
}
3、输出结果
发生异常的次数:569
java.lang.OutOfMemoryError: Metaspace
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)
at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
at com.lun.jvm.OOMEMetaspaceDemo.main(OOMEMetaspaceDemo.java:37)
五、垃圾收集器回收种类
GC算法(引用计数、复制、标记清除、标记整理)是内存回收的方法论,垃圾收集器就是算法落地的实现。
因为目前为止还没有完美的收集器的出现,更加没有万能的收集器,只是针对具体应用最合适的收集器,进行分代收集。
1、四种主要垃圾回收方式
- Serial
- Parallel
- CMS
- G1
- 串行垃圾回收器(Serial)
- 单线程环境设计且只使用一个线程进行垃圾收集,会暂停所有的用户线程,只有当垃圾回收完成时,才会重新唤醒主线程继续执行,所以不适合服务器环境
- 并行垃圾回收器(Parallel)
- 多个垃圾收集线程并行工作,此时用户线程也是阻塞的,适用于科学计算、大数据处理等弱交互场景,也就是说Serial和Parallel其实是类似的,不过是多了几个线程进行垃圾收集,但是主线程都会被暂停,但是并行垃圾收集器处理时间,肯定比串行的垃圾收集器要更短
- 并发垃圾回收器(CMS)
- 用户线程和垃圾收集线程同时执行(不一定是并行,可能是交替执行),不需要停顿用户线程,互联网公司都在使用,适用于响应时间有要求的场景
- G1垃圾回收器
- G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收
- ZGC(Java11的)
2、查看默认的垃圾收集器
java -XX:+PrintCommandLineFlags -version
输出结果
C:\Users\abc>java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=266613056 -XX:MaxHeapSize=4265808896 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)
从结果看到-XX:+UseParallelGC,也就是说默认的垃圾收集器是并行垃圾回收器。
或者
jps -l
得到Java程序号
jinfo -flags (Java程序号)
3、JVM默认的七大垃圾收集器
- 年轻代GC
- UserSerialGC:串行垃圾收集器
- UserParallelGC:并行垃圾收集器
- UserParNewGC:年轻代的并行垃圾回收器
- 老年代GC
- UserSerialOldGC:串行老年代垃圾收集器(已经被移除)
- UserParallelOldGC:老年代的并行垃圾回收器
- UserConcMarkSweepGC:(CMS)并发标记清除
- 老嫩通吃
- UserG1GC:G1垃圾收集器
GC之七大垃圾收集器概述
垃圾收集器就来具体实现这些GC算法并实现内存回收。
不同厂商、不同版本的虚拟机实现差别很大,HotSpot中包含的收集器如下图所示:
新生代
- 串行GC(Serial)/(Serial Copying)
- 并行GC(ParNew)
- 并行回收GC(Parallel)/(Parallel Scavenge)
GC之约定参数说明
- DefNew:Default New Generation
- Tenured:Old
- ParNew:Parallel New Generation
- PSYoungGen:Parallel Scavenge
- ParOldGen:Parallel Old Generation
六、Linux重要命令
1、top—整机性能查看
主要看load average,CPU,MEN三部分
load average表示系统负载,即任务队列的平均长度。 三个数值分别为 1分钟、5分钟、15分钟前到现在的平均值。 load average: 如果这个数除以逻辑CPU的数量,结果高于5的时候就表明系统在超负荷运转了。 top详解
2、uptime—系统性能命令的精简版
3、vmstat—cpu查看命令
- procs
- r:运行和等待的CPU时间片的进程数,原则上1核的CPU的运行队列不要超过2,整个系统的运行队列不超过总核数的2倍,否则代表系统压力过大,我们看蘑菇博客测试服务器,能发现都超过了2,说明现在压力过大
- b:等待资源的进程数,比如正在等待磁盘I/O、网络I/O等
- cpu
- us:用户进程消耗CPU时间百分比,us值高,用户进程消耗CPU时间多,如果长期大于50%,优化程序
- sy:内核进程消耗的CPU时间百分比
- us+sy参考值为80%,如果大于80%,说明可能存在CPU不足,从上面的图片可以看错,us+sy还没有超过80%,因此说明CPU的消耗不是很高
- id:处于空闲的CPU百分比
- wa:系统等待IO的CPU时间百分比
- st:来自于一个虚拟机偷取的CPU时间比
4、mpstat、pidstat—cpu查看命令
(1)mpstat
mpstat -P ALL 2
(2)pidstat 每个进程使用cpu的用量分解信息
pidstat -u 1 -p 进程编号
5、free、pidstat — 内存查看命令
应用程序可用内存数
经验值
- 应用程序可用内存l系统物理内存>70%内存充足
- 应用程序可用内存/系统物理内存<20%内存不足,需要增加内存
- 20%<应用程序可用内存/系统物理内存<70%内存基本够用
m/g:兆/吉
查看额外
pidstat -p 进程号 -r 采样间隔秒数
6、df—硬盘查看
7、iostat、pidstat—磁盘IO查看
磁盘I/O性能评估
磁盘块设备分布
- rkB/s每秒读取数据量kB;wkB/s每秒写入数据量kB;
- svctm lO请求的平均服务时间,单位毫秒;
- await l/O请求的平均等待时间,单位毫秒;值越小,性能越好;
- util一秒中有百分几的时间用于I/O操作。接近100%时,表示磁盘带宽跑满,需要优化程序或者增加磁盘;
- rkB/s、wkB/s根据系统应用不同会有不同的值,但有规律遵循:长期、超大数据读写,肯定不正常,需要优化程序读取。
- svctm的值与await的值很接近,表示几乎没有IO等待,磁盘性能好。
- 如果await的值远高于svctm的值,则表示IO队列等待太长,需要优化程序或更换更快磁盘。
8、ifstat—网络IO查看
默认本地没有,下载ifstat
wget http://gael.roualland.free.fr/lifstat/ifstat-1.1.tar.gz
tar -xzvf ifstat-1.1.tar.gz
cd ifstat-1.1
./configure
make
make install
查看网络IO
各个网卡的in out
观察网络负载情况程序
网络读写是否正常
- 程序网络I/O优化
- 增加网络I/O带宽
七、CPU占用过高的定位分析思路
1、先用top命令找出CPU占比最高的
2、ps -ef或者jps进一步定位,得知是一个怎么样的一个后台程序作搞屎棍
3、定位到具体线程或者代码
- ps -mp 进程 -o THREAD,tid,time
- -m 显示所有的线程
- -p pid进程使用cpu的时间
- -o 该参数后是用户自定义格式