无线循环里面 string = “i”会内存溢出吗?_JVM图文系列一文学会JVM性能优化!

d3ab1303c36c3a9d70b59261d52e7851.png

36654f249ee5c549c21a86a7f2febce3.png

公众号ID:Java-jiagou码字不易,加个鸡腿吧!

9 实战性能优化

9.1 重新认知JVM

之前我们画过一张图,是从Class文件到类装载器,再到运行时数据区的过程,现在咱们把这张图不妨丰富完善一下,展示了JVM的大体物理结构图。

e8ffcb6a90e89d4b3f7ad996c1fc3953.png

执行引擎:用于执行JVM字节码指令

主要由两种实现方式:

(1)将输入的字节码指令在加载时或执行时翻译成另外一种虚拟机指令;

(2)将输入的字节码指令在加载时或执行时翻译成宿主主机本地CPU的指令集。这两种方式对应着字节码的解释执行和即时编译。

9.2 堆内存溢出

9.2.1 代码

@RestControllerpublic class HeapController {    Listlist=new ArrayList();    @GetMapping("/heap")    public String heap(){        while(true){            list.add(new Person());        }    }}

记得设置参数比如-Xmx20M -Xms20M

9.2.2 运行结果

访问->http://localhost:8080/heap

Exception in thread "http-nio-8080-exec-2" java.lang.OutOfMemoryError: GC overhead limit exceeded

9.2.3 回顾jps和jinfo

5590c7d54e0aa740c4b5a59670e9503a.png

9.2.4 回顾jmap手动导出和参数自动导出

 jmap手动导出:jmap -dump:format=b,file=heap.hprof PID

7b1ba7512bd65ba5d9a3f81001da92b0.png

参数自动导出:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heapdump.hprof

50aaea563d072994a82f4aef314cd6a1.png

9.3 方法区内存溢出

比如向方法区中添加Class的信息

9.3.1 asm依赖和Class代码

xml<dependency>    <groupId>asmgroupId>    <artifactId>asmartifactId>    <version>3.3.1version>dependency>
public class MyMetaspace extends ClassLoader {    public static List> createClasses() {        List> classes = new ArrayList>();        for (int i = 0; i < 10000000; ++i) {            ClassWriter cw = new ClassWriter(0);            cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null,                    "java/lang/Object", null);            MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "",                    "()V", null, null);            mw.visitVarInsn(Opcodes.ALOAD, 0);            mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",                    "", "()V");            mw.visitInsn(Opcodes.RETURN);            mw.visitMaxs(1, 1);            mw.visitEnd();            Metaspace test = new Metaspace();            byte[] code = cw.toByteArray();            Class> exampleClass = test.defineClass("Class" + i, code, 0, code.length);            classes.add(exampleClass);        }        return classes;    }}

9.3.2 代码

@RestControllerpublic class NonHeapController {    List> list=new ArrayList>();    @GetMapping("/nonheap")    public String nonheap(){        while(true){            list.addAll(MyMetaspace.createClasses());        }    }}

设置Metaspace的大小,比如-XX:MetaspaceSize=50M -XX:MaxMetaspaceSize=50M

9.3.3 运行结果

访问->http://localhost:8080/nonheap

java.lang.OutOfMemoryError: Metaspace  at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0_191]  at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ~[na:1.8.0_191]

9.4 虚拟机栈

9.4.1 代码演示StackOverFlow

public class StackDemo {    public static long count=0;    public static void method(long i){        System.out.println(count++);        method(i);    }    public static void main(String[] args) {        method(1);    }}

9.4.2 运行结果

ac45fe155e4c506efc6a6aa535e589f2.png

9.4.3 说明

Stack Space用来做方法的递归调用时压入Stack Frame(栈帧)。所以当递归调用太深的时候,就有可能耗尽Stack Space,爆出StackOverflow的错误。

-Xss128k:设置每个线程的堆栈大小。JDK 5以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。

9.5 线程死锁

9.5.1 代码

//运行主类public class DeadLockDemo{    public static void main(String[] args){        DeadLock d1=new DeadLock(true);        DeadLock d2=new DeadLock(false);        Thread t1=new Thread(d1);        Thread t2=new Thread(d2);        t1.start();        t2.start();    }}//定义锁对象class MyLock{    public static Object obj1=new Object();    public static Object obj2=new Object();}//死锁代码class DeadLock implements Runnable{    private boolean flag;    DeadLock(boolean flag){        this.flag=flag;    }    public void run() {        if(flag) {            while(true) {                synchronized(MyLock.obj1) {                    System.out.println(Thread.currentThread().getName()+"----if获得obj1锁");                    synchronized(MyLock.obj2) {                        System.out.println(Thread.currentThread().getName()+"----if获得obj2锁");                    }                }            }        }        else {            while(true){                synchronized(MyLock.obj2) {                    System.out.println(Thread.currentThread().getName()+"----否则获得obj2锁");                    synchronized(MyLock.obj1) {                        System.out.println(Thread.currentThread().getName()+"----否则获得obj1锁");                    }                }            }        }    }}

9.4.2 运行结果

7549464fd16cef9775ce564722857c57.png

9.4.3 jstack分析

add4fae895b001a8f4706606388e4a67.png

把打印信息拉到最后可以发现

bbad34e4aa2ec6eb1031cdf0311a0b79.png

9.4.4 jvisualvm

9ef910c28c3ec6a78e22f726b4099e29.png

将线程信息dump出来

4951038b0f66b8c5b83f5977ebc5d39a.png

9.6 垃圾收集

内存被使用了之后,难免会有不够用或者达到设定值的时候,就需要对内存空间进行垃圾回收。

9.6.1 垃圾收集发生的时机

GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。

当然,我们可以手动进行垃圾回收,比如调用System.gc()方法通知JVM进行一次垃圾回收,但是具体什么时刻运行也无法控制。也就是说System.gc()只是通知要回收,什么时候回收由JVM决定。

但是不建议手动调用该方法,因为消耗的资源比较大。

一般以下几种情况会发生垃圾回收

(1)当Eden区或者S区不够用了

(2)老年代空间不够用了

(3)方法区空间不够用了

(4)System.gc()

虽然垃圾回收的时机是不确定的,但是可以结合之前一个对象的一辈子案例,文字图解再次梳理一下堆内存回收的流程。

一个对象的一辈子

我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。

有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。

于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。

8cdce2282902a229bf0757e4a353fe33.png

9.6.2 实验环境准备

我的本地机器使用的是jdk1.8和tomcat8.5,大家也可以使用linux上的tomcat,然后把gc日志下载下来即可。

9.6.3 GC日志文件

回顾升华一下垃圾收集器图

0ba2ff4c6720d64354ead475e612a328.png

要想分析日志的信息,得先拿到GC日志文件才行,所以得先配置一下,之前也看过这些参数。

 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:$CATALINA_HOME/logs/gc.log

比如打开windows中的catalina.bat,在第一行加上

set JAVA_OPTS=%JAVA_OPTS% -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:gc.log 

这样使用startup.bat启动tomcat的时候就能够在当前目录下拿到gc.log文件

可以看到默认使用的是ParallelGC

9.6.3.1 Parallel GC日志

【吞吐量优先】

2019-06-10T23:21:53.305+0800: 1.303: [GC (Allocation Failure) [PSYoungGen: 65536K[Young区回收前]->10748K[Young区回收后](76288K[Young区总大小])] 65536K[整个堆回收前]->15039K[整个堆回收后](251392K[整个堆总大小]), 0.0113277 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

`注意`如果回收的差值中间有出入,说明这部分空间是Old区释放出来的

f9b21145b1a508585494107c9d402e78.png

9.6.3.2 CMS日志

【停顿时间优先】

参数设置

-XX:+UseConcMarkSweepGC

重启tomcat获取gc日志,这里的日志格式和上面差不多,不作分析。

9.6.3.3 G1日志

G1日志格式参考链接:

https://blogs.oracle.com/poonam/understanding-g1-gc-logs

【停顿时间优先】

why?

https://blogs.oracle.com/poonam/increased-heap-usage-with-g1-gc

参数设置

-XX:+UseG1GC

4ef43086a000142dfacafb8f15e12246.png

9.6.4 GC日志文件分析工具

9.6.4.1 gceasy

可以比较不同的垃圾收集器的吞吐量和停顿时间

e1d29f1cd8e75df205f34182e0aaf9ba.png

4ed9fd5c8e77d48cffe8b5c1e3811eea.png

9.6.4.2 GCViewer

3d60cd86733003ca9a50f66a2b365818.png

9.6.5 G1调优

是否选用G1垃圾收集器的判断依据

https://docs.oracle.com/javase/8/docs/technotes/guides/vm/G1.html#use_cases

(1)50%以上的堆被存活对象占用

(2)对象分配和晋升的速度变化非常大

(3)垃圾回收时间比较长

(1)使用G1GC垃圾收集器: -XX:+UseG1GC

修改配置参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间

Throughput       Min Pause       Max Pause      Avg Pause       GC count  99.16%         0.00016s         0.0137s        0.00559s          12

(2)调整内存大小再获取gc日志分析

-XX:MetaspaceSize=100M-Xms300M-Xmx300M

比如设置堆内存的大小,获取到gc日志,使用GCViewer分析吞吐量和响应时间

Throughput       Min Pause       Max Pause      Avg Pause       GC count  98.89%          0.00021s        0.01531s       0.00538s           12

 (3)调整最大停顿时间

-XX:MaxGCPauseMillis=200    设置最大GC停顿时间指标

比如设置最大停顿时间,获取到gc日志,使用GCViewer分析吞吐量和响应时间

Throughput       Min Pause       Max Pause      Avg Pause       GC count  98.96%          0.00015s        0.01737s       0.00574s          12

(4)启动并发GC时堆内存占用百分比

-XX:InitiatingHeapOccupancyPercent=45 G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。值为 0 则表示“一直执行GC循环)'. 默认值为 45 (例如, 全部的 45% 或者使用了45%).

比如设置该百分比参数,获取到gc日志,使用GCViewer分析吞吐量和响应时间

Throughput       Min Pause       Max Pause      Avg Pause       GC count  98.11%          0.00406s        0.00532s       0.00469s          12

9.6.6 G1调优的最佳实践

官网建议:

(https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations)

(1)不要手动设置新生代和老年代的大小,只要设置整个堆的大小

  • G1收集器在运行过程中,会自己调整新生代和老年代的大小

  • 其实是通过adapt代的大小来调整对象晋升的速度和年龄,从而达到为收集器设置的暂停时间目标

  • 如果手动设置了大小就意味着放弃了G1的自动调优

(2)不断调优暂停时间目标

一般情况下这个值设置到100ms或者200ms都是可以的(不同情况下会不一样),但如果设置成50ms就不太合理。暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度。最终退化成Full GC。所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态。暂停时间只是一个目标,并不能总是得到满足。

(3)使用-XX:ConcGCThreads=n来增加标记线程的数量

IHOP如果阀值设置过高,可能会遇到转移失败的风险,比如对象进行转移时空间不足。如果阀值设置过低,就会使标记周期运行过于频繁,并且有可能混合收集期回收不到空间。 

> IHOP值如果设置合理,但是在并发周期时间过长时,可以尝试增加并发线程数,调高ConcGCThreads。

(4)MixedGC调优 

-XX:InitiatingHeapOccupancyPercent

-XX:G1MixedGCLiveThresholdPercent

-XX:G1MixedGCCountTarger

-XX:G1OldCSetRegionThresholdPercent

(5)适当增加堆内存大小

9.7 一张图总结JVM性能优化

bf27c8e4fb999f9c66109b101056ffcf.png

全文完!

20c5268f0d4854d2074c7d07268aaa7c.png

欢迎关注“Java架构师学习”

如果觉得不错,请给个「在看」

分享给你的朋友!

96d4559679364f122c713ebc76e703bd.gif

fd2fdc1bd09bc8b0eb35d5ccd0358b5f.png

c160f8e7d7a9df2696eb605ab30c48c1.gif

 THANDKS

- End -

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值