JVM+GC解析

1、JVM内存结构

2、GC的作用域

3、常见的垃圾回收算法

引用计数

  • 有对象引用加个1,没对象引用减个1,到0为止
  • 缺点:每次对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗;较难处理循环引用
  • JVM的实现一般不采用这种方式

复制
MinorGC的过程(复制——>清空——>互换)
Java堆从GC的角度还可以细分为:新生代(Eden区、From Survivor区和 To Survivor区)和老年代
1:eden、SurvivorFrom复制到SurvivorTo,年龄+1
首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和FROM区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1
2:清空eden、SurvivorFrom
然后,清空Eden和SurvivorFrom中的对象,也即复制之后有交换,谁空谁是to
3:SurvivorTo和SurvivorFrom互换
最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区。部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入老年代
标记清除
算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象(产生内存碎片)
标记整理
原理:
1、标记:与标记清除一样
2、压缩:再次扫描,并往一端滑动存活对象(没有内存碎片,可以利用bump,需要移动对象的成本)

4、JVM垃圾回收的时候如何确定垃圾?是否知道什么是GC Roots?

什么是垃圾?简单来说就是内存中已经不再被使用到的空间就是垃圾
要进行垃圾回收,如何判断一个对象是否可以被回收?

  • 引用计数法
    Java中,引用和对象是有关联的,如果要操作对象则必须用引用进行
    因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,给对象中添加一个引用计数器,每当有一个地方引用它,计数器值加1,每当有一个引用失效时,计数器值减1
    任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象
    那为什么主流的Java虚拟机里面都没有选用这种算法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题
  • 枚举根节点做可达性分析(根搜索路径)
    为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法
    所谓“GC roots”或者说tracing GC的“根集合”就是一组必须活跃的引用
    基本思路就是通过一系列名为“GC Roots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活;没有被遍历到的就自然被判定为死亡

Java中可以作为GC Roots的对象
虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象
方法区中的类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(Native方法)引用的对象

弹幕:回收堆,指向堆的其他区域的对象就是GC Roots

5、你说你做过JVM调优和参数配置,请问如何盘点查看JVM系统默认值?

JVM的参数类型
标配参数

  • -version
  • -help
  • java -showvision
    在jdk各个版本之间稳定,很少有大的变化
    X参数(了解)
  • -Xint 解释执行
  • -Xcomp 第一次使用就编译成本地代码
  • -Xmixed 混合模式(先编译后执行)
    XX参数
    Boolean类型
  • 公式:-XX:+或者-某个属性值,+表示开启,-表示关闭
  • case:是否打印GC收集细节:-XX:-PrintGCDetails/-XX:+PrintGCDetails
  • 是否使用串行垃圾回收器:-XX:-UserSerialGC/-XX:+UserSerialGC
    KV设值类型
  • 公式:-XX:属性key=属性值value
  • Case:-XX:MetaspaceSize=128m
  • -XX:MaxTenuringThreshold=15
    jinfo举例,如何查看当前运行程序的配置
  • 公式 jinfo -flag配置项 进程编号

如何查看一个正在运行中的java程序,它的某个jvm参数是否开启?具体值是多少?
jps
jinfo
jinfo -flag PrintGCDetails 13632是否开启了打印GC细节的参数

两个经典参数:-Xms和-Xmx
-Xms等价于-Xms:InitialHeapSize
-Xmx等价于-XX:MaxHeapSize

第一种,查看参数盘点家底
jps
jinfo -flag 具体参数 java进程编号
jinfo -flags java进程编号
第二种,查看参数盘点家底
-XX:+PrintFlagsInitial
主要查看初始默认
公式java -XX:+PrintFlagsInitial -version
java -XX:+PrintFlagsInitial
-XX:+PrintFlagsFinal
主要查看修改更新
:人为修改了参数
PrintFlagsFinal举例,运行java命令的同时打印出参数
D:\44>java -XX:+PrintFlagsFinal -Xss128k TT为运行的java类名字
-XX:+PrintCommandLineFlags

6、你平时工作用过的JVM常用基本配置参数有哪些?

在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似
元空间(Java8)与永久代(Java7)之间最大的区别在于:永久代使用的JVM的堆内存,但是Java8以后的元空间并不在虚拟机中而是使用本机物理内存
因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制
Runtime.getRuntime().totalMemory();返回Java虚拟机中的内存总量(堆内存大小)
Runtime.getRuntime().maxMemoey();返回Java虚拟机试图使用的最大内存量(堆内存大小)
常用参数
-Xms:初始大小内存,默认为物理内存1/64,等价于-XX:InitialHeapSize
-Xmx:最大分配内存,默认为物理内存1/4,等价于-XX:MaxHeapSize
-Xss:设置单个线程栈的大小,一般默认为512k~1024k,等价于-XX:ThreadStackSize
-Xmn:设置年轻代大小
-XX:MetaspaceSize:设置元空间大小:元空间的本质和永久代类似,都是对JVM规范中方法区的实现,不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存,因此,默认情况下,元空间的大小仅受本地内存限制
-Xms10m -Xmx10m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFinal

-XX:+UseSerialGC串行垃圾回收器
-XX:+UseParallelGC并行垃圾回收器

典型设置案例

-Xms128m -Xmx4096m -Xss1024k -XX:MetaspaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC

-XX:+PrintGCDetails输出详细GC收集日志信息
GC:
FullGC:
规律:名称:GC前内存占用->GC后内存占用(该区内存总大小)

新生代(1/3堆空间)(Eden(8/10)From(1/10)To(1/10))
老年代(2/3堆空间)
-XX:SurviorRatio设置新生代中eden和S0/S1空间的比例,默认-XX:SurvivorRatio=8,Eden:S0:S1=8:1:1
假如-XX:SurvivorRatio=4,Eden:S0:S1=4:1:1,SurvivorRatio值就是设置eden区的比例占多少,S0/S1相同

-XX:NewRatio:配置年轻代与老年代在堆结构的占比,默认-XX:NewRatio=2新生代占1,老年代占2,年轻代占整个堆的1/3
假如-XX:NewRatio=4新生代占1,老年代占4,年轻代占整个堆的1/5
NewRatio值就是设置老年代的占比,剩下的1给新生代

-XX:MaxTenuringThreshold设置垃圾最大年龄
查看默认进入老年代年龄:jinfo - flag MaxTenuringThreshold 1734417344为进程编号
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入进入老年代。对于老年代比较多的应用,可用提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代即被回收的概率

7、强引用、软引用、弱引用、虚引用分别是什么?

强引用
当内存不足时,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收
强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,一般认为就是可以被垃圾收集的了(当然具体回收还是要看垃圾收集策略)
软引用
软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集
对于只有软引用的对象来说,当系统内存充足时它不会被回收,当系统内存不足时,它会被回收
软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!

public class SoftReferenceDemo {
	public static void softRef_Memory_Enough() {
		Object o1 = new Object();
		SoftReference<Object> softReference = new SoftReference<>(o1);
		System.out.println(o1);
		System.out.println(softReference.get());

		o1 = null;
		System.gc();
		
		System.out.println(o1);
		System.out.println(softReference.get());
	}

	public static void softRef_Memory_NotEnough() {
	Object o1 = new Object();
		SoftReference<Object> softReference = new SoftReference<>(o1);
		System.out.println(o1);
		System.out.println(softReference.get());

		o1 = null;

		try {
			byte[] bytes = new byte[30 * 1024 * 1024];
		} catch (Throwable e) {
			e.printStackTrace();
		} finally {
			System.out.println(o1);
			System.out.println(softReference.get());
		}
	}
	
	public static void main(String[] args) {
	}
}

弱引用
弱引用需要用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());
	}
}

软引用和弱引用的适用场景
假如有一个应用需要读取大量的本地图片:

  • 如果每次读取图片都从硬盘读取则会严重影响性能
  • 如果一次性全部加载到内存中又可能造成内存溢出
    此时使用软引用可以解决这个问题:(使用路径来建立软引用)
    设计思路时:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题
    Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

你知道弱引用的话,能谈谈WeekHashMap吗?
key被置空,value也会被回收

public class WeakHashMapDemo { 
	public static void main(String[] args) {
		myHashMap();
		myWeakHashMap();
	}
	
	private static void myHashMap() {
		HashMap<Integer, String> map = new HashsMap<>();
		Integer key = new Integer(1);
		String value = "HashMap";

		map.put(key, value);
		System.out.println(map);		//{1=HashMap}

		key = null;
		System.out.println(map);		//{1=HashMap}

		System.gc();//不会被回收
	}

	private static void myWeakHashMap() {
		WeakHashMap<Integer, String> map = new WeakHashsMap<>();
		Integer key = new Integer(1);
		String value = "WeakHashMap";

		map.put(key, value);
		System.out.println(map);		//{2=WeakHashMap}

		key = null;
		System.out.println(map);		//{2=WeakHashMap}

		System.gc();
		System.out.println(map+"\t"+map.size());//{} 0
	}
}

虚引用
虚引用需要java.lang.ref.PhantomReference类来实现
顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用
虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。
PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作
换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。
Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作

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());//null

		o1 = null;
		System.gc();
		Thread.sleep(500);
		
		System.out.println(o1);//null
		System.out.println(weakReference.get());//null
		System.out.println(referenceQueue.poll());	//java.lang.ref.WeakReference@511d50c0
	}
}

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());//null
		System.out.println(referenceQueue.poll());//null

		o1 = null;
		System.gc();
		Thread.sleep(500);
		
		System.out.println(o1);//null
		System.out.println(phantomReference.get());//null
		System.out.println(referenceQueue.poll());	//java.lang.ref.PhantomReference@511d50c0
	}
}

类似 AOP的后置通知

java提供了4种引用类型,在垃圾回收的时候,各自有各自的特点,ReferenceQueue是用来配合引用工作的,没有ReferenceQueue一样可以运行。
创建引用的时候可以指定关联的队列,当GC释放对象内存的时候,会将引用加入到引用队列
如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动
这相当于一种通知机制
当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收,通过这种方式,JVM允许我们在对象被销毁后,做一些我们自己想做的事情

弹幕:虚引用主要是为了管理堆外内存,防止内存泄漏

8、请谈谈你对OOM的认识

java.lang.StackOverflowError

public static void main(String[] args) {
	stackOverfloeError();
}

private static void stackOverflowError() {
	stackOverflowError();//Exception in thread "main" java.lang.StackOverflowError
}

java.lang.OutOfMemoryError:Java heap space

public class JavaHeapSpaceDemo {
	public static void main(String[] args) {
		byte[] bytes = new byte[80 * 1024 * 1024];//80MB
		//Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	}
}

java.lang.OutOfMemoryError:GC overhead limit exceeded
GC回收时间过长时会抛出OutOfMemoryError。过长的定义是,超过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<>();
		try {
			while (true) {
				list.add(String.valueOf(++i).intern());//不停的new对象
			}
		} catch (Throwable e) {
			System.out.println("*******i:" + i);
			e.printStackTrace();
			throw e;
		}
	}
}

java.lang.OutOfMemoryError:Direct buffer memory
配置参数:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
故障现象:
Exception in thread “main” java.lang.OutOfMemoryError: Direct buffer memory
导致原因:
写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作(引用在JVM中,实例对象不在JVM中)
这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据
ByteBuffer.allocate(capability)第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢
ByteBuffer.allocateDirect(capability)第一种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快
但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象们就不会被回收
这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序就直接崩溃了

public class DirectBufferMemoryDemo {
	public static void main(String[] args) {
		System.out.println("配置的maxDirectMemory:" + (sun.misc.VM.maxDirectMemory() / (double)1024 / 1024 + "MB");
		try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
		//-XX:MaxDirectMemorySize=5m 我们配置为5MB,但实际使用6MB,故意使坏
		ByteBuffer bb = ByteBuffer.allocateDirect(6 * 1024 * 1024);
	}
}

java.lang.OutOfMemoryError:unable to create new native thread
高并发请求服务器时,经常出现如下异常:java.lang.OutOfMemoryError: unable to create new native thread
准确的讲该native thread异常与对应的平台有关
导致原因:
1、你的应用创建了太多线程了,一个应用进程创建多个线程,超过系统承载极限
2、你的服务器并不允许你的应用创建这么多线程。linux系统默认允许单个进程可以创建的线程数是1024个,你的应用创建超过这个数量,就会报java.lang,OutOfMemoryError: unable to create new native thread
解决办法:1、想办法降低你应用程序创建线程的数量,分析应用是否真的需要这么多线程,如果不是,改代码将线程数降到最低
2、对于有的应用,确实需要创建很多线程,远超过linux系统默认1024个线程的限制,可以通过修改linux服务器配置,扩大linux默认配置

public class UnableCreateNewThreadDemo {
	public static void main(String[] args) {
		for (int i = 1;  ; i++) {
			System.out.println("******* i = " + i);
			new Thread(() -> {
				try { Thread.sleep(Integer.MAX_VALUE); } catch (InterruptedException e ) { e.printStackTrace(); }
			}, ""+i).start();
		}
	}
}

解决办法:vim /etc/security/limits.d/90-nproc.conf
打开后发现除了root,其他账户都限制在1024个
如果希望某个用户生成的线程多一些,可以进行配置z3 soft nproc 20000
java.lang.OutOfMemoryError:Metaspace
Metaspace VM Metadata in native memory
元空间是方法区,装类的信息、元素(类模板),常量池
使用java -XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:Metaspacesize为21810376B(大约为20.8M)
Java8及以后的版本使用Metaspace来替代永久代
Metaspace是方法区在Hotspot中的实现,它与持久代最大的区别在于:Metaspace并不在虚拟机内存中而是使用本地内存也即在java8中,class metadata(the virtual machines internal presentation of Java class),被存储在叫做Metaspace的native memory
永久代(java8后被元空间Metaspace取代了)存放了以下信息:

  • 虚拟机加载的类信息
  • 常量池
  • 静态变量
  • 即时编译后的代码

模拟Metaspace空间溢出,我们不断生成类往元空间灌,类占据的空间总是会超过Metaspace指定的空间大小的

public class MetaspaceOOMTest {
	static class OOMTest { }
	public static void main(String[] args) {
		int i = 0; //模拟计数多少次以后发生异常
		try {
			while (true) {
				i++;
				Enhancer enhancer = new Enhancer();
				enhancer.setSuperclass(OOMTest.class);
				enhancer.setUseCache(false);
				enhancer.setCallback(new MethodInterceptor() {
					@Override
					public Object intercept(Onject o, Method method, Object[] objects, MethodProxy) throws Throwable {
						return methodProxy.invokeSuper(o, args);
					}
				});
				enhancer.create();
			}
		}
		catch (Throwable e) {
			System.out.println("********多少次后发生了异常:"+ i);
			e.printStackTrace();
		}
	}
}

9、GC垃圾回收算法和垃圾收集器的关系?分别是什么请你谈谈

GC算法(引用计数/复制/标清/标整)是内存回收的方法论,垃圾收集器就是算法落地实现
因为目前为止还没有完美的收集器出现,更加没有万能的收集器,只是针对具体应用最合适的收集器,进行分代收集(复制新生,标清标整用在老年代)
4种主要垃圾收集器
Serial、Parallel、CMS、G1
串行垃圾回收器(Serial)
它为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程,所以不适合服务器环境
并行垃圾回收器(Parallel)
多个垃圾收集线程并行工作,此时用户线程是暂停的,适合于科学计算/大数据处理平台处理等弱交互场景
并发垃圾回收器(CMS)
用户线程和垃圾收集线程同时执行(不一定是并行,可能是交替执行),不需要停顿用户线程
互联网公司多用它,适用对响应时间有要求的场景
串行(Serial)vs 并行(Parallel)
在单核CPU下并行可能更慢
STW(Stop-the-world)vs并发(Concurrent)
STW:暂停整个应用,时间可能会更长
并发(Concurrent):更为复杂,GC可能会抢占应用的CPU

G1垃圾回收器G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收

10、怎么查看服务器默认的垃圾收集器是哪个?生产上如何配置垃圾收集器的?谈谈你对垃圾收集器的理解?

怎么查看默认的垃圾收集器是哪个?
JVM参数:java -XX:+PrintCommandLineFlags -version
Java8默认是ParallelGC
默认的垃圾回收器有哪些?
java的gc回收的类型主要有几种:
UseSerialGC,UseParallelGC,UseConcMarkSweepGC,UseParNewGC,UseParallelOldGC,UseG1GC

垃圾收集器
G1横跨两界

Young GenSerial CopyingParallel Scavenge
Old GenSerial MSC(Serial Old)Parallel Compacting(Parallel Old)

年轻代用复制算法,老年代用标记清除或标记整理

部分参数说明:
DefNew:Default New Generation
Tenured:Old
ParNew:Parallel New Generation
PSYoungGen:Parallel Scavenge
ParOldGen:Parallel Old Generation

Server/Client模式分别是什么意思?
1、适用范围:只需要掌握Server模式即可,Client模式基本不会用
2、操作系统:
2.1 32位Windows操作系统,不论硬件如何都默认使用Client的JVM模式
2.2 32位其它操作系统,2G内存同时有2个cpu以上用Server模式,低于该配置还是Client模式
2.3 64位only server模式

新生代
串行GC(Serial)/(Serial Copying)
串行收集器:Serial收集器
一句话:一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它收集结束
串行收集器是最古老,最稳定以及效率高的收集器,只使用一个线程去回收但其在进行垃圾收集过程中可能会产生较长的停顿(“Stop-The-World"状态)。虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器
对应JVM参数是:-XX:+UserSerialGC
开启后会使用:Serial(Young区用)+Serial Old(Old区用)的收集器组合
表示:新生代、老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法
-Xms 10m -Xmx 10m -XX:+PrintGCDetails -XX:+UseSerialGC
并行GC(ParNew)
ParNew(并行)收集器
一句话:使用多线程进行垃圾回收,在垃圾收集时,会Stop-the-World暂停其他所有的工作线程直到它收集结束
ParNew收集器其实就是Serial收集器新生代的并行多线程版本,最常见的应用场景是配合老年代的CMS GC工作,其余的行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。它是很多java虚拟机运行在Server模式下新生代的默认垃圾收集器
常用对应JVM参数:-XX:+UseParNewGC 启用ParNew收集器,只影响新生代的收集,不影响老年代
开启上述参数后,会使用:ParNew(Young区用)+Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法
但是,ParNew+Tenured这样的搭配,java8已不再被推荐
备注:-XX:ParallelGCThreads限制线程数量,默认开启和CPU数目相同的线程数
并行回收GC(Parallel)/(Parallel Scavenge)
Parallel Scavenge收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。一句话:串行收集器在新生代和老年代的并行化
它关注的重点是:可控制的吞吐量(Thoughput=运行用户代码时间/(运行用户代码时间+垃圾收集时间),也即比如程序运行100分钟,垃圾收集时间1分钟,吞吐量就是99%。高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务
自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。(自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMills)或最大的吞吐量
常用JVM参数:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可相互激活)使用Parallel Scanvenge收集器
开启该参数后:新生代使用复制算法,老年代使用标记-整理算法

一条线上的蚂蚱

if (UseSerialGC)	i++;
if (UseConMarkSweepGC || UseParNewGC) i++;
if (UseParallelGC || useParallelOldGC) i++;
if (UseG1GC) 

-XX:ParallelGCThreads=数字N 表示启动多少个GC线程
cpu>8 N=5/8
cpu<8 N=实际个数

并行GC(Parallel Old)/(Parallel MSC)
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

并发标记清除GC(CMS)
CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。
适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。
CMS非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器
Concurrent Mark Sweep并发标记清除,并发收集低停顿,并发指的是与用户线程一起执行
开启该收集器的JVM参数:-XX:+UseConcMarkSweepGC 开启该参数后会自动将-XX:+UseParNewGC打开
开启该参数后,使用ParaNew(Young区用)+CMS(Old区用)+Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器

-Xms 10m -Xmx 10m -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC

4步过程
初始标记(CMS initial mark):只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程
并发标记(CMS concurrent mark)和用户线程一起:进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象
重新标记(CMS remark):为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程;由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
并发清除(CMS concurrent sweep)和用户线程一起:清除GC不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象;由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行
优点:并发收集低停顿
缺点:并发执行,对CPU资源压力大:由于并发进行,CMS在收集与应用线程会同时会增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间
采用的标记清除算法会导致大量碎片:标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数-XX:CMSFullGCsBeForeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC

串行GC( Serial Old)/(Serial MSC)
Serial Old是Serial垃圾收集器老年代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在Client默认的java虚拟机默认的年老代垃圾收集器
在Server模式下,主要有两个用途(了解,版本已经到8及以后)
1、在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用(Parallel Scavenge + Serial Old)
2、作为老年代版中使用CMS收集器的后备垃圾收集方案

(理论知道即可,实际中java8已经被优化掉了,没有了)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOlsGC

如何选择合适的垃圾收集器
组合的选择

  • 单CPU或小内存,单机程序-XX:+UseSerialGC
  • 多CPU,需要最大吞吐量,如后台计算型应用-XX:+UseParallelGC或者-XX:+UseParallelOldGC
  • 多CPU,追求低停顿时间,需快速响应如互联网应用-XX:+UseConcMarkSweepGC-XX:+ParNewGC
参数新生代垃圾收集器新生代算法老年代垃圾收集器老年代算法
-XX:+UseSerialGCSerialGC复制SerialOldGC标整
-XX:+UseParNewGCParNew复制SerialOldGC标整
-XX:+UseParallelGC/-XX:+UseParallelOldGCParallel[Scavenge]复制ParallelOld标整
-XX:+UseConcMarkSweepGCParNew复制CMS+Serial Old的收集器组合(Serial Old作为CMS出错的后备收集器)标清
-XX:+UseG1GCG1整体上采用标记-整理算法局部是通过复制算法,不会产生内存碎片

11、G1垃圾收集器

开启G1垃圾收集器
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC
以前收集器特点:
年轻代和年老代是各自独立且连续的内存块;
年轻代收集使用单eden+S0+S1进行复制算法;
老年代收集必须扫描整个老年代区域;
都是以尽可能少而快速地执行GC为设计原则

G1是什么?
G1(Garbage-First)收集器,是一款面向服务端应用的收集器
从官网的描述中,我们知道G1是一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。另外,它还具有以下特性:
像CMS收集器一样,能与应用程序线程并发执行
整理空闲空间更快
需要更多的时间来预测GC停顿时间
不希望牺牲大量的吞吐性能
不需要更大的Java Heap

G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现得更出色:
G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片
G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间

CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还是存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器-G1垃圾收集器
G1是在2012年才在jdk1.7是在2012年才在jdk1.7u4中可用。oracle官方计划在jdk9中将G1变成默认的垃圾收集器以替代CMS。它是一款面向服务端应用的收集器,主要应用在多CPU和大内存服务器环境下,极大的减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换java8以前的CMS收集器
主要改变是Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region,每个region从1M到32M不等。一个region有可能属于Eden,Survivor或者Tenured内存区域

特点:
1、G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW
2、G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片
3、宏观上看G1之中不再区分年轻代和老年代。把内存划分成多个独立的子区域(Region),可以近似理解为一个围棋的棋盘
4、G1收集器里面讲整个的内存区都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但它们不再是物理隔离的,而是一部分Region的集合且不需要Region是连续的,也就是说依然会采用不同的GC方式来处理不同的区域。
5、G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换

底层原理
Region区域化垃圾收集器:最大好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可

  • 区域化内存划分Region,整体编为了一些不连续的内存区域,避免了全内存区的GC操作
  • 核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小,在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数:-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
  • 大小范围在1MB~32MB,最多能设置2048个区域,也即能支持的最大内存为:32MB * 2048 = 65536MB = 64G内存

G1算法将堆划分为若干个区域(Region):Eden、Survior、Old、Humongous,它仍然属于分代收集器,这些Region的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间,这些Region的一部分包含老年代,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了
在G1中,还有一种特殊的区域,即Humongous(巨大的)区域。如果一个对象占用的空间超过了分区容量的50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时不得不启动Full GC。

回收步骤:
G1收集器下的Young GC
针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集+形成连续的内存块,避免内存碎片

  • Eden区的数据移动到Survivor区,假如出现Survivor区空间不够,Eden区数据会部分晋升到Old区
  • Survivor区的数据移动到新的Survivor区,部分数据会晋升到Old区
  • 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行

4步过程:
初始标记:只标记GC Roots能直接关联到的对象
并发标记:进行GC Roots Tracing的过程
最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
筛选回收:根据时间来进行价值最大化的回收(弹幕:根据每个内存块的回收价值来判断优先回收的内存块)

常用配置参数:
-XX:+UseG1GC
-XX:G1HeapRegionSize=n:设置G1区域的大小,值是2的幂范围是1MB到32MB.目标是根据最小的Java堆大小划分出约2048个区域
-XX:MaxGCPauseMillis=n:最大GC停顿时间,单位毫秒,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间
-XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的时候就触发GC,默认为45
-XX:ConcGCThreads=n:并发GC使用的线程数
-XX:G1ReservePercent=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%

开发人员仅仅需要声明以下参数即可:
三步归纳:开始G1+设置最大内存+设置最大停顿时间
-XX:+UseG1GC
-Xmx32g
-XX:MaxGCPauseMillis-100

和CMS相比的优势:
1)G1不会产生内存碎片
2)是可以精确控制停顿。该收集器是把整个堆(新生代、老年代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域

JVMGC+SpringBoot微服务的生产部署和调参优化
1、IDEA开发完微服务工程
2、maven进行clean package
3、要求微服务启动的时候,同时配置我们的JVM/GC的调优参数
3.1 内
3.2 外====》重点
在有包的路径下,运行jar命令,公式如下:
java -server jvm各种参数 -jar 第一步上面的jar/war包名字

12、生产环境服务器变慢,诊断思路和性能评估谈谈?

整机:top
uptime,系统性能命令的精简版

CPU:vmstat
查看CPU(包含不限于)
查看所有cpu核信息mpstat -P ALL 2
每个进程使用cpu的用量分解信息pidstat -u 1 -p 进程编号

vmstat -n 2 3
一般vmstat工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔数单位是秒,第二个参数是采样的次数
-procs
r:运行和等待CPU时间片的进程数,原则上1核的CPU的运行队列不要超过2,整个系统的运行队列不能超过总核数的2倍,否则代表系统压力过大
b:等待资源的进程数,比如正在等待磁盘I/O、网络I/O等
-cpu
us:用户进程消耗CPU时间百分比,us值高,用户进程消耗CPU时间多,如果长期大于50%,优化程序
sy:内核进程消耗的CPU时间百分比
内存:free
应用程序可用内存数
free
free -g
free -m
经验值

  • 应用程序可用内存/系统物理内存>70%内存充足
  • 应用程序可用内存/系统物理内存<20%内存不足,需要增加内存
  • 20%<应用程序可用内存/系统物理内存<70%内存基本够用

pidstat -p 进程号 -r 采样间隔秒数
硬盘:df
查看磁盘剩余空间数
df -h
磁盘IO:isostat
磁盘I/O性能评估
isostat -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队列等待太长,需要优化程序或更快磁盘

pidstat -p 进程号 -r 采样间隔秒数也可以查看
网络:ifstat
默认本地没有,下载ifstat
查看网络IO
各个网卡的in、out
观察网络负载情况
程序网络读写是否正常
-程序网络I/O优化
-增加网络I/O带宽

13、假如生产环境出现CPU占用过高,请谈谈你的分析思路和定位

1、先用top命令找出CPU占比最高的
2、ps -ef或者jps进一步定位,得知是一个怎么样的一个后台程序给我们惹事
3、定位到具体线程或者代码
ps -mp 进程 -o THREAD,tid,time
-m显示所有的线程
-p pid 进程使用cpu的时间
-o 该参数后是用户自定义格式
4、将需要的线程ID转换为16进制格式(英文小写格式)
`printf "%x\n"有问题的线程ID
5、jstack进程ID|grep tid(16进制线程ID小写英文)-A60

14、对于JDK自带的JVM监控和性能分析工具用过哪些?一般你是怎么用的?

15、Github

常用词含义
watch:会持续收到该项目的动态
fork:复制某个项目到自己的Github仓库中
star:可以理解为点赞
clone:将项目下载至本地
follow:关注你感兴趣的作者,会收到他们的动态

in关键词限制搜索范围
公式:xxx关键词in:name或description或readme
xxx in:name项目名包含xxx的seckill in:name
xxx in:description项目描述包含xxx的
xxx in:readme项目的readme文件中包含xxx的
组合使用:搜索项目名或者readme中包含秒杀的项目seckill in:name,readme

stars或fork数量关键词去查找
公式:xxx关键词 stars通配符 :>或者:>=
区间范围数字:数字1…数字2
查找stars数大于等于5000的springboot项目springboot stars:>=5000
查找forks数大于500的springcloud项目springcloud forks:>5000
组合使用:查找fork在100到200之间并且stars数在80到100之间的springboot项目springboot forks:100..200 stars:80..100

awesome加强搜索
公式:awesome关键字 awesome系列:一般是用来收集学习、工具书籍类相关的项目
搜索优秀的redis相关的项目,包括框架、教程等awesome redis
高亮显示某一行代码
1行:地址后面紧跟#L数字
地址后面#L13
多行:地址后面紧跟#L数字1-L数字2
地址后面L13-L23

项目内搜索
英文t
http://help.github.com/en/articles/using-keyboard-shortcuts
搜索某个地区内的大佬
公式:location:地区
language:语言

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值