欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等

程序员文章站 2022-03-13 10:33:53
...

标题测试工具

jvisual
jvisual 自从jdk8之后就被移除掉了,我们需要自己去下载
https://visualvm.github.io/
下载之后,GC图是不存在的,需要自己安装
Tools->Plugins->Available Plugins->Visual GC
勾选,然后Install即可

Jvm Heap 结构

Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等

上图中,内存一共分为两个大区,堆区域非堆区。

堆区,是随着我们的应用启动而启动的,在程序运行过程中,对象和数组就是存放在这个区的,注意,堆区是一块共享的区域,操作共享区域就会有锁和同步的概念。

跟堆息息相关的还有GC(垃圾回收机制),而堆(Heap)也是GC的主要工作场所

Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等
上面这张图显示了GC在堆中的工作过程。

Gc过程

一般刚刚new出来的对象,会存在Eden区,也就是新生代,经过垃圾回收之后,剩下的对象会被放置到存活区(Survivor),也就是上图的Form Space和To Space区。这两个区的功能是一样的,可以看做一个是主,一块是副本,也就是主从,但是关系并不是这样,他们是并行工作的。即交替工作的。
我们知道一个对象再经过多次的垃圾回收之后,还是存活,那么他就会被放置到老年区,也就是Old Generation中,在没有进入老年区之前,这个对象会一直在存活区中,但是我们知道,存活区中有些对象也会被回收,这样,久而久之。存活区就是如下的样子
Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等
也就是残缺不全,那么需要整理的过程,这就涉及到了垃圾的回收算法,其中有一种算法就是复制拷贝算法。也就是读取我们当前这个区中有效的对象,将按照顺序存放到另外一个区域。这时候也就需要另外一个备用区。这就为什么有了两个存活区的原因。但是GC两个存活区不一定会交替工作,在某些时候,只是单个工作,这个主要取决于GC算法
###GC策略与GC算法
请参考如下写的比较详细的文章,本文只是为了检验效果。
https://www.cnblogs.com/sunfie/p/5125283.html

Code

下面的代码是一个循环的往一个List列表中放入随机数的代码,可以看到这段代码会一直放变量,同事其产生的对象是没有办法被回收,通过下面的图,让我们了解GC机制

public static void main(String[] args) throws InterruptedException {
		var list = new ArrayList<String>();
		while (true) {
			list.add(Math.random() + "");
			System.out.println(list.size());
		}
	}
	

使用默认垃圾回收-XX:+UseG1GC

效果图

Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等

JVM参数

-XX:MaxTenuringThreshold=0
-XX:+PrintGC
-Xms500m
-Xmx500m

-XX:MaxTenuringThreshold=0

最大转为年老代代数,表示一个存在于EdenSpace的数据,在经过多少次的垃圾回收之后,会进入年老区,一般情况下,如果设置的越大,那么这个数据在Survivor区存活的时间也就越长。因为经过垃圾回收之后,存在于Eden区的数据只有两条必然的道路,就是存活区与老年区。
而决定他的去向是,我们怎么知道他是否适合进入老年区?
Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等

-XX:MaxTenuringThreshold=16
-XX:+PrintGC
-Xms500m
-Xmx500m
-XX:+UseParallelGC

Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等

-XX:MaxTenuringThreshold=16
-XX:+PrintGC
-XX:PermSize=32m
-XX:SurvivorRatio=1
-Xms500m
-Xmx500m
-Xmn200m
-XX:+UseParallelGC

上图中,我们把最大堆内存跟初始堆内存设置为500m,
并且设置xmn 即年轻代堆(YoungGenerator)内存为200m
并且Eden:Survivor=1:1
由SurvivorRatio,我们知道

Eden+Survivor*2=Young Generator
假设Eden为x ,Survivor为y
那么x:y=1
所以根据上面的公式
x+2y=200m
即3y=200m
可得x=y=66m
并且OldGeneration=StackSize-YoungGenerator
500-200=300m
如下图展示的结果
注意需要断点情况下看,随着程序的运行JVM会适当的调整各个区块的大小

Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等

打印GC

-Xloggc is deprecated. Will useinstead.
-XX:+PrintGC
-XX:+PrintGCDetails

-Xlog:gc:gc.log
-Xlog:gc
-Xlog:gc*

Java 死锁实现

public class Runner implements Runnable {

	int a, b;

	public Runner(int a, int b) {
		this.a = a;
		this.b = b;
	}

	public static void main(String[] args) {
		for (int i = 0; i < 4; i++) {
			var t1=new Thread(new Runner(10, 8));
			t1.setName("T"+i+"A");
			var t2=new Thread(new Runner(8, 10));
			t2.setName("T"+i+"B");
			t1.start();
			t2.start();
		}
	}

	@Override
	public void run() {
		// 获取锁
		String name = Thread.currentThread().getName();
		System.out.println(name+" Enter");
		synchronized (Integer.valueOf(a)) {
			System.out.println(name+" got lock a:"+a);
			synchronized (Integer.valueOf(b)) {
				System.out.println(name+" got lock b:"+b);
				System.out.println(a + "==>" + b);
			}
		}
		System.out.println(name+" Leave");
	}
}

我们的代码每次是开启两个线程,线程A,与线程B,并且按照批次进行划分
所以线程的编号是
T0A T0B
T1A T1B
T2A T2B
…以此类推,但是线程的执行顺序是随机的,但是有一点可以知道,A线程之间是不会相互死锁的,发生死锁都是在A线程拿了锁1 的时候,B线程拿了锁2,而此时A线程需要拿锁2,而B线程要拿锁1,这就死锁的

T0A Enter
T0A got lock a:10
T0A got lock b:8					//T0A拿了并且释放了
10==>8
T0A Leave
T0B Enter
T1B Enter
T3B Enter
T3A Enter
T3A got lock a:10				//A类线程拿了锁2(数字10)
T2A Enter
T1A Enter
T2B Enter
T0B got lock a:8					//B类线程拿了锁1(数字8)
//此时已经发生死锁,因为AB类线程互相等待对方的锁,

我们使用Jvisual进行查看,可以看到一直闪烁,说明已经发生了死锁。
Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等

并且可以看到这一块区域,有一个红色的提示,意思即发现死锁,注意,如果没有这么多的面板,请点击plugins进行安装。图中粉红色哪一块线程就是发生锁住的了。这就是发生在等待锁的时候。并不表示红色的是死锁,而是,等待锁。

Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等

在后面,我们可以线程跑到哪里
Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等
请看下图的分析,上一个步骤中,勾选了Threads inspect下面的线程,查看我们的线程现在运行到哪里了,在干嘛。
Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等

jstack命令

jstack 命令用于把线程的信息打印出来,用法如下:

jstack -l [pid]>stack.log
pid可以通过jps 进行查询

之所以使用>导向符,是为了将输出送入到文件,方便查看。这个文件中的信息是所有线程目前运行到的位置。
也就是我们上面分析那个死锁 的线程信息
Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等
效果如下图所示
Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等

jmap 命令

常用的命令是,可以使用这个命令导出堆内存。用于分析内存泄漏

常用命令
jmap -histo:[live] pid
live可以加可以不加,pid是线程id,使用jps命令查看就可以了

Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等

在jvisual 中同样有工具可以查看
使用Jvisual效果比之前的好看多了。并且是实时的,我们点击Heap Dump就可以将堆进行导出
Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等

细心的可以注意到Heap histogram右边有一个Per thread allocations,可以看到每一个不同的线程分配的堆内存大小
Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等

这个视图可以说是很方便了。提供了足够的信息给我们查看

使用一个比较明显的例子来查看分析

public class OutOfMemory {

	public static void main(String[] args) throws IOException {
		var list = new ArrayList<String>();
		for (int i = 0; i < 1000000; i++) {
			list.add(UUID.randomUUID().toString());
		}
		BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
		String line = bufr.readLine();
		System.out.println(line);
	}
}

这个程序是 一个随机产生UUID,并且将这个UUID转成字符串放到List列表中,懂GC的同学应该知道这个是不会被回收的,因为存在着引用。我们把堆内存dump出来,按照前面的方法,很厉害吧,应用里面的String 类字符串实例占用空间居然达到28%,这个一定是内存泄漏啦。再看看实例数,不对,我们就循环了1000000次,怎么会多11935个,看下面的count,其实java运行的时候虚拟机自身也会产生一些字符串,这个很正常的嘛。问题来了,下面的字符串我怎么知道是在哪里的?
Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等

深究Jvm 之OQL语言

众所周知,Jvm 中的内存结构设计的非常严谨,堆区与非堆区两个大区。还记得上一步已经对Heap进行dump 了,也就是把内存中的堆区当出来。这么大的堆区,怎么知道哪里内存泄漏?哪里出了问题,以及我们想对堆区进行更加深入细致的查询处理与学习。光靠上一步的还是远远不够的,我们在想,如果我们能用类似于SQL脚本,把堆区中的对象按照一定的查询过滤映射条件进行查询出来,那该有多好啊,OQL应运而生。OQL 全称Objects Query Language,对象查询语言,每一个类全限定名为可以想象成为表名,即java.lang.String是一个表名。那么尝试写一个OQL
s