一个java程序对应一个进程 一个进程对应一个jvm实例 一个jvm实例中只有一个运行时数据区 一个运行时数据区只有一个方法区和堆 一个进程中的多个线程需要共享同一个方法区和堆空间 每个线程拥有独立的一套程序计数器,本地方法栈,虚拟机栈
代码示例
JDK 自带的工具:Java VisualVM ,来查看堆内存
JVM参数
GC查看:四者相加等于10m
代码示例
public class SimpleHeap {
private int id;//属性、成员变量
public SimpleHeap(int id) {
this.id = id;
}
public void show() {
System.out.println("My ID is " + id);
}
public static void main(String[] args) {
SimpleHeap sl = new SimpleHeap(1);
SimpleHeap s2 = new SimpleHeap(2);
int[] arr = new int[10];
Object[] arr1 = new Object[10];
}
}
字节码文件
现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:
Java7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区
Java8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间
约定:新生区 <–> 新生代 <–> 年轻代 、 养老区 <–> 老年区 <–> 老年代、 永久区 <–> 永久代
新生代又细分为伊甸园区、幸存者区。
-XX:+PrintGCDetails
代码示例
/**
* 1. 设置堆空间大小的参数
* -Xms 用来设置堆空间(新生代+老年代)的初始内存大小
* -X 是jvm的运行参数
* ms 是memory start
* -Xmx 用来设置堆空间(新生代+老年代)的最大内存大小
*
* 2. 默认堆空间的大小
* 初始内存大小:物理电脑内存大小 / 64
* 最大内存大小:物理电脑内存大小 / 4
*
* 3. 手动设置:-Xms600m -Xmx600m
* 开发中建议将初始堆内存和最大的堆内存设置成相同的值。
*
* 4. 查看设置的参数:方式一: jps / jstat -gc 进程id
* 方式二:-XX:+PrintGCDetails
*/
public class HeapSpaceInitial {
public static void main(String[] args) {
//返回Java虚拟机中的堆内存总量
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
//返回Java虚拟机试图使用的最大堆内存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms : " + initialMemory + "M");
System.out.println("-Xmx : " + maxMemory + "M");
}
}
设置堆初始化空间和最大空间
-Xms600m -Xmx600m
执行结果: -Xms : 580M -Xmx : 580M
两种查看堆内存的方式
总空间计算:409600 + 163840 + 20480 + 20480 = 614400;614400/1024 = 600
Java虚拟机中的堆内存总量为580?
因为幸存者0区和1区只能有一个存放对象,也就是说必须有一个是空的,所以只计算一个幸存者区
虚拟机堆空间计算:409600 + 163840 + 20480 = 593920;593920/1024 = 580
设置VM options
-Xms600m -Xmx600m -XX:+PrintGCDetails
返回结果
代码示例
/**
* 设置虚拟机参数:-Xms600m -Xmx600m
*/
public class OOMTest {
public static void main(String[] args) {
ArrayList<Picture> list = new ArrayList<>();
while(true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(new Picture(new Random().nextInt(1024 * 1024)));
}
}
}
堆内存变化图
Old 区域一点一点在变大,直到最后一次垃圾回收器无法回收垃圾时,堆内存被撑爆,抛出 OutOfMemoryError 错误
执行结果
大对象导致的堆内存溢出
下面这参数开发中一般不会调
配置新生代与老年代在堆结构的占比
配置伊甸园区和幸存者区在堆结构的占比
代码示例
/**
* -Xms600m -Xmx600m
*
* -XX:NewRatio : 设置新生代与老年代的比例。默认值是2.
* -XX:SurvivorRatio :设置新生代中Eden区与Survivor区的比例。默认值是8
* -XX:-UseAdaptiveSizePolicy :关闭自适应的内存分配策略 (暂时用不到)
* -Xmn:设置新生代的空间的大小。 (一般不设置)
*
*/
public class EdenSurvivorTest {
public static void main(String[] args) {
System.out.println("我只是来打个酱油~");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
通过命令行查看:
通过Java VisualVM查看:
代码示例
/**
* -Xms600m -Xmx600m
*/
public class HeapInstanceTest {
byte[] buffer = new byte[new Random().nextInt(1024 * 200)];
public static void main(String[] args) {
ArrayList<HeapInstanceTest> list = new ArrayList<HeapInstanceTest>();
while (true) {
list.add(new HeapInstanceTest());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
注意【伊甸园区、幸存者区、老年区】的内存变化趋势
JVM在进行GC时,并非每次都对上面三个内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。 针对HotSpot VM的实现,它里面的GC按照回收区域又分为两个种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)
年轻代GC(Minor GC / Young GC)触发机制
图解垃圾回收
老年代GC(Major GC / Full GC)触发机制
Full GC触发机制(后面细讲 )
full gc是开发或者调优中尽量要避免的。这样用户线程暂停的时间会短一些
代码示例
/**
* 测试MinorGC 、 MajorGC、FullGC
* VM参数:-Xms9m -Xmx9m -XX:+PrintGCDetails
*
*/
public class GCTest {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "atguigu.com";
while (true) {
list.add(a);
a = a + a;
i++;
}
} catch (Throwable t) {
t.printStackTrace();
System.out.println("遍历次数为:" + i);
}
}
}
执行结果
为什么需要把java堆分代?不分代就不能正常工作了吗?
内存分配策略或对象提升(Promotion)规则
针对不同年龄段的对象分配原则如下所示:
代码示例
/**
* 测试:大对象直接进入老年代
* VM参数:-Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails
*/
public class YoungOldAreaTest {
public static void main(String[] args) {
byte[] buffer = new byte[1024 * 1024 * 20];//20m
}
}
执行结果
年轻代占比3/1,伊甸园占比10/8,所以Eden16m,老年代40m;数组20m,Eden放不下而且整个空间小于20m,所以没有执行GC,直接放入老年代。
为什么有TLAB(Thread Local Allocation Buffer)?
什么是 TLAB
内存图:
单个线程对象分配空间图解过程
TLAB中分配对象失败(空间不足),则在Eden中加锁并且为对象分配空间,防止其他线程操作此空间
常用参数设置
-XX:HandlePromotionFalilure 设置空间分配担保
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。 1)如果大于,则此次Minor GC是安全的 2)如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允担保失败。 a.如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。 I.如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的; II.如果小于,则进行一次Full GC。 b.如果HandlePromotionFailure=false,则进行一次Full GC。
JDK7以后,此参数不可设置,默认为true
堆是分配对象存储的唯一选择吗?
逃逸分析概述
逃逸分析参数设置
代码示例
public void my_method() {
V v = new V();
// use v
// ....
v = null;
}
public static StringBuffer createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
如果想要StringBuffer sb不发生逃逸,可以这样写
public static String createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
代码示例
/**
* 栈上分配测试
* VM参数:-server -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
*/
public class StackAllocation {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();
}
// 查看执行时间
long end = System.currentTimeMillis();
System.out.println("花费的时间为: " + (end - start) + " ms");
// 为了方便查看堆内存中对象个数,线程sleep
try {
Thread.sleep(1000000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
private static void alloc() {
User user = new User();//未发生逃逸
}
static class User {
}
}
结论:开启逃逸分析后,应该就是只有标量替换生效,而没有使用栈上分配
如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
代码示例
public void f() {
Object hellis = new Object();
synchronized(hellis) {
System.out.println(hellis);
}
}
以上代码,hellis作为锁,每个线程进入f()方法,都会创建Object()对象,此时每个线程都能进入同步方法,锁已无意义,所以JIT编译器会将同步过程消除成为以下代码
public void f() {
Object hellis = new Object();
System.out.println(hellis);
}
字节码文件中并没有进行优化,加锁和释放锁的操作依然存在,同步省略操作是在解释运行时发生的
代码示例
public static void main(String args[]) {
alloc();
}
class Point {
private int x;
private int y;
}
private static void alloc() {
Point point = new Point(1,2);
System.out.println("point.x" + point.x + ";point.y" + point.y);
}
以上代码,经过标量替换后,就会变成
private static void alloc() {
int x = 1;
int y = 2;
System.out.println("point.x = " + x + "; point.y=" + y);
}
结论: 1.可以看到,Point这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个聚合量了。 2.那么标量替换有什么好处呢?就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。 3.标量替换为栈上分配提供了很好的基础。
标量替换参数设置
参数 -XX:+EliminateAllocations:开启了标量替换(默认打开),允许将对象打散分配在栈上,不需要GC。
逃逸分析的不足
栈上分配(创建对象还没实现),只是实现了标量替换(将对象打散),所以本质上对象还是都分配在堆上