TLAB、OOM、调优工具(实现原理)、调优实战

一、TLAB

新生代堆区独享的堆区。

因为并发情况下分配内存,会存在性能问题。所以为了解决这个问题,设计了一个在线程级别分配对象的功能,就是TLAB.

New对象与指针碰撞

new对象怎么就出问题了呢?
java中我们要创建一个对象,用关键字new就可以了。但是,在我们日常中,有很多生命周期很短的对象。比如:

public void dome(){
    User user=new user();
    user.sayhi();
}

这种对象的作用域都不会逃逸出方法外,也就是说该对象的生命周期会随着方法的调用开始而开始,方法的调用结束而结束。
假设JVM所有的对象都放在堆内存中(为什么用假设,因为JVM并不是这样)一旦方法结束,没有了指向该对象的引用,该对象就需要被GC回收,如果存在很多这样的情况,对GC来说压力山大呀。

那么什么又是指针碰撞呢?
假设JVM虚拟机上,堆内存都是规整的。堆内存被一个指针一分为二。指针的左边都被塞满了对象,指针的右变是未使用的区域。每一次有新的对象创建,指针就会向右移动一个对象size的距离。这就被称为指针碰撞。

在这里插入图片描述
好,问题来了。如果我们用多线程执行刚才的dome方法,一个线程正在给A对象分配内存,指针还没有来的及修改,同时为B对象分配内存的线程,仍引用这之前的指针指向。这样就出现毛病了。
(要注意的是,上面两种情况解决方案不止一个,我今天主要是讲TLAB,其他方案自行查询)

TLAB

TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。

如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率。

TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。

TLAB的本质其实是三个指针管理的区域:start,top 和 end,每个线程都会从Eden分配一块空间,例如说100KB,作为自己的TLAB,其中 start 和 end 是占位用的,标识出 eden 里被这个 TLAB 所管理的区域,卡住eden里的一块空间不让其它线程来这里分配。

TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。从这一点看,它被翻译为 线程私有分配区 更为合理一点
当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB,而在老TLAB里的对象还留在原地什么都不用管——它们无法感知自己是否是曾经从TLAB分配出来的,而只关心自己是在eden里分配的。

TLAB的缺点

事务总不是完美的,TLAB也又自己的缺点。因为TLAB通常很小,所以放不下大对象。

  • 1、TLAB空间大小是固定的,但是这时候一个大对象,我TLAB剩余的空间已经容不下它了。(比如100kb的TLAB,来了个110KB的对象)

  • 2,TLAB空间还剩一点点没有用到,有点舍不得。(比如100kb的TLAB,装了80KB,又来了个30KB的对象)
    所以JVM开发人员做了以下处理,设置了最大浪费空间。
    当剩余的空间小于最大浪费空间,那该TLAB属于的线程在重新向Eden区申请一个TLAB空间。进行对象创建,还是空间不够,那你这个对象太大了,去Eden区直接创建吧!
    当剩余的空间大于最大浪费空间,那这个大对象请你直接去Eden区创建,我TLAB放不下没有使用完的空间。

    当然,又回造成新的病垢。

  • 3,Eden空间够的时候,你再次申请TLAB没问题,我不够了,Heap的Eden区要开始GC,

  • 4,TLAB允许浪费空间,导致Eden区空间不连续,积少成多。以后还要人帮忙打理。

二、PLAB

老年代线程独享的堆区

可以看到和 TLAB 很像,PLAB 即 Promotion Local Allocation Buffers。

用在年轻代对象晋升到老年代时。

在多线程并行执行 YGC 时,可能有很多对象需要晋升到老年代,此时老年代的指针就“热”起来了,于是搞了个 PLAB。

先从老年代 freelist(空闲链表 申请一块空间,然后在这一块空间中就可以通过指针加法(bump the pointer)来分配内存,这样对 freelist 竞争也少了,分配空间也快了。
在这里插入图片描述

三、OOM(Out of Memory )

为什么会发生OOM

回收的速度比不上用的速度

来不及回收

哪几个区会发生OOM

1、堆区

package com.jihu.test.oom;

import java.util.ArrayList;
import java.util.List;

public class HeapOverFlowTest {
    int[] intArr = new int[58];

    // -Xms15m -Xmx15m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:/tmp/heapdump.hprof
    public static void main(String[] args) {
        List<HeapOverFlowTest> objs = new ArrayList<>();



        for (;;) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            objs.add(new HeapOverFlowTest());
        }
    }
}


在这里插入图片描述
在这里插入图片描述

我们这里不停的创建对象,JVM会一直full GC.

在这里插入图片描述
GC overhead limit exceeded Eoor: 这里指的是频繁的full gc导致的OOM,这里还不是因为head overflow。

GC日志

GC日志
相关参数:
-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:…/logs/gc.log 日志文件的输出路径
–XX:+PrintFlagsFinal -version可以输出按字母排序的所有XX参数和值的表格
-XX:+HeapDumpOnOutOfMemoryError参数表示当JVM发生OOM时,自动生成DUMP文件。

日志内容:
1、gc类型:GC、Full GC
2、gc原因:Metadata GC Threshold、Last ditch collection……
3、gc前内存数据
4、gc后内存数据
5、花费的时间:用户态、内核态、实际用时

使用perfma查看dump出的堆区GC日志

1、寒泉子公司的:https://xpocket.perfma.com/

先点击社区讨论,然后点击控制台,上传dump出来的gc日志
在这里插入图片描述
然后我们点击本地上传:
在这里插入图片描述
在这里插入图片描述
然后我们点击到类视图中来分析:
在这里插入图片描述

我们可以看到int[]这个类占了很大的内存,我们总共设置了15m,它就占用了12m. 我们点进去分析,查看被引用对象列表:
在这里插入图片描述
在这里插入图片描述
然后我们查看这个列表,看看是被在哪里被创建出来的。
在这里插入图片描述
这里已经可以定位到具体的某一个类。

使用VisualVM分析日志

我们启动visualVM之后,选择文件,装入本地日志:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们点击类,然后可以看到int占用了非常多的内存。
在这里插入图片描述
我们点进去查看发现很多引用是类HeapOverFlowTest.这样就能定位到具体的类了。

通过日志定位问题

1、找到内存占用比较多的实例

调优原则

1、通过日志找到具体的原因,到底是是否是程序的原因
2、如果不是程序的原因,那就调优堆区

full gc产生的原因

老年代满了
分析什么样的对象会进入老年代

2、方法区

CHLIB是字节码增强工具,可以直接操作字节码。

我们写一段程序,借助GCLIB,动态生成instanceKlass对象:

package com.jihu.test.oom;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class MetaSpaceOverFlowTest {

    /**
     * -XX:+PrintGCDetails -XX:MetaspaceSize=20m -XX:MaxMetaspaceSize=20m
     * 
     * 需要CGLIB依赖:
     * <dependency>
     * <groupId>cglib</groupId>
     * <artifactId>cglib</artifactId>
     * <version>2.2.2</version>
     * </dependency>
     * <p>
     * 通过CGLIB模拟向元空间写入数据
     */
    public static void main(final String[] args) {
        while (true) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(MetaSpaceOverFlowTest.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object obj, Method method, Object[] objects, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });

            System.out.println("running...");

            enhancer.create();
        }
    }
}

在这里插入图片描述
在这里插入图片描述
GC日志:

[GC (Metadata GC Threshold) 
	// GC:表明此次是Minor GC
	// Metadata GC Threshold: 产生GC的原因
[PSYoungGen: 4337K->64K(335360K)] 7912K->3638K(2415616K), 0.0005997 secs] 
	// PSYoungGen: 使用的新生代垃圾回收器, Parallel Scavenge
	// 4337K:GC之前新生代占用的内存大小
	// 64K:GC之后新生代占用的内存大小
	// 335360KK:新生代总内存大小
	
	// 7912K:GC之前堆占用的空间
	// 3638K: GC之后堆占用的空间
	// 2415616K:整个堆的空间大小
	// 0.0005997 secs:执行GC的时间
[Times: user=0.00 sys=0.00, real=0.00 secs] 
// user=0.00:GC在用户态的耗时时间
// sys=0.00:GC在内核态的耗时时间
// real=0.00 secs GC阶段实际耗时

[Full GC (Metadata GC Threshold) 
	//	Full GC:表明此次是full gc	
	//	Metadata GC Threshold:GC原因
[PSYoungGen: 64K->0K(335360K)] 
	// 	PSYoungGen:新生代垃圾回收器, Parallel Scavenge
	//	64K:GC之前新生代占用内存大小
	// 	0K: GC之后新生代占用内存大小
[ParOldGen: 3574K->3573K(2080256K)] 3638K->3573K(2415616K), 
	//	ParOldGen: 老年代垃圾回收器, Parallel Old
	// 	3574K:GC之前老年代占用内存大小
	// 	3573K:GC之后老年代占用内存大小
	//	2080256K:老年代占用的总内存大小

	// 	3638K:GC之前整个堆占用的大小
	//	3573K: GC之后整个堆占用的大小
	//	2415616K:整个堆占用的总大小  	
[Metaspace: 19840K->19840K(1067008K)], 0.0174651 secs] 
	//	Metaspace:元空间
	//	19840K:GC之前元空间占用的内存大小
	// 	19840K:GC之后元空间占用的内存大小
	//	1067008K:整个元空间占用的内存大小
	// 	0.0174651 secs:GC执行时间
[Times: user=0.02 sys=0.00, real=0.02 secs] 
	// user=0.00:GC在用户态的耗时时间
	// sys=0.00:GC在内核态的耗时时间
	// real=0.00 secs GC阶段实际耗时

我们来使用G1垃圾回收器,然后查看GC日志:
-XX:+PrintGCDetails -XX:MetaspaceSize=20m -XX:MaxMetaspaceSize=20m -XX:+UseG1GC

[GC pause (G1 Evacuation Pause) (young), 0.0094929 secs]
   [Parallel Time: 2.3 ms, GC Workers: 4]
      [GC Worker Start (ms): Min: 1572.1, Avg: 1572.1, Max: 1572.2, Diff: 0.0]
      [Ext Root Scanning (ms): Min: 0.1, Avg: 0.4, Max: 0.6, Diff: 0.4, Sum: 1.7]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.3, Diff: 0.3, Sum: 0.4]
      [Object Copy (ms): Min: 1.5, Avg: 1.7, Max: 1.8, Diff: 0.2, Sum: 6.6]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Termination Attempts: Min: 1, Avg: 3.0, Max: 5, Diff: 4, Sum: 12]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.1, Max: 0.3, Diff: 0.3, Sum: 0.4]
      [GC Worker Total (ms): Min: 2.3, Avg: 2.3, Max: 2.3, Diff: 0.0, Sum: 9.1]
      [GC Worker End (ms): Min: 1574.4, Avg: 1574.4, Max: 1574.4, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.0 ms]
   [Other: 7.1 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.5 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.0 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 9216.0K(9216.0K)->0.0B(14.0M) Survivors: 0.0B->2048.0K Heap: 9216.0K(192.0M)->1724.3K(192.0M)]
 [Times: user=0.00 sys=0.00, real=0.01 secs] 

然后我们来讲代码中的一个参数useCache设置成true再来看看:

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MetaSpaceOverFlowTest.class);
enhancer.setUseCache(true);

在这里插入图片描述

此时我们看到元空间的内存占用很平稳。
enhancer.setUseCache(true); 设置成true,此时生成的类比较少。
会根据类的全限定名去判断,如果已经存在了,就不会再去创建了。

方法区调优

1、参数

-XX:MetaspaceSize=10m
-XX:MaxMetaspaceSize=10m

2、调优原则

1、最大、最小设置成一样大
2、程序运行起来后,通过visualVM、arthas查看占用了多少内存,向上调优,预留20%以上的空间

3、栈

问题:一个栈帧占多少内存?

我们测试的时候会发现,栈的深度是一直在变化的。

栈上分配
多大的对象会在栈上分配

栈溢出测试

package com.jihu.test.oom;

public class StackOverFlowTest {

    private int val = 1;

    public void test() {
        val++;

        test();
    }

    public static void main(String[] args) {
        StackOverFlowTest stackOverFlowTest = new StackOverFlowTest();

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        try {
            stackOverFlowTest.test();
        } catch (Throwable t) {
            t.printStackTrace();

            System.out.println(stackOverFlowTest.val);
        }
    }


}
-------------------
第一次结果:
20540
	at com.jihu.test.oom.StackOverFlowTest.test(StackOverFlowTest.java:10)
----------------
第二次结果:
	at com.jihu.test.oom.StackOverFlowTest.test(StackOverFlowTest.java:10)
19219

从结果上我们可以看到,每次运行的结果都是不同的,这是因为有栈上分配存在的原因。

我们调整一下,将栈大小设置为250k. 继续测试:

-Xss250k

------------------
栈深度: 2763
每个栈帧占多少字节:250 * 1024 / 2763

Process finished with exit code 0

四、调优工具

jps

1、jps
在这里插入图片描述
可以列出java进程和进程id,但是只有类名。

2、jps -l
在这里插入图片描述
列出java进程,此时是类的权限定名。

3、jps -lmv
在这里插入图片描述
会列出更多的调优参数。

jps 实现原理

Java进程在创建的时候,会生成相应的文件,进程相关的信息会写入到该文件中。Windows下默认路径是C:\Users\username\AppData\Local\Temp\hsperfdata_username(注意,AppData是隐藏目录),Linux下默认路径是/tem/hsperfdata_username.

在这里插入图片描述

在这里插入图片描述
所以我们有时候命名kill了一个进程,但是使用jps命令依然可以查到,原因就是kill了之后非正常退出,文件没有被删掉。

jstat

Jstat是JDK自带的一个轻量级小工具。全称“Java Virtual Machine statistics monitoring tool”,它位于java的bin目录下,主要利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控。可见,Jstat是轻量级的、专门针对JVM的工具

使用时,需加上查看进程的进程id,和所选参数。参考格式如下:

jstat -options 

可以列出当前JVM版本支持的选项,常见的有:

  • l class (类加载器)
  • l compiler (JIT)
  • l gc (GC堆状态)
  • l gccapacity (各区大小)
  • l gccause (最近一次GC统计和原因)
  • l gcnew (新区统计)
  • l gcnewcapacity (新区大小)
  • l gcold (老区统计)
  • l gcoldcapacity (老区大小)
  • l gcpermcapacity (永久区大小)
  • l gcutil (GC统计汇总)
  • l printcompilation (HotSpot编译统计)

在这里插入图片描述
jstat实现原理和jps是一样的,也会生成对应的文件。

这些命令工具我们熟悉一下即可,主要将工具VisualVM搞清楚。因为即使是线上,我们也是dump出gc日志,然后使用VisualVM来进行分析的。

Jstat实现原理

jstat输出的这些值从哪来的
PerfData文件
Windows下默认理解是C:\Users\username\AppData\Local\Temp\hsperfdata_username
Linux下默认路径是/tmp/hsperfdata_username

PerfData文件

1、文件创建

取决于两个参数

-XX:-/+UsePerfData

默认是开启的
关闭方式:-XX:-UsePerfData。如果关闭了,就不会创建PerfData文件

-XX:-/+PerfDisableSharedMem(禁用共享内存)

默认是关闭的,即支持内存共享。如果禁用了,依赖于PerfData文件的工具就无法正常工作了

2、文件删除

默认情况下随Java进程的结束而销毁

3、文件更新

-XX:PerfDataSamplingInterval = 50ms
即内存与PerfData文件的数据延迟为50ms
纯Java编写
\openjdk\jdk\src\share\classes\sun\tools\jstat\Jstat.java

Java Agent

其实就是进程attach,有两种实现方式:

1、命令行
程序没有启动时可以通过在命令行上指定javaagent的方式来启动代理

-javaagent:jarpath[=options]

#如:
java -javaagent:xxx-agent.jar -cp xxx.jar com.wwh.xxxx

通过命令行的方式可以指定多个代理,并且支持参数。初始化Java虚拟机(JVM)之后,将按照指定代理的顺序调用每个premain方法,然后调用真正的应用程序main方法。每个premain方法必须返回,以便继续启动程序。

2、启动后attach
程序已经启动后可以通过VirtualMachine 来加载启动代理:

VirtualMachine vm = VirtualMachine.attach("2177");
vm.loadAgent(jar);
vm.detach();

注意:

代理JAR的manifest中必须包含属性 Agent-Class。此属性的值是代理类的名称。
代理类必须实现一个公共静态的 agentmain 方法,如下所示。

Java Agent(JVMTI Agent)是调优工具可以调试java进程的本质

五、实战

统计线程数

jstack -l 6972 | grep ‘java.lang.Thread.State’ | wc -l

死锁

可使用jstack、jconsle、visualVM

package com.jihu.test.oom;

public class DeadLock implements Runnable {
	/**
	 * 定义两个Object对象,模拟两个线程占有的共享资源
	 * 此处需要注意的是,o1和o2 需要有static修饰,定义为静态对象,这样o1和o2才能在多个线程之间调用,才属于共享资源,
	 * 没有static修饰的话,DeadLock的每个实例对象中的 o1和o2 都将是独立存在,相互隔离的,
	 */
	public static Object o1 = new Object();
	public static Object o2 = new Object();

	public int flag; // 属性,又叫成员变量

	public DeadLock(int flag) {
		super();
		this.flag = flag;
	}

	@Override
	public void run() {
		if (flag == 1) {
			// 代码块1
			synchronized (o1) {
				System.out.println("one-1");
				try {
					Thread.sleep(1000);
				} catch (Exception e) {
					e.printStackTrace();
				}
				synchronized (o2) {
					System.out.println("one-2");
				}
			}
		} else {
			// 代码块2
			synchronized (o2) {
				System.out.println("two-1");
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized (o1) {
					System.out.println("two-2");
				}
			}
		}
	}

	public static void main(String[] args) {
		//创建线程1,flag 属性值为1
		DeadLock deadLock1 = new DeadLock(1);
		//创建线程1,flag 属性值为2
		DeadLock deadLock2 = new DeadLock(2);
		//启动线程1和线程2
		/**
		 * 线程1启动之后,调用顺序是
		 * (1)执行代码块1,同时获取到o1对象锁,开始执行,线程沉睡1秒
		 * (2)接着去获取o2的对象锁,由于第二个线程先获取的是o2的对象锁,所以需要等待代码块2执行完毕,才能获取到o2的对象锁
		 */
		new Thread(deadLock1).start();
		/**
		 * 线程2启动之后,调用顺序是
		 * (1)执行代码块2,同时获取到o2对象锁,开始执行,线程沉睡1秒
		 * (2)接着去获取o1的对象锁,由于第一个线程先获取的是o1的对象锁,所以需要等待代码块1执行完毕,才能获取到o1的对象锁
		 */
		new Thread(deadLock2).start();
		/** 以上分析可得,线程一和线程二共用了对象o1和o2,各自都想要获取对方的锁,从而形成阻塞,一直等待下去,这种现象就是死锁。*/

		while (true);
	}

}

使用VisualVM查看:
在这里插入图片描述
在这里插入图片描述

CPU占用过高

排查思路
1、找到进程
2、找到线程
3、分析代码

package com.jihu.test.oom;

public class CPUHigh {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hi");
                }
            }
        }, "thread-couhigh").start();
    }
}

可以使用top(linux)命令查看占用CPU较高的进程。类似于windows的任务管理器一样。
在这里插入图片描述
2、定位到目前占用CPU最高的线程ID

top -H -p 6290

在这里插入图片描述
线程ID由十进制转成十六进制
3、定位线程

jstack 6290(进程ID)|grep 18a1(线程ID,十六进制) -A 30

参考文章:https://www.jianshu.com/p/8be816cbb5ed
参考文章:https://www.jianshu.com/p/f5efc53ced5d

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值