JVM 必知必会
JVM 简介
JAVA 虚拟机简介
Java 虚拟机定义
Java 虚拟机有多层含义
- 一套规范:Java 虚拟机规范。定义概念上 Java 虚拟机的行为表现
- 一种实现:例如 HotSpot,J9,JRockit。需要实现 JVM 规范,但具体实现方式不需要与“概念中”的 JVM 一样。
- 一个运行中的实例,某个 JVM 实现的某次运行的实例。
- 只要输入为符合规范的 Class 文件即可执行。并非一定要执行 Java 程序,可以支持其它语言,像 Scala、Clojure、Groovy、Fantom、Fortress、Nice、Jython、 JRuby、Rhino、Ioke、Jaskell、(C、Fortran)
JVM 和 JRE、JDK 的关系
- JVM:Java Virtual Machine,负责执行符合规范的 Class 文件。
- JRE:Java Runtime Environment,包含 JVM 和类库。
- JDK:Java Development Kit,包含 JRE 和一些开发工具,如 javac。
JVM 实例和 JVM 执行引擎实例
JVM 实例对应了一个独立运行的 java 程序,而 JVM 执行引擎实例则对应了属于用户运行程序的线程;也就是 JVM 实例是进程级别,而执行引擎是线程级别的。
JVM 的基本结构
类加载子系统
JVM 的类加载是通过 ClassLoader 及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:
- Bootstrap ClassLoader 负责加载$JAVA_HOME/jre/lib 里所有的类库到内存,Bootstrap ClassLoader 是 JVM 级别的,由 C++实现,不是 ClassLoader 的子类,开发者也无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
- Extension ClassLoader 负责加载 java 平台中扩展功能的一些 jar 包,主要是由 sun.misc.Launcher$ExtClassLoader 实现的,是一个 java 类,继承自 URLClassLoader 超类。它将负责%JRE_HOME/lib/ext 目录下的 jar 和 class 加载到内存,开发者可以直接使用该加载器。
- App ClassLoader 负责加载环境变量 classpath 中指定的 jar 包及目录中 class 到内存中,开发者也可以直接使用系统类加载器。
- Custom ClassLoader 属于应用程序根据自身需要自定义的 ClassLoader(一般为 java.lang.ClassLoader 的子类) 在程序运行期间,通过 java.lang.ClassLoader 的子类动态加载 class 文件,体现 java 动态实时类装入特性,如 tomcat、jboss 都会根据 j2ee 规范自行实现 ClassLoader。自定义 ClassLoader 在某些应用场景还是比较适用,特别是需要灵活地动态加载 class 的时候。
内存模型
这张图是我见过的最能描述 JVM 内存模型的图,JVM 包括两个子系统和两个组件。两个子系统为:class loader(类装载)、Execution engine(执行引擎);两个组件为:Runtime data area(运行时数据区)、Native interface(本地接口)
Class loader 功能:根据给定的全限定名类名(如:java.lang.Object) 来装载 class 文件到 Runtime data area 中的 method area。程序中可以 extends java.lang.ClassLoader 类来实现自己的 Class loader。
Execution engine 功能:执行 classes 中的指令。任何 JVM specification 实现(JDK) 的核心都是 Execution engine,不同的 JDK 例如 Sun 的 JDK 和 IBM 的 JDK 好坏主要就取决于他们各自实现的 Execution engine 的好坏。
Native interface 组件:与 native libraries 交互,是其它编程语言交互的接口。当调用 native 方法的时候,就进入了一个全新的并且不再受虚拟机限制的世界,所以也很容易出现 JVM 无法控制的 native heap OutOfMemory。
Runtime Data Area 组件:这就是我们常说的 JVM 的内存。主要分为五个部分:
- Heap (堆):一个 Java 虚拟实例中只存在一个堆空间
- Method Area(方法区域):被装载的 class 的信息存储在 Method area 的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的 class 文件,然后读入这个 class 文件内容并把它传输到虚拟机中。
- Java Stack(java 的栈):虚拟机只会直接对 Java stack 执行两种操作:以帧为单位的压栈或出栈
- Program Counter(程序计数器):每一个线程都有它自己的 PC 寄存器,也是该线程启动时创建的。PC 寄存器的内容总是指向下一条将被执行指令的饿地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。
- Native method stack(本地方法栈):保存 native 方法进入区域的地址
以上五部分只有 Heap 和 Method Area 是被所有线程的共享使用的;而 Java stack, Program counter 和 Native method stack 是以线程为粒度的,每个线程独自拥有自己的部分。
内存回收简介
Sun 的 JVM GC(垃圾回收) 原理:把对象分为:年轻代(Young)、年老代(Tenured)、持久代(Perm),对不同生命周期的对象使用不同的算法。(基于对对象生命周期分析)
- Young(年轻代) 年轻代分三个区。一个 Eden 区,两个 Survivor 区。大部分对象在 Eden 区中生成。当 Eden 区满时,还存活的对象将被复制到 Survivor 区(两个中的一个),当这个 Survivor 区满时,此区的存活对象将被复制到另外一个 Survivor 区,当这个 Survivor 去也满了的时候,从第一个 Survivor 区复制过来的并且此时还存活的对象,将被复制年老区(Tenured。需要注意,Survivor 的两个区是对称的,没先后关系,所以同一个区中可能同时存在从 Eden 复制过来 对象,和从前一个 Survivor 复制过来的对象,而复制到年老区的只有从第一个 Survivor 去过来的对象。而且,Survivor 区总有一个是空的。
- Tenured(年老代) 年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。
- Perm(持久代) 用于存放静态文件,如今 Java 类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class,例如 Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
举个例子:当在程序中生成对象时,正常对象会在年轻代中分配空间,如果是过大的对象也可能会直接在年老代生成(据观测在运行某程序时候每次会生成一个十兆的空间用收发消息,这部分内存就会直接在年老代分配)。年轻代在空间被分配完的时候就会发起内存回收,大部分内存会被回收,一部分幸存的内存会被拷贝至 Survivor 的 from 区,经过多次回收以后如果 from 区内存也分配完毕,就会也发生内存回收然后将剩余的对象拷贝至 to 区。等到 to 区也满的时候,就会再次发生内存回收然后把幸存的对象拷贝至年老区。
通常我们说的 JVM 内存回收总是在指堆内存回收,确实只有堆中的内容是动态申请分配的,所以以上对象的年轻代和年老代都是指的 JVM 的 Heap 空间,而持久代则是之前提到的 Method Area,不属于 Heap。
内存分析命令
jinfo:
查看 Java 进程的栈空间大小:sudo -u tomcat /home/java/default/bin/jinfo - ThreadStackSize 14750
查看是否使用了压缩指针:sudo -u tomcat /home/java/default/bin/jinfo -flag UseCompressedOops 14750
查看系统属性:sudo -u tomcat /home/java/default/bin/jinfo -sysprops 14750
jstack:
查看一个指定的 Java 进程中的线程的状态:sudo -u tomcat /home/java/default/bin/jstack 14750
jstat:
查看 gc 的信息:sudo -u tomcat /home/java/default/bin/jstat -gcutil 14750
jmap&mat
空间中各个年龄段的空间的使用情况:sudo -u tomcat /home/java/default/bin/jmap -heap 14750
jmap 指定的 dump 文件一定要是 tomcat 用户可写,比如可以新创建一个文件夹
sudo mkdir /home/memdump
sudo chown tomcat:tomcat /home/memdump
sudo -u tomcat /home/java/default/bin/jmap -dump:live,format=b,file=/home/memdump/memMap.20131125.hprof 14750
垃圾收集
垃圾收集器
垃圾收集算法
收集器选择
垃圾收集器选择
JVM 给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0 以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0 以后,JVM 会根据当前系统配置进行判断。
吞吐量优先的并行收集器
如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
典型配置:
java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
-XX:+UseParallelGC: 选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。 -XX:ParallelGCThreads=20: 配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。 java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
-XX:+UseParallelOldGC: 配置年老代垃圾收集方式为并行收集。JDK6.0 支持对年老代并行收集。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=100: 设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM 会自动调整年轻代大小,以满足此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
-XX:+UseAdaptiveSizePolicy: 设置此选项后,并行收集器会自动选择年轻代区大小和相应的 Survivor 区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
响应时间优先的并发收集器
如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
典型配置:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC: 设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4 的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn 设置。
-XX:+UseParNewGC: 设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction: 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次 GC 以后对内存空间进行压缩、整理。 -XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片
G1
Garbage First 介绍
Garbage First 简称 G1,它的目标是要做到尽量减少 GC 所导致的应用暂停的时间,让应用达到准实时的效果,同时保持 JVM 堆空间的利用率,其最大的特色在于允许指定在某个时间段内 GC 所导致的应用暂停的时间最大为多少,例如在 100 秒内最多允许 GC 导致的应用暂停时间为 1 秒,这个特性对于准实时响应的系统而言非常的吸引人,这样就再也不用担心系统突然会暂停个两三秒了。
目标
从设计目标看 G1 完全是为了大型应用而准备的。
支持很大的堆
高吞吐量
- 支持多 CPU 和垃圾回收线程
- 在主线程暂停的情况下,使用并行收集
- 在主线程运行的情况下,使用并发收集
实时目标:可配置在 N 毫秒内最多只占用 M 毫秒的时间进行垃圾回收 当然 G1 要达到实时性的要求,相对传统的分代回收算法,在性能上会有一些损失。
算法详解
G1 可谓博采众家之长,力求到达一种完美。他吸取了增量收集优点,把整个堆划分为一个一个等大小的区域(region)。内存的回收和划分都以 region 为单位;同时,他也吸取了 CMS 的特点,把这个垃圾回收过程分为几个阶段,分散一个垃圾回收过程;而且,G1 也认同分代垃圾回收的思想,认为不同对象的生命周期不同,可以采取不同收集方式,因此,它也支持分代的垃圾回收。
为了达到对回收时间的可预计性,G1 在扫描了 region 以后,对其中的活跃对象的大小进行排序,首先会收集那些活跃对象小的 region,以便快速回收空间(要复制的活跃对象少了),因为活跃对象小,里面可以认为多数都是垃圾,所以这种方式被称为 Garbage First(G1)的垃圾回收算法,即:垃圾优先的回收。
回收步骤:
1. 初始标记(Initial Marking)
G1 对于每个 region 都保存了两个标识用的 bitmap,一个为 previous marking bitmap,一个为 next marking bitmap,bitmap 中包含了一个 bit 的地址信息来指向对象的起始点。
开始 Initial Marking 之前,首先并发的清空 next marking bitmap,然后停止所有应用线程,并扫描标识出每个 region 中 root 可直接访问到的对象,将 region 中 top 的值放入 next top at mark start(TAMS)中,之后恢复所有应用线程。
触发这个步骤执行的条件为:
G1 定义了一个 JVM Heap 大小的百分比的阀值,称为 h,另外还有一个 H,H 的值为(1-h)Heap Size,目前这个 h 的值是固定的,后续 G1 也许会将其改为动态的,根据 jvm 的运行情况来动态的调整,在分代方式下,G1 还定义了一个 u 以及 soft limit,soft limit 的值为 H-uHeap Size,当 Heap 中使用的内存超过了 soft limit 值时,就会在一次 clean up 执行完毕后在应用允许的 GC 暂停时间范围内尽快的执行此步骤;
在 pure 方式下,G1 将 marking 与 clean up 组成一个环,以便 clean up 能充分的使用 marking 的信息,当 clean up 开始回收时,首先回收能够带来最多内存空间的 regions,当经过多次的 clean up,回收到没多少空间的 regions 时,G1 重新初始化一个新的 marking 与 clean up 构成的环。
2.并发标记(Concurrent Marking)
按照之前 Initial Marking 扫描到的对象进行遍历,以识别这些对象的下层对象的活跃状态,对于在此期间应用线程并发修改的对象的以来关系则记录到 remembered set logs 中,新创建的对象则放入比 top 值更高的地址区间中,这些新创建的对象默认状态即为活跃的,同时修改 top 值。
3.最终标记暂停(Final Marking Pause)
当应用线程的 remembered set logs 未满时,是不会放入 filled RS buffers 中的,在这样的情况下,这些 remebered set logs 中记录的 card 的修改就会被更新了,因此需要这一步,这一步要做的就是把应用线程中存在的 remembered set logs 的内容进行处理,并相应的修改 remembered sets,这一步需要暂停应用,并行的运行。
4.存活对象计算及清除(Live Data Counting and Cleanup)
值得注意的是,在 G1 中,并不是说 Final Marking Pause 执行完了,就肯定执行 Cleanup 这步的,由于这步需要暂停应用,G1 为了能够达到准实时的要求,需要根据用户指定的最大的 GC 造成的暂停时间来合理的规划什么时候执行 Cleanup,另外还有几种情况也是会触发这个步骤的执行的:
G1 采用的是复制方法来进行收集,必须保证每次的”to space”的空间都是够的,因此 G1 采取的策略是当已经使用的内存空间达到了 H 时,就执行 Cleanup 这个步骤;
对于 full-young 和 partially-young 的分代模式的 G1 而言,则还有情况会触发 Cleanup 的执行,full-young 模式下,G1 根据应用可接受的暂停时间、回收 young regions 需要消耗的时间来估算出一个 yound regions 的数量值,当 JVM 中分配对象的 young regions 的数量达到此值时,Cleanup 就会执行;partially-young 模式下,则会尽量频繁的在应用可接受的暂停时间范围内执行 Cleanup,并最大限度的去执行 non-young regions 的 Cleanup。
JVM 参数
类加载
类加载过程
类加载原理
类加载器
我们无法获得引导类加载器,因为它是使用 c 实现的,而且使用引导类加载器加载的类通过 getClassLoader 方法返回的是 null. 所以无法直接操作引导类加载器,但是我们可以根据 Class.getClassLoader 方法是否为 null 判断这个类是不是引导类加载器加载的;但是我们可以通过下面的方法获得经由“引导类加载器”加载的类的路径(值得注意的是:每个 jar 包对应了一个 URL)。
public class ClassLoaderTest {
public static void main(String[] args) {
List<URL> list = Arrays.asList(sun.misc.Launcher.getBootstrapClassPath().getURLs());
for(URL url : list){
System.out.println(url.toString());
}
}
}
Output:
file:/D:/Program%20Files/Java/jdk1.6.0_13/jre/lib/resources.jar
file:/D:/Program%20Files/Java/jdk1.6.0_13/jre/lib/rt.jar
file:/D:/Program%20Files/Java/jdk1.6.0_13/jre/lib/sunrsasign.jar
file:/D:/Program%20Files/Java/jdk1.6.0_13/jre/lib/jsse.jar
file:/D:/Program%20Files/Java/jdk1.6.0_13/jre/lib/jce.jar
file:/D:/Program%20Files/Java/jdk1.6.0_13/jre/lib/charsets.jar
file:/D:/Program%20Files/Java/jdk1.6.0_13/jre/classes
从这个例子中我们可以看出 Bootstrap ClassLoader 加载的为$JAVA_HOME/jre/lib 目录下的 jar 包。
Bootstrap ClassLoader、Extension ClassLoader、App ClassLoader 三者的关系如下:Bootstrap ClassLoader 由 JVM 启动,然后初始化 sun.misc.Launcher,sun.misc.Launcher 初始化 Extension ClassLoader、App ClassLoader。Bootstrap ClassLoader 是 Extension ClassLoader 的 parent,Extension ClassLoader 是 App ClassLoader 的 parent。
但是这并不是继承关系,只是语义上的定义,基本上,每一个 ClassLoader 实现,都有一个 Parent ClassLoader。可以通过 ClassLoader 的 getParent 方法得到当前 ClassLoader 的 parent。Bootstrap ClassLoader 比较特殊,因为它不是 java class 所以 Extension ClassLoader 的 getParent 方法返回的是 NULL。我们举下面的实例说明一下:
public class ClassLoaderTest2 {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println("current loader---->"+loader);
System.out.println("parent loader-->"+loader.getParent());
System.out.println("grandparent loader->"+loader.getParent().getParent());
}
}
Output:
current loader---->sun.misc.Launcher$AppClassLoader@19821f
parent loader---->sun.misc.Launcher$ExtClassLoader@addbf1
grandparent loader---->null
了解了 ClassLoader 的原理和流程以后,我们可以试试自定义 ClassLoader。关于自定义 ClassLoader: 由于一些特殊的需求,我们可能需要定制 ClassLoader 的加载行为,这时候就需要自定义 ClassLoader 了。
自定义 ClassLoader 需要继承 ClassLoader 抽象类,重写 findClass 方法,这个方法定义了 ClassLoader 查找 class 的方式。
主要可以扩展的方法有:
- findClass 定义查找 Class 的方式
- defineClass 将类文件字节码加载为 jvm 中的 class
- findResource 定义查找资源的方式
如果嫌麻烦的话,我们可以直接使用或继承已有的 ClassLoader 实现,比如 java.net.URLClassLoader java.security.SecureClassLoader java.rmi.server.RMIClassLoader sun.applet.AppletClassLoader Extension ClassLoader 和 App ClassLoader 都是 java.net.URLClassLoader 的子类。
这个是 URLClassLoader 的构造方法: public URLClassLoader(URL[] urls, ClassLoader parent) public URLClassLoader(URL[] urls) urls 参数是需要加载的 ClassPath url 数组,可以指定 parent ClassLoader,不指定的话默认以当前调用类的 ClassLoader 为 parent。下面以一个例子加以说明: Java 代码 1:
public class ClassWillBeLoaded {//这个类是要被装载的一个类(测试类).
public static void main(String[] args) {
ClassWillBeLoaded obj = new ClassWillBeLoaded();
}
public String doTask(String str1,String str2){
return str1+" "+str2;
}
public class ClassLoaderTest3 {//使用反射机制调用通过 URLClassLoader 装载的类中的 doTask 方法
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException, InstantiationException {
URL url = new URL("file:D:/share/ClassLoadTest.jar");
URL[] urls = {url};
ClassLoader classLoader = new URLClassLoader(urls);
Thread.currentThread().setContextClassLoader(classLoader);//设置该线程的上下文 ClassLoader
Class clazz=classLoader.loadClass("classLoader.ClassWillBeLoaded");//使用 loadClass 方法加载 class,这个 class 是在 urls 参数指定的 classpath 下边
Method taskMethod = clazz.getMethod("doTask", String.class, String.class);//然后我们就可以用反射做些事情了
Object returnValue = taskMethod.invoke(clazz.newInstance(),"test","success");
System.out.println((String)returnValue);
} }
要哪一个 class loader 加载呢?答案在于全盘负责委托机制,这是出于安全的原因。每次只要一个 class 被 loaded,系统的 class loader 就首先被调用。然而它不会立即去 load 这个这个类。取而代之的是,他会把这个 task 委托给他的 parent class loader,也就是 extension class loader;同样的,extension class loader 也会委托给它的 parent class loader 也就是 bootstrap class loader。
因此,bootstrap class loader 总是被给第一个去 load class 的机会。如果 bootstrap class loader 找不到类的话,那么 extension class loader 将会 load,如果 extension class loader 也没有找到对应的类的话,system class loader 将会执行这个 task,如果 system class loader 也没有找到的话,java.lang.ClassNotFoundException 将会被抛出。另外一个原因是避免了重复加载类,每一次都是从底向上检查类是否已经被加载,然后从顶向下加载类,保证每一个类只被加载一次。
加载过程中会先检查类是否被已加载,检查顺序是自底向上,从 Custom ClassLoader 到 BootStrap ClassLoader 逐层检查,只要某个 classloader 已加载就视为已加载此类,保证此类只被所有 ClassLoader 加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
Java Class 文件格式解析
VisualVM
VisualVM 提供在运行的 Java 应用程序的详细信息。在 VisualVM 的图形用户界面中可以方便、快捷地查看多个 Java 应用程序的相关信息。因为 JDK 自带该工具,且属于免费软件,我们对 java 应用程序进行简单的监控分析时直接用该工具,当然如果有更复杂、更专业的监控分析需求,则最好选择商用软件。
使用
JDK1.6u7 以后版本已携带该工具,如果你安装的 JDK 并未携带该工具,读者可从 https://visualvm.java.net 下载,直接终端中输入 jvisualvm 回车或进入 jdk 的 bin 目录后输入命令即可打开,打开界面如下所示:
在左侧的“应用程序”窗口中,可以快速查看本地和远程 JVM 上运行的 Java 应用程序。接下来我们比照命令行工具简单操作一下。
可查看 JVM 进程及进程配置、环境等相关信息,功能类 jps、jinfo 命令行。 这里我们就看 viaualvm 的一些信息,应用程序-本地栏下出现一个 ViaualVM 的节点,我们双击这个节点,进入以下界面:
从该界面概述选项卡里我们可以查看进程 pid、JVM 参数等信息。
可查看应用程序内存、CPU、堆、方法区、线程等信息,功能类 jstat、jstack。
我们点击“监视”、“线程”选项卡可直观地查看 CPU、内存、类、线程、垃圾回收情况等信息。如下图所示:
生成 Dump、分析 Dump、生成快照等,功能类 jmap、jhat。
右键单击应用程序节点将打开弹出式菜单,从该弹出式菜单中可以生成线程 dump 或堆 dump。生成 dump 将扩展到应用程序节点下,如下两张图所示:
其它功能及功能扩展。
我们可以在应用程序的 Profiler 选项卡下对 cup 和内存的性能进行分析。
VisualVM 还可以很方便地扩展功能,大家可以点击工具菜单,进入插件界面,点击可用插件,然后就可以对其功能进行扩展了,如下图所示:
这里我们下载安装 Visual GC。下载安装成功后重新打开应用程序节点 VisualVM,我们可以看到界面中多了一个 Visual GC 的选项卡,打开后入下图所示:
Memory Analyzer
案例分析
一些案例
系统频繁 Full gc 问题分析及解决办法
频繁的 Full GC(完全垃圾回收)是 Java 程序中常见的性能问题之一 。当发生 Full GC 时,JVM 会暂停所有的应用线程,进行较为彻底的垃圾回收,通常需要较长的时间,会导致响应延迟增加,严重时影响整个系统的吞吐量和可用性。
问题分析:
Full GC 频繁的原因可以通过多种因素引起,通常需要从以下几个方面来进行分析:
- 堆内存配置不合理
- 堆内存过小 :JVM 的堆内存太小,不能容纳应用的对象,导致垃圾回收频繁。
- 堆内存过大 :虽然堆内存过大可能减少垃圾回收频率,但这可能会导致 Full GC 的时间更长,特别是当堆内存过大时,Full GC 需要更多时间来进行内存整理和压缩。
- 垃圾回收器选择不合适
- 不同的垃圾回收器(如
Serial GC
、Parallel GC
、G1 GC
、ZGC
等)在不同的场景下有不同的表现。如果选择了不合适的垃圾回收器,可能会导致 Full GC 频繁发生。
- 内存泄漏
- 内存泄漏 是指程序不再使用的对象未被垃圾回收器回收,导致堆内存不断增长,最终触发 Full GC。常见的内存泄漏包括长时间持有不必要的对象引用、静态集合、缓存未清理等。
- 对象的生命周期过长
- 如果很多对象的生命周期较长,或者存在过度分配的对象,可能会导致垃圾回收频繁发生。特别是在年轻代(Young Generation)存活的对象数量增加时,容易导致 Full GC。
- 老年代(Old Generation)空间不足
- 如果老年代(Old Generation)空间不足,垃圾回收会尝试回收老年代中的对象,触发 Full GC。老年代满了时,JVM 会进行 Full GC,以清理一些不再使用的对象。
- Young Generation 收集不够及时
- 在使用如 Parallel GC 时,Young Generation 的垃圾回收效率低,导致大量对象晋升到 Old Generation,最终引发 Full GC。
解决方法:
- 调整堆内存配置:
- 增加堆内存大小 :确保堆内存大小适当。如果堆内存太小,可以增加堆的大小,减少频繁的 GC。你可以通过设置
-Xms
(初始堆内存大小)和-Xmx
(最大堆内存大小)来调整堆的大小。例如:bash java -Xms4g -Xmx8g -jar your-application.jar
- 合理调整年轻代(Young Generation)大小 :如果年轻代太小,垃圾回收会频繁发生,可以通过调整
-Xmn
来设置年轻代大小。通常,年轻代的大小设置为堆内存的 1/3 或 1/4 是比较合理的。
- 选择合适的垃圾回收器:
- 根据应用的需求,选择合适的垃圾回收器。例如:
- G1 GC :对于大规模堆内存(特别是 8GB 以上的堆)和低延迟要求的应用,
G1 GC
是一个较好的选择。bash java -XX:+UseG1GC -Xms4g -Xmx8g -jar your-application.jar
- ZGC 或 Shenandoah GC :这两种垃圾回收器专为低延迟应用设计,适用于大内存(超过 16GB)的场景,且具有较短的停顿时间。
bash java -XX:+UseZGC -Xms4g -Xmx8g -jar your-application.jar
- Parallel GC :适合批处理型任务,吞吐量高,但可能会有较长的暂停。
bash java -XX:+UseParallelGC -Xms4g -Xmx8g -jar your-application.jar
- G1 GC :对于大规模堆内存(特别是 8GB 以上的堆)和低延迟要求的应用,
- 避免内存泄漏:
- 监控对象引用 :确保没有不必要的对象引用保持在内存中,特别是长时间存活的对象(如静态引用、缓存、单例等)。可以通过工具如
JVisualVM
、YourKit
或Eclipse MAT
来检测内存泄漏。 - 使用对象池 :在需要频繁创建和销毁对象的地方使用对象池,以减少 GC 压力。
- 优化对象的生命周期:
- 确保应用尽量避免长时间持有短生命周期对象的引用,尽量让年轻代中的对象尽快被回收。
- 在一些场景下,可以使用弱引用(
WeakReference
)或软引用(SoftReference
)来允许垃圾回收器及时回收不再使用的对象。
- 调整老年代(Old Generation)的大小:
- 如果老年代空间不足,垃圾回收会在 Full GC 中做更多的工作。可以通过调整
-XX:NewRatio
(年轻代与老年代的比例)或-XX:MaxTenuringThreshold
(对象晋升到老年代的阈值)来优化老年代的空间使用。例如:bash java -XX:NewRatio=3 -XX:MaxTenuringThreshold=15 -Xms4g -Xmx8g -jar your-application.jar
- 避免过度依赖 Full GC:
- 尽量减少依赖
Full GC
来清理内存的情况。如果是因为内存不足而频繁触发Full GC
,可以通过增加内存、优化内存使用来减少Full GC
的触发。
- 增加垃圾回收日志:
- 启用垃圾回收日志来分析频繁发生的 Full GC 和其原因,可以通过以下参数开启:
bash java -Xlog:gc* -Xms4g -Xmx8g -jar your-application.jar
或者对于老版本的 JVM 使用:bash java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xms4g -Xmx8g -jar your-application.jar
- 通过分析这些日志,检查 GC 的行为,特别是 Full GC 的触发频率和原因。
结论:
频繁的 Full GC 会严重影响应用性能,因此需要通过合理的内存配置、选择合适的垃圾回收器、避免内存泄漏和优化对象生命周期来减少其发生。通过调整 JVM 参数、分析垃圾回收日志,能够更好地理解垃圾回收的行为,从而进行有效优化。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
下一篇: Org mode 中的代码块
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论