JVM优化

本文详细介绍了垃圾回收的基本概念、常见算法(引用计数法、标记清除、标记压缩和复制算法、分代算法),以及JVM中不同垃圾收集器的参数和Tomcat服务器的配置优化技巧,包括管理用户、禁用AJP服务、线程池设置和性能提升的代码建议。
摘要由CSDN通过智能技术生成

一丶垃圾回收

一.什么是垃圾回收:

程序的运行必然需要申请内存资源,无效的对象资源如果不及时处理就会一直占有内存资源,最终将导致内存溢出,所以对内存资源的管理是非常重要了。

二.垃圾回收常见的算法:

1.引用计数法:假设有一个对象A,只要有一个A的引用,那么对象A的引用计数器就加一,当引用失败时,对象A的引用计数器就减一,如果对象A的引用计数器值为0,就说明对象A没有引用了,就可以被回收。(无法解决循环引用问题)

较为特殊的有循环引用:

class TestA{
public TestB b;
}
class TestB{
public TestA a;
}
public class Main{
public static void main(String[] args){
A a = new A();
B b = new B();
a.b=b;
b.a=a;
a = null;
b = null;
}
}

虽然a和b都为null,但是由于a和b都存在循环引用,这样a和b永远都不会被回收。

2.标记清除法:

标记清除算法,是将垃圾回收分为 2 个阶段,分别是标记和清除。
  •   标记:从根节点开始标记引用的对象。
  •   清除:未被标记引用的对象就是垃圾对象,可以被清理。

3.标记压缩算法:

标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的清理未标记的对象,而是将存的对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题
4.复制算法:
将原有的内存一分为二,每次只使用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个空间中,然后将该内存空间清空,并交换两个内存的角色,完成垃圾回收。
5.分代算法:
根据回收对象的特点进行选择不同的算法, jvm 中,年轻代适合使用复制算法,老年代适合使用
标记清除或标记压缩算法。

三.几种垃圾收集器的参数:

串行垃圾收集器:

-XX:+UseSerialGC 指定年轻代和老年代都使用串行垃圾收集器 -
-XX:+PrintGCDetails 打印垃圾回收的详细信息
-XX:+UseSerialGC -XX:+PrintGCDetails -Xms16m -Xmx16m

 并行垃圾收集器:

ParNew垃圾收集器:通过-XX:+UseParNewGC参数设置年轻代使用ParNew回收器,老年代使用的依然是串行收集器

ParallelGC垃圾收集器:

相关参数如下:
-XX:+UseParallelGC
年轻代使用 ParallelGC 垃圾回收器,老年代使用串行回收器。
-XX:+UseParallelOldGC
年轻代使用 ParallelGC 垃圾回收器,老年代使用 ParallelOldGC 垃圾回收器。
-XX:MaxGCPauseMillis
设置最大的垃圾收集时的停顿时间,单位为毫秒需要注意的时,ParallelGC 为了达到设置的停顿时间,可能会调整堆大小或其他的参数,如果堆的大小设置的较小,就会导致GC 工作变得很频繁,反而可能会影响到性能。该参数使用需谨慎。
-XX:GCTimeRatio
设置垃圾回收时间占程序运行时间的百分比,公式为 1/(1+n) 。它的值为0~100 之间的数字,默认值为 99 ,也就是垃圾回收时间不能超过 1%
-XX:UseAdaptiveSizePolicy
自适应 GC 模式,垃圾回收器将自动调整年轻代、老年代等参数,达到吞吐量、堆大小、停顿时间之间的平衡。一般用于,手动调整参数比较困难的场景,让收集器自动进行调整。
CMS垃圾收集器:

CMS垃圾回收器的执行过程如下:

  • 初始化标记(CMS-initial-mark) ,标记root,会导致stw
  • 并发标记(CMS-concurrent-mark),与用户线程同时运行;
  • 预清理(CMS-concurrent-preclean),与用户线程同时运行;
  • 重新标记(CMS-remark) ,会导致stw
  • 并发清除(CMS-concurrent-sweep),与用户线程同时运行;
  • 调整堆大小,设置CMS在清理之后进行内存压缩,目的是清理内存中的碎片;
  • 并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;

G1垃圾收集器:

不需要再对内存空间进行代划分清理,取而代之的是将堆内存划分为许多区域。

G1 划分的区域中,年轻代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者 Survivor空间,G1 收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。
youngGC:

YoungGC主要是对Eden区的数据进行GC,在Eden空间耗尽时会被触发

Eden 空间的数据移动到 Survivor 空间中,如果 Survivor 空间不够, Eden 空间的部分数据会直接晋升到年老代空间。
Survivor 区的数据移动到新的 Survivor 区中,也有部分数据晋升到老年代空间中。最终Eden 空间的数据为空, GC 停止工作,应用线程继续执行。
Remember Set(已记忆集合)

在寻找老年代对象时,全盘扫描会比较麻烦,RememberSet则会跟踪指向某个堆内的对象引用,那么在扫描时只需要扫描RS即可,提高了工作效率。

G1垃圾收集器的相关参数:
  • --XX:+UseG1GC  使用G1垃圾收集器
  • --XX:MaxGCPauseMillis  设置期望达到的最大GC停顿时间指标默认200ms
  • --XX:G1HeapRegionSize=n  设置G1区域大小,值是2的幂1~32MB。目标是根据JAVA最小的堆划分出越2048个区域
  • --XX:ParallelGCThreads=n   设置STW工作线程数的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量相同,最多 8
  • --XX:ConcGCThreads=n    设置并行标记的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 1/4 左右。
  • XX:InitiatingHeapOccupancyPercent=n    设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。

四:可视化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 日志文件的输出路径

GC Easy可视化工具:

GC Easy是一款在线的可视化工具,易用、功能强大,

二丶Tomcat配置优化

1.Tomcat 管理用户相关配置:

#修改配置文件,配置tomcat的管理用户
vim tomcat-users.xml
#写入如下内容:
<role rolename="manager"/>
<role rolename="manager-gui"/>
<role rolename="admin"/>
<role rolename="admin-gui"/>
<user username="tomcat" password="tomcat" roles="admin-gui,admin,manager-gui,manager"/>
#保存退出
#如果是tomcat7,配置了tomcat用户就可以登录系统了,但是tomcat8中不行,还需要修改另一个配置文件,否则访
问不了,提示403
vim webapps/manager/META-INF/context.xml
#将<Valve的内容注释掉
<Context antiResourceLocking="false" privileged="true" >
<!-- <Valve className="org.apache.catalina.valves.RemoteAddrValve"
allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" /> -->

2.禁用AJP服务:

修改 conf 下的 server.xml 文件,将 AJP 服务禁用掉即可。
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

3.执行器(线程池)

在tomcat中,每一个用户请求都是一个线程,所以使用线程池提高性能。

修改server.xml文件:

<!--将注释打开-->
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="500" minSpareThreads="50" prestartminSpareThreads="true"
maxQueueSize="100"/>
<!--
参数说明:
maxThreads:最大并发数,默认设置 200,一般建议在 500 ~ 1000,根据硬件设施和业务来判断
minSpareThreads:Tomcat 初始化时创建的线程数,默认设置 25
prestartminSpareThreads: 在 Tomcat 初始化的时候就初始化 minSpareThreads 的参数值,如果不等于
true,minSpareThreads 的值就没啥效果了
maxQueueSize,最大的等待队列数,超过则拒绝请求
-->
<!--在Connector中设置executor属性指向上面的执行器-->
<Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />

4种运行模式:

1. bio 默认的模式 , 性能非常低下 , 没有经过任何优化处理和支持 .
2. nio nio(new I/O) ,是 Java SE 1.4 及后续版本提供的一种新的 I/O 操作方式 ( java.nio 包及其子包 ) Java nio 是一个基于缓冲区、并能提供非阻塞I/O 操作的 Java API ,因此 nio 也被看成是 non-blocking I/O 的缩写。它拥有比传统I/O 操作 (bio) 更好的并发运行性能。
3. apr 安装起来最困难 , 但是从操作系统级别来解决异步的 IO 问题 , 大幅度的提高性能 .
推荐使用 nio ,不过,在 tomcat8 中有最新的 nio2 ,速度更快,建议使用 nio2.
设置 nio2
<Connector executor="tomcatThreadPool" port="8080"
protocol="org.apache.coyote.http11.Http11Nio2Protocol"
connectionTimeout="20000"
redirectPort="8443" /

5.调整年轻代大小:

在bin目录下的cataline.sh中加入以下配置:

JAVA_OPTS="-XX:+UseParallelGC -XX:+UseParallelOldGC -Xms128m -Xmx1024m -XX:NewSize=64m
-XX:MaxNewSize=256m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -
XX:+PrintHeapAtGC -Xloggc:../logs/gc.log"

三丶JVM字节码

1.通过javap命令查看class文件的字节码内容:

例如:将Test1.class的字节码内容重定向到Test1.txt中

javap -v Test1.class > Test1.txt

2.常量池:

官网文档:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4-140

3.描述符:

官网: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2
方法描述符:
示例:
The method descriptor for the method:
Object m(int i, double d, Thread t) {...}

is

(IDLjava/lang/Thread;)Ljava/lang/Object;

四丶代码优化建议:

优化,不仅仅是在运行环境进行优化,还需要在代码本身做优化,如果代码本身存在性能问题,那么在其他方面再怎么优化也不可能达到效果最优的。

1、尽可能使用局部变量

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。

2、尽量减少对变量的重复计算

明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的。所以例如下面的操作:
建议替换为:
这样,在 list.size() 很大的时候,就减少了很多的消耗。

3、尽量采用懒加载的策略,即在需要的时候才创建

4、异常不应该用来控制程序流程

for (int i = 0; i < list.size(); i++)
{...}
int length = list.size();
for (int i = 0, i < length; i++)
{...}
String str = "aaa";
if (i == 1){
list.add(str);
}
//建议替换成
if (i == 1){
String str = "aaa";
list.add(str);
}
异常对性能不利。抛出异常首先要创建一个新的对象, Throwable 接口的构造函数调用名为 fifillInStackTrace() 的本地同步方 法,fifillInStackTrace() 方法检查堆栈,收集调用跟踪信息。只要有异常被抛出, Java 虚拟机就必须调整调用堆栈,因为在处理过程中创建 了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。

5、不要将数组声明为public static fifinal

因为这毫无意义,这样只是定义了引用为 static fifinal ,数组的内容还是可以随意改变的,将数组声明为 public 更是一个安全漏洞,这意味着这个数组可以被外部类所改变。

6、不要创建一些不使用的对象,不要导入一些不使用的类

这毫无意义,如果代码中出现 "The value of the local variable i is not used" "The import java.util is never used",那么请删除这些无用的内容

7、程序运行过程中避免使用反射

反射是 Java 提供给用户一个很强大的功能,功能强大往往意味着效率不高。不建议在程序运行过程中使用尤其是频 繁使用反射机制,特别是 Method invoke 方法。如果确实有必要,一种建议性的做法是将那些需要通过反射加载的类在项目启动的时候通过反射实例化出一个对象并放入内存。

8、使用数据库连接池和线程池

这两个池都是用于重用对象的,前者可以避免频繁地打开和关闭连接,后者可以避免频繁地创建和销毁线程。

9、容器初始化时尽可能指定长度

容器初始化时尽可能指定长度,如: new ArrayList<>(10); new HashMap<>(32); 避免容器长度不足时,扩容带来的性能损耗。

10ArrayList随机遍历快,LinkedList添加删除快

11、使用Entry遍历Map

避免使用这种方式:

12、不要手动调用System.gc();

Map<String,String> map = new HashMap<>();
for (Map.Entry<String,String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
}
Map<String,String> map = new HashMap<>();
for (String key : map.keySet()) {
String value = map.get(key);
}

13String尽量少用正则表达式

正则表达式虽然功能强大,但是其效率较低,除非是有需要,否则尽可能少用。replace() 不支持正则 replaceAll() 支持正则如果仅仅是字符的替换建议使用replace()

14、日志的输出要注意级别

15、对资源的close()建议分开操作

// 当前的日志级别是error
LOGGER.info("保存出错!" + user);
try{
XXX.close();
YYY.close();
}
catch (Exception e){
...
}
// 建议改为
try{
XXX.close();
}
catch (Exception e){
...
}
try{
YYY.close();
}
catch (Exception e){
...
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

月初,

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值