内存分配实战和常用Java调优工具
实战:内存分配与回收策略
对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
-Xms20M -Xmx20M参数限制Java堆大小为20M不可扩展;-Xmn10M参数将10M分配给新生代,剩下的10M分配给老年代;-XX:SurvivorRatio=8将新生代Eden区与一个Survivor区的空间比例设置为8:1,新生代总的可用空间为9216K(Eden区 1个Survivor区的容量)
在执行allocation4的初始化时将会触发一次MinorGC,产生的原因是:前面allocation1、allocation2、allocation3已经占用了6M的空间,Eden区已经没有足够的空间分配给allocation4,因此需要将allocation1、allocation2、allocation3尝试移动到Survivor区,而Survivor区空间明显不够,只能通过分配担保机制提前转移到老年代中
MinorGC后,4M的allocation4对象顺利分配在Eden中,因此程序执行完的结果是Eden占用4M,Survivor空闲,老年代被占用6M
代码语言:java复制/**
* 对象优先在Eden分配
* {@link 《深入理解Java虚拟机》第三版 代码清单3-7}
* VM Args:-Xms20M -Xmx20M -Xmn10M -XX: PrintGCDetails -XX:SurvivorRatio=8
*/
public class EdenAllocation {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1 = new byte[2 * _1MB];
byte[] allocation2 = new byte[2 * _1MB];
byte[] allocation3 = new byte[2 * _1MB];
// 去掉注释后,需要在Eden区分配4M给allocation4,Eden区空间不足,触发MinorGC
// byte[] allocation4 = new byte[4 * _1MB];
}
}
大对象直接进入老年代
在选用Serial和ParNew作为新生代收集器时,使用参数-XX:PretenureSizeThreshold,指定大于设置值的对象直接在老年代分配,以避免在Eden区以及Survivor区之间因为垃圾回收产生的来回复制
大对象是指需要大量连续内存空间的Java对象,最典型的就是长字符串和大数组;程序中存在大对象,会导致在给大对象分配内存空间时提前触发GC或频繁触发GC,并且大对象在移动时,也意味着高额的内存复制开销
代码语言:java复制/**
* 大对象直接进入老年代
* {@link 《深入理解Java虚拟机》第三版 代码清单3-8}
* VM Args:-Xms20M -Xmx20M -Xmn10M -XX: PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728
*/
public class BigObjAllocation {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1 = new byte[4 * _1MB];
}
}
长期存活的对象将进入老年代
对象通常在Eden区诞生(年龄为0),如果经过一次MinorGC仍然存活,并且能被Survivor容纳的话,该对象就会被移动到Survivor空间中,并且将其对象的年龄增加1岁,当它的年龄增加到一定程度后就会被晋升到老年代中
通过参数-XX:MaxTenuringThreshold设置当对象年龄到达设置值时,即晋升老年代
代码语言:java复制/**
* 长期存活的对象将进入老年代
* {@link 《深入理解Java虚拟机》第三版 代码清单3-9}
* VM Args:-Xms20M -Xmx20M -Xmn10M -XX: PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX: PrintTenuringDistribution
*/
public class AliveObjLong {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1 = new byte[_1MB /4];
byte[] allocation2 = new byte[4 * _1MB];
byte[] allocation3 = new byte[4 * _1MB];
allocation3 = null;
allocation3 = new byte[4 * _1MB];
}
}
动态对象年龄判定
为了能更好的适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
空间分配担保
在发生MinorGC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这次的MinorGC可以确保是安全的;如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure的参数设置,看是否允许担保失败;如果允许,则继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次MinorGC;如果小于或者设置了不允许担保,这时会改进为进行一次FullGC
常用Java调优工具
jps
虚拟机进程状况工具
代码语言:shell复制jps [options] [hostid]
选项 | 作用 |
---|---|
-q | 输出LVMID |
-m | 输出main()的启动的参数 |
-l | 输出主类全名或Jar路径 |
-v | 输出启动时的JVM参数 |
jstat
虚拟机各种运行状态信息及监视工具
代码语言:shell复制jstat [option vmid [interval[s|ms] [count] ] ]
选项 | 作用 |
---|---|
-class | 监视类加载、卸载数量、总空间以及类加载所耗费的时间 |
-gc、-gcnew、-gcold | 监视Java堆状况 |
-gccapacity、-gcnewcapacity、-gcoldcapacity | 监视Java堆状况,主要关注Java堆各个区域使用到的最大、最小空间
-gcutil | 监视Java堆状况,主要关注已使用空间的占比
-gccause | 监视Java堆状况,主要关注已使用空间的占比,还额外输出上一次垃圾收集产生的原因j |
jstack
Java堆栈跟踪工具,用于生成虚拟机当前时刻的线程快照
代码语言:shell复制jstack [option] vmid
选项 | 作用 |
---|---|
-F | 当正常输出的请求不被响应时,强制输出线程堆栈 |
-l | 除堆栈外,显示关于锁的附加信息 |
JHDSB
基于服务性代理的调试工具
代码语言:java复制/**
* JHDSB工具实用
* {@link 《深入理解Java虚拟机》第三版 代码清单4-6}
* VM Args:-Xmx10m -XX: UseSerialGC -XX: UseCompressedOops
*/
public class Jhsdb {
static class Test {
static ObjectHolder staticObj = new ObjectHolder();
ObjectHolder instanceObj = new ObjectHolder();
void foo() {
ObjectHolder localObj = new ObjectHolder();
System.out.println("done"); // 断点处
}
}
private static class ObjectHolder{};
public static void main(String[] args) {
Test test = new Jhsdb.Test();
test.foo();
}
}
实例操作步骤:
- jps查询到当前程序的进程ID
- jhsdb hsdb --pid vmid;进入JHSDB图形化模式
- scanoops 0x00007f32c7800000 0x00007f32c7b500000 Jhsdb$ObjectHolder;在新生代范围查找ObjectHolder实例
- revptrs 0x00007f32c7a7c458;查找引用实例的位置
JConsole
Java监视和管理控制台
代码语言:java复制/**
* JConsole监视演示代码
* {@link 《深入理解Java虚拟机》第三版 代码清单4-7}
* VM Args:-Xms100m -Xmx100m
*/
public class JConsoleMemory {
static class OOMObject {
public byte[] placeholder = new byte[64 * 1024];
}
public static void fillHeap(int num) throws InterruptedException {
List<OOMObject> list = new ArrayList<>();
for (int i = 0; i < num; i ) {
// 以64kb/50ms的速度往堆中填充数据,填充1000次
Thread.sleep(50);
list.add(new OOMObject());
}
System.gc();
}
public static void main(String[] args) throws Exception {
fillHeap(1000);
}
}
代码语言:java复制/**
* JConsole线程等待演示代码
* {@link 《深入理解Java虚拟机》第三版 代码清单4-8}
* VM Args:-Xms100m -Xmx100m -XX: UseSeriGC
*/
public class JConsoleThread {
public static void createBusyThread() {
Thread thread = new Thread(() -> {
while (true);
}, "testBusyThread");
thread.start();
}
public static void createLockThread(final Object lock) {
Thread thread = new Thread(() -> {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "testLockThread");
thread.start();
}
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
br.readLine();
createBusyThread();
br.readLine();
Object obj = new Object();
createLockThread(obj);
}
}
代码语言:java复制/**
* JConsole线程死锁演示代码
* {@link 《深入理解Java虚拟机》第三版 代码清单4-9}
* VM Args:-Xms100m -Xmx100m
*/
public class JConsoleDeadThread {
static class SynAddRunable implements Runnable {
int a, b;
public SynAddRunable(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
synchronized (Integer.valueOf(a)) {
synchronized (Integer.valueOf(b)) {
System.out.println(a b);
}
}
}
}
public static void main(String[] args) throws Exception {
for (int i = 0; i < 100; i ) {
new Thread(new SynAddRunable(1, 2)).start();
new Thread(new SynAddRunable(2, 1)).start();
}
}
}