Gc学习笔记:浅谈GC,简略分析CMS,Jvm堆内存结构,JVM性能调优等
标题测试工具
jvisual
jvisual 自从jdk8之后就被移除掉了,我们需要自己去下载
https://visualvm.github.io/
下载之后,GC图是不存在的,需要自己安装
Tools->Plugins->Available Plugins->Visual GC
勾选,然后Install即可
Jvm Heap 结构
上图中,内存一共分为两个大区,堆区域非堆区。
堆区,是随着我们的应用启动而启动的,在程序运行过程中,对象和数组就是存放在这个区的,注意,堆区是一块共享的区域,操作共享区域就会有锁和同步的概念。
跟堆息息相关的还有GC(垃圾回收机制),而堆(Heap)也是GC的主要工作场所
上面这张图显示了GC在堆中的工作过程。
Gc过程
一般刚刚new出来的对象,会存在Eden区,也就是新生代,经过垃圾回收之后,剩下的对象会被放置到存活区(Survivor),也就是上图的Form Space和To Space区。这两个区的功能是一样的,可以看做一个是主,一块是副本,也就是主从,但是关系并不是这样,他们是并行工作的。即交替工作的。
我们知道一个对象再经过多次的垃圾回收之后,还是存活,那么他就会被放置到老年区,也就是Old Generation中,在没有进入老年区之前,这个对象会一直在存活区中,但是我们知道,存活区中有些对象也会被回收,这样,久而久之。存活区就是如下的样子
也就是残缺不全,那么需要整理的过程,这就涉及到了垃圾的回收算法,其中有一种算法就是复制拷贝算法。也就是读取我们当前这个区中有效的对象,将按照顺序存放到另外一个区域。这时候也就需要另外一个备用区。这就为什么有了两个存活区的原因。但是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
效果图
JVM参数
-XX:MaxTenuringThreshold=0
-XX:+PrintGC
-Xms500m
-Xmx500m
-XX:MaxTenuringThreshold=0
最大转为年老代代数,表示一个存在于EdenSpace的数据,在经过多少次的垃圾回收之后,会进入年老区,一般情况下,如果设置的越大,那么这个数据在Survivor区存活的时间也就越长。因为经过垃圾回收之后,存在于Eden区的数据只有两条必然的道路,就是存活区与老年区。
而决定他的去向是,我们怎么知道他是否适合进入老年区?
-XX:MaxTenuringThreshold=16
-XX:+PrintGC
-Xms500m
-Xmx500m
-XX:+UseParallelGC
-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
-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进行查看,可以看到一直闪烁,说明已经发生了死锁。
并且可以看到这一块区域,有一个红色的提示,意思即发现死锁,注意,如果没有这么多的面板,请点击plugins进行安装。图中粉红色哪一块线程就是发生锁住的了。这就是发生在等待锁的时候。并不表示红色的是死锁,而是,等待锁。
在后面,我们可以线程跑到哪里
请看下图的分析,上一个步骤中,勾选了Threads inspect下面的线程,查看我们的线程现在运行到哪里了,在干嘛。
jstack命令
jstack 命令用于把线程的信息打印出来,用法如下:
jstack -l [pid]>stack.log
pid可以通过jps 进行查询
之所以使用>导向符,是为了将输出送入到文件,方便查看。这个文件中的信息是所有线程目前运行到的位置。
也就是我们上面分析那个死锁 的线程信息
效果如下图所示
jmap 命令
常用的命令是,可以使用这个命令导出堆内存。用于分析内存泄漏
常用命令
jmap -histo:[live] pid
live可以加可以不加,pid是线程id,使用jps命令查看就可以了
在jvisual 中同样有工具可以查看
使用Jvisual效果比之前的好看多了。并且是实时的,我们点击Heap Dump就可以将堆进行导出
细心的可以注意到Heap histogram右边有一个Per thread allocations,可以看到每一个不同的线程分配的堆内存大小
这个视图可以说是很方便了。提供了足够的信息给我们查看
使用一个比较明显的例子来查看分析
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运行的时候虚拟机自身也会产生一些字符串,这个很正常的嘛。问题来了,下面的字符串我怎么知道是在哪里的?
深究Jvm 之OQL语言
众所周知,Jvm 中的内存结构设计的非常严谨,堆区与非堆区两个大区。还记得上一步已经对Heap进行dump 了,也就是把内存中的堆区当出来。这么大的堆区,怎么知道哪里内存泄漏?哪里出了问题,以及我们想对堆区进行更加深入细致的查询处理与学习。光靠上一步的还是远远不够的,我们在想,如果我们能用类似于SQL脚本,把堆区中的对象按照一定的查询过滤映射条件进行查询出来,那该有多好啊,OQL应运而生。OQL 全称Objects Query Language,对象查询语言,每一个类全限定名为可以想象成为表名,即java.lang.String是一个表名。那么尝试写一个OQLs