jvisualvm
给一个heap out of memory的例子:
public class TestHeapSpace {
public static void main(String[] args) {
List<TestHeapSpace> list = new ArrayList<>();
for(;;){
list.add(new TestHeapSpace());
}
}
}
list中一直放入对象,终有一天会堆溢出。
为了让效果明显,我们减少堆内存的容量:
最大和最小都配成5m,这样就不会有伸缩空间了。同时,要求将堆溢出错误的文件导出。
在cmd窗口输入jvisualvm
,把导出的hprof
文件装入。
在这上面我们很容易看到为何会堆溢出。
for(;;){
list.add(new TestHeapSpace());
System.gc();
}
若我们催促gc进行回收,可以看到一直没有报堆溢出的错误。
打开抽样器:
我们看到,TestHeapSpace
的实例还是在一直增加,因为它是无法被回收的(list一直存在着),但是jvm创建对象所使用的其他对象就可以被回收(比如字节数组)。
jconsole
写一个栈溢出的例子:
package com.ocean.jvmtools;
import java.util.concurrent.TimeUnit;
public class TestStackMemory {
private int length;
public int getLength() {
return length;
}
public void test() {
length++;
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
test();
}
public static void main(String[] args) {
TestStackMemory testStackMemory = new TestStackMemory();
try {
testStackMemory.test();
} catch (Throwable throwable) {
System.out.println(testStackMemory.getLength());
throwable.printStackTrace();
}finally {
}
}
}
这是一个递归调用的例子,一个方法就会对应一个栈帧,所以栈一定会被占满。
我们给栈100k的大小,以此更快溢出。
为了能够让程序活得久一点,我们在test方法中加入睡眠,这就给我们争取时间来打开分析工具了。
报错后,我们会打印length的值。
这次我们在cmd控制台输入jconsole
,用它来连接程序。
看一下线程的标签:
这里有我们的main
线程,有jvm的reference handler
线程以及finalizer
线程,看名字就知道,前者用于处理引用,后者是垃圾回收器的线程。
还有jconsole自己的tcp线程。
main
线程的状态是TIMED_WAITING
。
线程一共有六种状态,TIMED_WAITING
是因为我们调用了sleep方法。
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
死锁
比如有两个熊孩子吵架,A对B说:“你先把笔还给我我再还你的尺子”。B对A说:“你先还我的尺子我再还你的笔”。于是他们就这样僵持着。这就是死锁。
给一段代码:
package com.ocean.jvmtools;
import sun.security.jgss.LoginConfigImpl;
import java.util.concurrent.TimeUnit;
public class DeadLock {
private static final Object LOCK1 = new Object();
private static final Object LOCK2 = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (LOCK1){
System.out.println(Thread.currentThread().getName()+" starts...");
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK2){
System.out.println(Thread.currentThread().getName()+" ends...");
}
}
},"A").start();
new Thread(()->{
synchronized (LOCK2){
System.out.println(Thread.currentThread().getName()+" starts...");
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK1){
System.out.println(Thread.currentThread().getName()+" ends...");
}
}
},"B").start();
}
}
线程A和线程B分别拿到了LOCK1和LOCK2。它们同时睡了500毫秒,然后都向对方索要锁,但是自己又不释放锁。
所以,程序卡住了。
打开jconsole观察:
线程A和B的状态都是BLOCKED
,并且都试图获取对方已经持有的锁。
jconsole专门有死锁检测工具:
一点击就能检测到。
当然,你也可以用命令行的工具,比如jstack
。
jstack -l pid
这是也有详细的堆栈信息。
元空间溢出
jdk1.8元空间的默认大小是21m,到达这个大小后会进行垃圾回收,如果类的持续产生还是超过了21m,那他就会自动扩容。元空间的内存使用的是计算机的内存,会一直扩容下去。
因此我们要限制元空间的大小,来看到内存溢出。
为了一直产生类,我们使用cgilib:
```java
package com.ocean.jvmtools;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
public class TestMetaSpace {
public static void main(String[] args) {
while(true){
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TestMetaSpace.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor)(obj, method, arg1, proxy)->
proxy.invokeSuper(obj,arg1));
enhancer.create();
System.out.println("class is created!");
}
}
}
我们在运行过程中动态地产生TestMetaSpace
这个类的子类,子类的信息会存入元空间。
我们限定元空间最大为100m,然后用TraceClassLoading
来跟踪类的创建。
用jconsole可以看到类的数量一直在增加:
最后控制台报的错:
jcmd
我们还是拿TestMetaSpace这个例子,并且介绍jcmd
这个工具。
通过jps
命令我们拿到TestMetaSpace的进程id(7860),然后输入jcmd 7860 help
。
它这里面有很多选项,可以一个个玩。
比如用jcmd 7860 VM.flags
可以看到虚拟机的启动参数:
使用jcmd 7860 GC.class_histogram
可以看到当前的类信息。