1、强软弱虚引用
强引用
当内存不足的时候,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,打死也不回收~!
强引用是我们最常见的普通对象引用,只要还有一个强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在Java中最常见的就是强引用,
把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对
象以后永远都不会被用到,JVM也不会回收,因此强引用是造成Java内存泄漏的主要原因之一。
public class StrongReferenceDemo {public static void main(String[] args) {// 这样定义的默认就是强应用Object obj1 = new Object();// 使用第二个引用,指向刚刚创建的Object对象Object obj2 = obj1;// 置空obj1 = null;// 垃圾回收System.gc();System.out.println(obj1);System.out.println(obj2);}
}
输出结果我们能够发现,即使 obj1 被设置成了null,然后调用gc进行回收,但是也没有回收实例出来的对象,obj2还是能够指向该地址,也就是说垃圾回收器,
并没有将该对象进行垃圾回收
null
java.lang.Object@14ae5a5
软引用
软引用是一种相对弱化了一些的引用,需要用Java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集,对于只有软引用的对象来讲:
- 当系统内存充足时,它不会被回收
- 当系统内存不足时,它会被回收
软引用通常在对内存敏感的程序中,比如高速缓存就用到了软引用,内存够用的时候就保留,不够用就回收
public class SoftReferenceDemo {/*** 内存够用的时候*/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;// 手动GCSystem.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自动GCtry {// 创建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();}
}
我们写了两个方法,一个是内存够用的时候,一个是内存不够用的时候
我们首先查看内存够用的时候,首先输出的是 o1 和 软引用的 softReference,我们都能够看到值
然后我们把o1设置为null,执行手动GC后,我们发现softReference的值还存在,说明内存充足的时候,软引用的对象不会被回收
java.lang.Object@14ae5a5
java.lang.Object@14ae5a5[GC (System.gc()) [PSYoungGen: 1396K->504K(1536K)] 1504K->732K(5632K), 0.0007842 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 228K->651K(4096K)] 732K->651K(5632K), [Metaspace: 3480K->3480K(1056768K)], 0.0058450 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] null
java.lang.Object@14ae5a5
下面我们看当内存不够的时候,我们使用了JVM启动参数配置,给初始化堆内存为5M
-Xms5m -Xmx5m -XX:+PrintGCDetails
但是在创建对象的时候,我们创建了一个30M的大对象
// 创建30M的大对象
byte[] bytes = new byte[30 * 1024 * 1024];
这就必然会触发垃圾回收机制,这也是中间出现的垃圾回收过程,最后看结果我们发现,o1 和 softReference都被回收了,因此说明,软引用在内存不足的时候,会自动回收
java.lang.Object@7f31245a
java.lang.Object@7f31245a[GC (Allocation Failure) [PSYoungGen: 31K->160K(1536K)] 682K->811K(5632K), 0.0003603 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 160K->96K(1536K)] 811K->747K(5632K), 0.0006385 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 96K->0K(1536K)] [ParOldGen: 651K->646K(4096K)] 747K->646K(5632K), [Metaspace: 3488K->3488K(1056768K)], 0.0067976 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 646K->646K(5632K), 0.0004024 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 646K->627K(4096K)] 646K->627K(5632K), [Metaspace: 3488K->3488K(1056768K)], 0.0065506 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] null
null
弱引用
不管内存是否够,只要有GC操作就会进行回收
弱引用需要用 java.lang.ref.WeakReference
类来实现,它比软引用生存期更短
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的空间。
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());}
}
我们看结果,能够发现,我们并没有制造出OOM内存溢出,而只是调用了一下GC操作,垃圾回收就把它给收集了
java.lang.Object@14ae5a5
java.lang.Object@14ae5a5[GC (System.gc()) [PSYoungGen: 5246K->808K(76288K)] 5246K->816K(251392K), 0.0008236 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 808K->0K(76288K)] [ParOldGen: 8K->675K(175104K)] 816K->675K(251392K), [Metaspace: 3494K->3494K(1056768K)], 0.0035953 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] null
null
软引用和弱引用的使用场景
场景:假如有一个应用需要读取大量的本地图片
- 如果每次读取图片都从硬盘读取则会严重影响性能
- 如果一次性全部加载到内存中,又可能造成内存溢出
此时使用软引用可以解决这个问题
设计思路:使用HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占的空间,从而有
效地避免了OOM的问题
Map<String, SoftReference<String>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
WeakHashMap是什么?
比如一些常常和底层打交道的,mybatis等,底层都应用到了WeakHashMap
WeakHashMap和HashMap类似,只不过它的Key是使用了弱引用的,也就是说,当执行GC的时候,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);}
}
虚引用
概念
虚引用又称为幽灵引用,需要java.lang.ref.PhantomReference
类来实现
顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
如果一个对象持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列
ReferenceQueue联合使用。
虚引用的主要作用和跟踪对象被垃圾回收的状态,仅仅是提供一种确保对象被finalize以后,做某些事情的机制。
PhantomReference的get方法总是返回null,因此无法访问对象的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比
finalization机制更灵活的回收操作
换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候,收到一个系统通知或者后续添加进一步的处理,Java技术允许使用finalize()方法在
垃圾收集器将对象从内存中清除出去之前,做必要的清理工作
这个就相当于Spring AOP里面的后置通知
场景
一般用于在回收时候做通知相关操作
引用队列 ReferenceQueue
软引用,弱引用,虚引用在回收之前,需要在引用队列保存一下
我们在初始化的弱引用或者虚引用的时候,可以传入一个引用队列
Object o1 = new Object();// 创建引用队列
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();// 创建一个弱引用
WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue);
那么在进行GC回收的时候,弱引用和虚引用的对象都会被回收,但是在回收之前,它会被送至引用队列中
完整代码如下:
public class PhantomReferenceDemo {public static void main(String[] args) {Object o1 = new Object();// 创建引用队列ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();// 创建一个弱引用WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue);// 创建一个弱引用
// PhantomReference<Object> weakReference = new PhantomReference<>(o1, referenceQueue);System.out.println(o1);System.out.println(weakReference.get());// 取队列中的内容System.out.println(referenceQueue.poll());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@14ae5a5
java.lang.Object@14ae5a5
null
执行GC操作
null
null
java.lang.ref.WeakReference@7f3124
从这里我们能看到,在进行垃圾回收后,我们弱引用对象,也被设置成null,但是在队列中还能够导出该引用的实例,这就说明在回收之前,该弱引用的实例被放
置引用队列中了,我们可以通过引用队列进行一些后置操作
2、经典错误
JVM中常见的两个错误
StackoverFlowError :栈溢出
OutofMemoryError: java heap space:堆溢出
除此之外,还有以下的错误
- java.lang.StackOverflowError
- 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
架构
OutOfMemoryError和StackOverflowError是属于Error,不是Exception
StackOverFlowError
堆栈溢出,我们有最简单的一个递归调用,就会造成堆栈溢出,也就是深度的方法调用
栈一般是512K,不断的深度调用,直到栈被撑破
public class StackOverflowErrorDemo {public static void main(String[] args) {stackOverflowError();}/*** 栈一般是512K,不断的深度调用,直到栈被撑破* Exception in thread "main" java.lang.StackOverflowError*/private static void stackOverflowError() {stackOverflowError();}
}
运行结果
Exception in thread "main" java.lang.StackOverflowErrorat com.moxi.interview.study.oom.StackOverflowErrorDemo.stackOverflowError(StackOverflowErrorDemo.java:17)
OutOfMemoryError:java heap space
创建了很多对象,导致堆空间不够存储
public class JavaHeapSpaceDemo {public static void main(String[] args) {// 堆空间的大小 -Xms10m -Xmx10m// 创建一个 80M的字节数组byte [] bytes = new byte[80 * 1024 * 1024];}
}
我们创建一个80M的数组,会直接出现Java heap space
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
GC overhead limit exceeded
GC回收时间过长时会抛出OutOfMemoryError,过长的定义是,超过了98%的时间用来做GC,并且回收了不到2%的堆内存
连续多次GC都只回收了不到2%的极端情况下,才会抛出。假设不抛出GC overhead limit 错误会造成什么情况呢?
那就是GC清理的这点内存很快会再次被填满,迫使GC再次执行,这样就形成了恶性循环,CPU的使用率一直都是100%,而GC却没有任何成果。
Direct buffer memory
这是由于NIO引起的
写NIO程序的时候经常会使用ByteBuffer来读取或写入数据,这是一种基于通道(Channel) 与 缓冲区(Buffer)的I/O方式,它可以使用Native 函数库直接分配堆外内
存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
ByteBuffer.allocate(capability):第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢
ByteBuffer.allocteDirect(capability):第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存的拷贝,所以速度相对较快
但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象就不会被回收,这时候怼内存充足,但本地内存可能已经使用光
了,再次尝试分配本地内存就会出现OutOfMemoryError,那么程序就奔溃了。
一句话说:本地内存不足,但是堆内存充足的时候,就会出现这个问题
我们使用 -XX:MaxDirectMemorySize=5m 配置能使用的堆外物理内存为5M
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
然后我们申请一个6M的空间
// 只设置了5M的物理内存使用,但是却分配 6M的空间
ByteBuffer bb = ByteBuffer.allocateDirect(6 * 1024 * 1024);
这个时候,运行就会出现问题了
配置的maxDirectMemory:5.0MB
[GC (System.gc()) [PSYoungGen: 2030K->488K(2560K)] 2030K->796K(9728K), 0.0008326 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 488K->0K(2560K)] [ParOldGen: 308K->712K(7168K)] 796K->712K(9728K), [Metaspace: 3512K->3512K(1056768K)], 0.0052052 secs] [Times: user=0.09 sys=0.00, real=0.00 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memoryat java.nio.Bits.reserveMemory(Bits.java:693)at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)at com.moxi.interview.study.oom.DIrectBufferMemoryDemo.main(DIrectBufferMemoryDemo.java:19)
unable to create new native thread
导致原因:
-
应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
-
服务器并不允许你的应用程序创建这么多线程,linux系统默认运行单个进程可以创建的线程为1024个,如果应用创建超过这个数量,就会报
java.lang.OutOfMemoryError:unable to create new native thread
解决方法:
- 想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低
- 对于有的应用,确实需要创建很多线程,远超过linux系统默认1024个线程限制,可以通过修改linux服务器配置,扩大linux默认限制
public class UnableCreateNewThreadDemo {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();}}
}
这个时候,就会出现下列的错误,线程数大概在 900多个
Exception in thread "main" java.lang.OutOfMemoryError: unable to cerate new native thread
linux环境可以在如下配置文件中修改对应用户最大创建线程数量
/etc/security/limits.d/20-nproc.conf
Metaspace
元空间内存不足,Matespace元空间应用的是本地内存
-XX:MetaspaceSize
的初始化大小为20M
元空间是什么
元空间就是我们的方法区,存放的是类模板,类信息,常量池等
Metaspace是方法区HotSpot中的实现,它与永久代最大的区别在于:Metaspace并不在虚拟内存中,而是使用本地内存,也即在java8中,class metadata(the
virtual machines internal presentation of Java class),被存储在叫做Matespace的native memory
永久代(java8后背元空间Metaspace取代了)存放了以下信息:
- 虚拟机加载的类信息
- 常量池
- 静态变量
- 即时编译后的代码
模拟Metaspace空间溢出,我们不断生成类往元空间里灌输,类占据的空间总会超过Metaspace指定的空间大小
代码
在模拟异常生成时候,因为初始化的元空间为20M,因此我们使用JVM参数调整元空间的大小,为了更好的效果
-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
代码如下:
public class MetaspaceOutOfMemoryDemo {// 静态类static class OOMTest {}public static void main(final String[] args) {// 模拟计数多少次以后发生异常int i =0;try {while (true) {i++;// 使用Spring的动态字节码技术Enhancer enhancer = new Enhancer();enhancer.setSuperclass(OOMTest.class);enhancer.setUseCache(false);enhancer.setCallback(new MethodInterceptor() {@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {return methodProxy.invokeSuper(o, args);}});}} catch (Exception e) {System.out.println("发生异常的次数:" + i);e.printStackTrace();} finally {}}
}
会出现以下错误:
发生异常的次数: 201
java.lang.OutOfMemoryError:Metaspace
3、垃圾收集器
GC垃圾回收算法和垃圾收集器关系
天上飞的理念,要有落地的实现(垃圾收集器就是GC垃圾回收算法的实现)
GC算法是内存回收的方法论,垃圾收集器就是算法的落地实现
GC算法主要有以下几种
- 引用计数(几乎不用,无法解决循环引用的问题)
- 复制拷贝(用于新生代)
- 标记清除(用于老年代)
- 标记整理(用于老年代)
因为目前为止还没有完美的收集器出现,更没有万能的收集器,只是针对具体应用最合适的收集器,进行分代收集(那个代用什么收集器)
四种主要的垃圾收集器
- Serial:串行回收
-XX:+UseSeriallGC
- Parallel:并行回收
-XX:+UseParallelGC
- CMS:并发标记清除
- G1
- ZGC:(java 11 出现的)
Serial
串行垃圾回收器,它为单线程环境设计且值使用一个线程进行垃圾收集,会暂停所有的用户线程,只有当垃圾回收完成时,才会重新唤醒主线程继续执行。所以不适合服务器环境
Parallel
并行垃圾收集器,多个垃圾收集线程并行工作,此时用户线程也是阻塞的,适用于科学计算 / 大数据处理等弱交互场景,也就是说Serial 和 Parallel其实是类似的,不过是多了几个线程进行垃圾收集,但是主线程都会被暂停,但是并行垃圾收集器处理时间,肯定比串行的垃圾收集器要更短
CMS
并发标记清除,用户线程和垃圾收集线程同时执行(不一定是并行,可能是交替执行),不需要停顿用户线程,互联网公司都在使用,适用于响应时间有要求的场景。并发是可以有交互的,也就是说可以一边进行收集,一边执行应用程序。
G1
G1垃圾回收器将堆内存分割成不同区域,然后并发的进行垃圾回收
垃圾收集器总结
注意:并行垃圾回收在单核CPU下可能会更慢
查看默认垃圾收集器
使用下面JVM命令,查看配置的初始参数
-XX:+PrintCommandLineFlags
然后运行一个程序后,能够看到它的一些初始配置信息
-XX:InitialHeapSize=266376000 -XX:MaxHeapSize=4262016000 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
移动到最后一句,就能看到 -XX:+UseParallelGC
说明使用的是并行垃圾回收
-XX:+UseParallelGC
默认垃圾收集器有哪些
Java中一共有7大垃圾收集器
- UserSerialGC:串行垃圾收集器
- UserParallelGC:并行垃圾收集器
- UseConcMarkSweepGC:(CMS)并发标记清除
- UseParNewGC:年轻代的并行垃圾回收器
- UseParallelOldGC:老年代的并行垃圾回收器
- UseG1GC:G1垃圾收集器
- UserSerialOldGC:串行老年代垃圾收集器(已经被移除)
底层源码
各垃圾收集器的使用范围
新生代使用的:
- Serial Copying: UserSerialGC,串行垃圾回收器
- Parallel Scavenge:UserParallelGC,并行垃圾收集器
- ParNew:UserParNewGC,新生代并行垃圾收集器
老年区使用的:
- Serial Old:UseSerialOldGC,老年代串行垃圾收集器
- Parallel Compacting(Parallel Old):UseParallelOldGC,老年代并行垃圾收集器
- CMS:UseConcMarkSwepp,并行标记清除垃圾收集器
各区都能使用的:
G1:UseG1GC,G1垃圾收集器
垃圾收集器就来具体实现这些GC算法并实现内存回收,不同厂商,不同版本的虚拟机实现差别很大,HotSpot中包含的收集器如下图所示:
部分参数说明
- DefNew:Default New Generation
- Tenured:Old
- ParNew:Parallel New Generation
- PSYoungGen:Parallel Scavenge
- ParOldGen:Parallel Old Generation
Java中的Server和Client模式
使用范围:一般使用Server模式,Client模式基本不会使用
操作系统
- 32位的Window操作系统,不论硬件如何都默认使用Client的JVM模式
- 32位的其它操作系统,2G内存同时有2个cpu以上用Server模式,低于该配置还是Client模式
- 64位只有Server模式
GC之Serial收集器
是一个单线程单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它收集结束。
串行收集器是最古老,最稳定以及效率高的收集器,只使用一个线程去回收但其在垃圾收集过程中可能会产生较长的停顿(Stop-The-World 状态)。 虽然在收集垃
圾过程中需要暂停所有其它的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃
圾收集器依然是Java虚拟机运行在Client模式下默认的新生代垃圾收集器
对应JVM参数是:-XX:+UseSerialGC
开启后会使用:Serial(Young区用) + Serial Old(Old区用) 的收集器组合
表示:新生代、老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintConmandLineFlags -XX:+UseSerialGC
GC之ParNew收集器
并行收集器,使用多线程进行垃圾回收,在垃圾收集,会Stop-the-World暂停其他所有的工作线程直到它收集结束
ParNew收集器其实就是Serial收集器新生代的并行多线程版本,最常见的应用场景时配合老年代的CMS GC工作,其余的行为和Serial收集器完全一样,ParNew垃
圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。它是很多Java虚拟机运行在Server模式下新生代的默认垃圾收集器。
常见对应JVM参数:-XX:+UseParNewGC 启动ParNew收集器,只影响新生代的收集,不影响老年代
开启上述参数后,会使用:ParNew(Young区用) + Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintConmandLineFlags -XX:+UseParNewGC
但是会出现警告,即 ParNew 和 Serial Old 这样搭配,Java8已经不再被推荐
备注:-XX:ParallelGCThreads=数字N 表示启动多少个GC线程
cpu>8 N= 5/8
cpu<8 N=实际个数
GC之Parallel收集器
Parallel Scavenge收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。一句话:串行
收集器在新生代和老年代的并行化。
它重点关注的是:
可控制的吞吐量(Thoughput=运行用户代码时间(运行用户代码时间+垃圾收集时间),也即比如程序运行100分钟,垃圾收集时间1分钟,吞吐量就是99% )。高吞吐量
意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务。
自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。(自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整
这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量。
常用JVM参数:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)使用Parallel Scanvenge收集器。
开启该参数后:新生代使用复制算法,老年代使用标记-整理算法
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintConmandLineFlags -XX:+UseParallelGC
GC之ParallelOld收集器
Parallel Old收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,Parallel Old收集器在JDK1.6才开始提供。
在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配老年代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量。在JDK1.6以前
(Parallel Scavenge + Serial Old)
Parallel Old正是为了在老年代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,JDK1.8后可以考虑新生代Parallel Scavenge和老年代Parallel
Old 收集器的搭配策略。在JDK1.8及后(Parallel Scavenge + Parallel Old)
JVM常用参数:
-XX +UseParallelOldGC:使用Parallel Old收集器,设置该参数后,新生代Parallel+老年代 Parallel Old
使用老年代并行收集器:
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintConmandLineFlags -XX:+UseParallelOldlGC
GC之CMS收集器
CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以最短回收停顿时间为目标的收集器
适合应用在互联网或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。
CMS非常适合堆内存大,CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。
Concurrent Mark Sweep:并发标记清除,并发收集低停顿,并发指的是与用户线程一起执行
开启该收集器的JVM参数: -XX:+UseConcMarkSweepGC 开启该参数后,会自动将 -XX:+UseParNewGC打开,开启该参数后,使用ParNew(young 区用)+ CMS(Old 区用) + Serial Old 的收集器组合,Serial Old将作为CMS出错的后备收集器
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC
四个步骤
-
初始标记(CMS initial mark)
- 只是标记一个GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程
-
并发标记(CMS concurrent mark)和用户线程一起
- 进行GC Roots跟踪过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象
-
重新标记(CMS remark)
-
为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程,由于并发标记时,用户
线程依然运行,因此在正式清理前,在做修正
-
-
并发清除(CMS concurrent sweep)和用户线程一起
-
清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象,由于耗时最长的并发标记和并发清除过程中,垃
圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。
-
优点:并发收集低停顿
缺点:并发执行,对CPU资源压力大,采用的标记清除算法会导致大量碎片
由于并发进行,CMS在收集与应用线程会同时增加对堆内存的占用,也就是说,CMS必须在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担
保机制,串行老年代收集器将会以STW方式进行一次GC,从而造成较大的停顿时间
标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩,CMS也提供了参数
-XX:CMSFullGCSBeForeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC
GC之SerialOld收集器
Serial Old是Serial垃圾收集器老年代版本,它同样是一个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在Client默认的Java虚拟机中默认的老年代垃圾收集器
在Server模式下,主要有两个用途(了解,版本已经到8及以后)
- 在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用(Parallel Scavenge + Serial Old)
- 作为老年代版中使用CMS收集器的后备垃圾收集方案。
配置方法:
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintConmandLineFlags -XX:+UseSerialOldlGC
该垃圾收集器,目前已经不推荐使用了
垃圾收集器如何选择
组合的选择
- 单CPU或者小内存,单机程序
- -XX:+UseSerialGC
- 多CPU,需要最大的吞吐量,如后台计算型应用
- -XX:+UseParallelGC(这两个相互激活)
- -XX:+UseParallelOldGC
- 多CPU,追求低停顿时间,需要快速响应如互联网应用
- -XX:+UseConcMarkSweepGC
- -XX:+ParNewGC
参数 | 新生代垃圾收集器 | 新生代算法 | 老年代垃圾收集器 | 老年代算法 |
---|---|---|---|---|
-XX:+UseSerialGC | SerialGC | 复制 | SerialOldGC | 标记整理 |
-XX:+UseParNewGC | ParNew | 复制 | SerialOldGC | 标记整理 |
-XX:+UseParallelGC | Parallel [Scavenge] | 复制 | Parallel Old | 标记整理 |
-XX:+UseConcMarkSweepGC | ParNew | 复制 | CMS + Serial Old的收集器组合,Serial Old作为CMS出错的后备收集器 | 标记清除 |
-XX:+UseG1GC | G1整体上采用标记整理算法 | 局部复制 |
G1垃圾收集器
开启G1垃圾收集器
-XX:+UseG1GC
以前收集器的特点
- 年轻代和老年代是各自独立且连续的内存块
- 年轻代收集使用单eden + S0 + S1 进行复制算法
- 老年代收集必须扫描珍整个老年代区域
- 都是以尽可能少而快速地执行GC为设计原则
G1是什么
G1:Garbage-First 收集器,是一款面向服务端应用的收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能满足垃圾收集暂停时间的要
求。另外,它还具有一下特征:
- 像CMS收集器一样,能与应用程序并发执行
- 整理空闲空间更快
- 需要更多的时间来预测GC停顿时间
- 不希望牺牲大量的吞吐量性能
- 不需要更大的Java Heap
G1收集器设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色
- G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
- G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。
CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优
点,JAVA7发布了一个新的垃圾收集器-G1垃圾收集器
G1是在2012年才在JDK1.7中可用,Oracle官方计划在JDK9中将G1变成默认的垃圾收集器以替代CMS,它是一款面向服务端应用的收集器,主要应用在多CPU和
大内存服务器环境下,极大减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换Java8以前的CMS收集器
主要改变时:Eden,Survivor和Tenured等内存区域不再是连续了,而是变成一个个大小一样的region,每个region从1M到32M不等。一个region有可能属于
Eden,Survivor或者Tenured内存区域。
特点
-
G1能充分利用多CPU,多核环境硬件优势,尽量缩短STW
-
G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片
-
宏观上看G1之中不再区分年轻代和老年代。把内存划分成多个独立的子区域(Region),可以近似理解为一个围棋的棋盘
-
G1收集器里面将整个内存区域都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但他们不再是物理隔离的,
而是通过一部分Region的集合且不需要Region是连续的,也就是说依然会采取不同的GC方式来处理不同的区域
-
G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代和老年代的区别,也不需要完全独立的Survivor(to space)堆做复制准备,G1只有逻辑上的
分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换。
底层原理
Region区域化垃圾收集器,化整为零,打破了原来新生区和老年区的壁垒,避免了全内存扫描,只需要按照区域来进行扫描即可。
区域化内存划片Region,整体遍为了一些列不连续的内存区域,避免了全内存区的GC操作。
核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置子区域大小
在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切
换。启动时可以通过参数-XX:G1HeapRegionSize=n
可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
大小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为:32MB*2048 = 64G内存
Region区域化垃圾收集器
Region区域化垃圾收集器
G1将新生代、老年代的物理空间划分取消了
同时对内存进行了区域划分
G1算法将堆划分为若干个区域(Reign),它仍然属于分代收集器,这些Region的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存
活对象拷贝到老年代或者Survivor空间
这些Region的一部分包含老年代,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压
缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片的问题存在了。
在G1中,还有一种特殊的区域,叫做Humongous(巨大的)区域,如果一个对象占用了空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象,这
些巨型对象默认直接分配在老年代,但是如果他是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响,为了解决这个问题,G1划分了一个Humongous
区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H区来存储,为了能找到连续的H区,有时候不得不启动Full GC。
回收步骤
针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集 + 形成连续的内存块,避免内碎片
- Eden区的数据移动到Survivor区,加入出现Survivor区空间不够,Eden区数据会晋升到Old区
- Survivor区的数据移动到新的Survivor区,部分数据晋升到Old区
- 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行
回收完成后
小区域收集 + 形成连续的内存块,最后在收集完成后,就会形成连续的内存空间,这样就解决了内存碎片的问题
四步过程
- 初始标记:只标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing(链路扫描)的过程
- 最终标记:修正并发标记期间,因为程序运行导致标记发生变化的那一部分对象
- 筛选回收:根据时间来进行价值最大化回收
参数配置
开发人员仅仅需要申明以下参数即可
三步归纳:-XX:+UseG1GC -Xmx32G -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=n:最大GC停顿时间单位毫秒,这是个软目标,JVM尽可能停顿小于这个时间
G1和CMS比较
- G1不会产生内碎片
- 是可以精准控制停顿。该收集器是把整个堆(新生代、老年代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。
4、linux服务问题诊断
top
使用top命令的话,重点关注的是 %CPU、%MEM 、load average 三个指标
- load average三个指标:分别代表1、5、15分钟的负载情况
在这个命令下,按1的话,可以看到每个CPU的占用情况
uptime:系统性能命令的精简版
vmstat
- 查看CPU(包含但是不限于)
- 查看额外
- 查看所有CPU核信息:mpstat -p ALL 2
- 每个进程使用CPU的用量分解信息:pidstat -u 1 -p 进程编号
命令格式:vmstat -n 2 3
一般vmstat工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔数(单位秒),第二个参数是采样的次数
r列表示运行和等待CPU时间片的进程数,这个值如果长期大于系统CPU的个数,说明CPU不足,需要增加CPU。
b列表示在等待资源的进程数,如正在等待I/O、内存交换等。
us列显示了用户进程消耗的CPU时间百分比,此列是关注的重点。us的值比较高时,说明用户进程消耗的CPU时间多,如果长期大于50%,就需要考虑优化程序或算法。
sy列显示了内核进程消耗的CPU时间百分比。sy的值较高时,说明内核消耗的CPU资源很多。根据经验,us+sy的参考值为80%,如果大于80%说明可能存在CPU资源不足的情况。
id列显示了CPU处在空闲状态的时间百分比。
wa列显示了I/O等待所占用的CPU时间百分比。wa值越高,说明I/O等待越严重。根据经验,wa的参考值为20%,如果wa值超过20%,说明I/O等待严重。
free
free -h:以人类能看懂的方式查看物理内存
free -m:以MB为单位,查看物理内存
free -g:以GB为单位,查看物理内存
df
格式:df -h (-h:human,表示以人类能看到的方式换算)
iostat
系统慢有两种原因引起的,一个是CPU高,一个是大量IO操作
格式:iostat -xdk 2 3
磁盘块设备分布:
rkB /s:每秒读取数据量kB;
wkB/s:每秒写入数据量kB;
svctm I/O:请求的平均服务时间,单位毫秒
await I/O:请求的平均等待时间,单位毫秒,值越小,性能越好
util:一秒钟有百分几的时间用于I/O操作。接近100%时,表示磁盘带宽跑满,需要优化程序或者增加磁盘;
rkB/s,wkB/s根据系统应用不同会有不同的值,但有规律遵循:长期、超大数据读写,肯定不正常,需要优化程序读取。
svctm的值与await的值很接近,表示几乎没有I/O等待,磁盘性能好,如果await的值远高于svctm的值,则表示I/O队列等待太长,需要优化程序或更换更快磁盘
网络IO:ifstat
CPU占用过高的定位分析思路
-
先用top命令找出CPU占比最高的
-
ps -ef或者jps进一步定位,得知是一个怎么样的一个后台程序作搞屎棍
-
ps -mp 进程id -o THREAD,tid,time
- -m 显示所有的线程
- -p pid进程使用cpu的时间
- -o 该参数后是用户自定义格式
-
将需要的线程ID转换为16进制格式(英文小写格式),命令printf %x 9298将9298转换为十六进制
-
jstack 进程ID | grep tid(16进制线程ID小写英文) -A60
演示:
top
jps
ps -mp 4467 -o THREAD,tid,time
printf %x 9298
jstack 4467 | grep 2452 -A100
定位到大量占用cpu的程序在com.gykjit.spd.system.websocket.DataWebSocketServer的第47行,检查源码发现有同事提交了死循环代码。
内存占用问题排查
启动参数添加,这样如果发生oom报错,就可以通过visualvm分析dump文件
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/tmp/heapdump.hprof
未发生oom也可以手动生成堆快照
jmap -dump:live,format=b,file=./heapdump.hprof 14988
arthas
也可以在服务器上安装阿里arthas
arthas官网
https://arthas.aliyun.com/doc/quick-start.html
下载arthas-boot,并通过java -jar arthas-boot.jar 启动
wget https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar
arthas-boot是Arthas的启动程序,它启动后,会列出所有的Java进程,用户可以选择需要诊断的目标进程。
选择对应进程,例如wm进程输入7 ,再Enter/回车
然后输入dashboard回车
5、字符串常量
String:intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否
则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
public class ConstantsTest {public static void main(String[] args) {String str1 = new StringBuilder("58").append("tongcheng").toString();System.out.println(str1 == str1.intern());System.out.println();String str2 = new StringBuilder("ja").append("va").toString();System.out.println(str2 == str2.intern());System.out.println();String str3 = new StringBuilder("58").append("tongcheng").toString();System.out.println(str3 == str3.intern());}
}
运行结果:
truefalsefalse
通过执行结果可以判断出在这段代码执行前常量池中就有一个Java字符串。
-
类加载器和rt.jar - 根加载器提前部署加载rt.jar
-
OpenJDK8源码
-
http://openjdk.java.net/
-
openjdk8\jdk\src\share\classes\sun\misc
-
-
考查点 - intern()方法,判断true/false?- 《深入理解java虚拟机》书原题是否读过经典JVM书籍
这段代码在JDK 6中运行,会得到两个false,而在JDK 7中运行,会得到一个true和一个false。产生差异的原因是,在JDK 6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串实例的引用,而由StringBuilder创建的字符串对象实例在Java堆上,所以必然不可能是同一个引用,结果将返回false。而JDK 7(以及部分其他虚拟机,例如JRockit)的intern()方法实现就不需要再拷贝字符串的实例到永久代了,既然字符串常量池已经移到Java堆中,那只需要在常量池里记录一下首次出现的实例引用即可,因此intern()返回的引用和由StringBuilder创建的那个字符串实例就是同一个。而对str2比较返回false,这是因为“java”这个字符串在执行StringBuilder.toString()之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern()方法要求“首次遇到"”的原则,“计算机软件"这个字符串则是首次出现的,因此结果返回true。
sun.misc.Version类会在JDK类库的初始化过程中被加载并初始化,而在初始化时它需要对静态常量字段根据指定的常量值(ConstantValue〉做默认初始化,此时
被sun.misc.Version.launcher静态常量字段所引用的"java"字符串字面量就被intern到HotSpot VM的字符串常量池——StringTable里了。
6、LockSupport
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport中的park()和 unpark()的作用分别是阻塞线程和解除阻塞线程。总之,比wait/notify,await/signal更强。3种让线程等待和唤醒的方法方式1:使用Object中的wait()方法让线程等待,使用object中的notify()方法唤醒线程
方式2:使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
传统的synchronized和Lock
Object中的wait和notify方法必须要在同步块或者方法里面且成对出现使用,否则会抛出java.lang.IllegalMonitorStateException。
调用顺序要先wait后notify
Condition的await和signal方法必须要在同步块或者方法里面且成对出现使用,否则会抛出java.lang.IllegalMonitorStateException。
调用顺序要先await后signal
LockSupport
LockSupport是用来创建锁和共他同步类的基本线程阻塞原语。
LockSuport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻寨之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程
LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,
调用一次unpark就加1变成1,
调用一次park会消费permit,也就是将1变成0,同时park立即返回。
如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。