一个进程对应一个jvm实例,一个运行时数据区,又包含多个线程,这些线程共享了方法区和堆,每个线程包含了程序计数器、本地方法栈和虚拟机栈。
1.一个jvm实例只存在一个堆内存,堆也是java内存管理的核心区域
2.Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间(堆内存的大小是可以调节的)
3.《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。
4.所有的线程共享java堆,在这里还可以划分线程私有的缓冲区(TLAB:Thread Local Allocation Buffer).(面试问题:堆空间一定是所有线程共享的么?不是,TLAB线程在堆中独有的)
5.《Java虚拟机规范》中对java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。从实际使用的角度看,“几乎”所有的对象的实例都在这里分配内存 (‘几乎’是因为可能存储在栈上,另见逃逸分析)
6。数组或对象永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。
7.在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
8.堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域
编写HeapDemo/HeapDemo1代码:
public class HeapDemo {public static void main(String[] args) {System.out.println("start...");try {Thread.sleep(1000000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("end...");}
}
首先对虚拟机进行配置,如图 Run-Edit configurations:
在jdk目录,jdk1.8.0_171.jdk/Contents/Home/bin下找到jvisualvm 运行(或者直接终端运行jvisualvm),查看进程,可以看到我们设置的配置信息:
可以看到HeapDemo配置-Xms10m, 分配的10m被分配给了新生代3m和老年代7m:
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];}
}
JDK 7以前: 逻辑上分为新生区+养老区+永久区(即Xms/Xmx分配的内存物理上没有涉及永久区)
JDK 8以后: 逻辑上分为新生区+养老区+元空间(即Xms/Xmx分配的内存物理上没有涉及元空间)
1.Java堆区用于存储java对象实例,堆的大小在jvm启动时就已经设定好了,可以通过 "-Xmx"和 "-Xms"来进行设置
2.一旦堆区中的内存大小超过 -Xmx所指定的最大内存时,将会抛出OOM异常。
3.通常会将-Xms和-Xmx两个参数配置相同的值,其目的就是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
4.查看设置的堆内存参数:
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");//-Xms : 245MSystem.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 3641MSystem.out.println("系统内存大小为:" + initialMemory * 64.0 / 1024 + "G");//系统内存大小为:15.3125GSystem.out.println("系统内存大小为:" + maxMemory * 4.0 / 1024 + "G");//系统内存大小为:14.22265625Gtry {Thread.sleep(1000000);} catch (InterruptedException e) {e.printStackTrace();}}
}
设置堆大小为600m,打印出的结果为575m,这是因为幸存者区S0和S1各占据了25m,但是他们始终有一个是空的,存放对象的是伊甸园区和一个幸存者区。
java.lang.OutOfMemoryError: Java heap space。代码示例:
/*** -Xms600m -Xmx600m*/
public class OOMTest {public static void main(String[] args) {ArrayList list = new ArrayList<>();while(true){try {Thread.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}list.add(new Picture(new Random().nextInt(1024 * 1024)));}}
}class Picture{private byte[] pixels;public Picture(int length) {this.pixels = new byte[length];}
}
1.存储在JVM中的java对象可以被划分为两类:
2.Java堆区进一步细分可以分为年轻代(YoungGen)和老年代(OldGen)。其中年轻代可以分为Eden空间、Survivor0空间和Survivor1空间(有时也叫frmo区,to区)
3.配置新生代与老年代在堆结构的占比
4.在hotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1(测试的时候是6:1:1),开发人员可以通过选项 -XX:SurvivorRatio 调整空间比例,如-XX:SurvivorRatio=8
5.几乎所有的Java对象都是在Eden区被new出来的
6.绝大部分的Java对象都销毁在新生代了(IBM公司的专门研究表明,新生代80%的对象都是“朝生夕死”的)
7.可以使用选项-Xmn设置新生代最大内存大小(这个参数一般使用默认值就好了)
测试代码:
/*** -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();}}
}
为新对象分配内存是件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配的问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。
1.new的对象先放伊甸园区。此区有大小限制。
2.当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
3.然后将伊甸园中的剩余的幸存对象移动到幸存者0区。
4.如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
5.如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
6.啥时候能去养老区呢?可以设置次数。默认是15次。·可以设置参数:-XX:MaxTenuringThreshold=进行设置。
7.在养老区,相对悠闲。当老年区内存不足时,再次触发GC:Major GC,进行养老区的内存清理。
8.若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常。
总结:针对幸存者s0,s1区:复制之后有交换,谁空谁是to。
关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不再永久区/元空间收集。
注意:只有伊甸园满了才会触minorGC/youngGC,而幸存者区满了是绝对不会触发minorGC的。
代码举例:
public class HeapInstanceTest {byte[] buffer = new byte[new Random().nextInt(1024 * 200)];public static void main(String[] args) {ArrayList list = new ArrayList();while (true) {list.add(new HeapInstanceTest());try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}}
}
对应堆空间分配过程:
1.JDK命令行
2.Eclipse:Memory Analyzer Tool
3.Jconsole
4.VisualVM
5.Jprofiler
6.Java Flight Recorder
7.GCViewer
8.GC Easy
JVM在进行GC时,并非每次都针对上面三个内存区域(新生代、老年代、方法区)一起回收的,大部分时候回收都是指新生代。
针对hotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(Full GC)
1.部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:
2.整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。
年轻代GC(Minor GC)触发机制:
老年代GC(Major GC/Full GC)触发机制
Full GC触发机制
为什么要把Java堆分代?不分代就不能正常工作了么
分配60m堆空间,新生代 20m ,Eden 16m, s0 2m, s1 2m,buffer对象20m,Eden 区无法存放buffer, 直接晋升老年代。
/** 测试:大对象直接进入老年代* -Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails*/
public class YoungOldAreaTest {// 新生代 20m ,Eden 16m, s0 2m, s1 2m// 老年代 40mpublic static void main(String[] args) {//Eden 区无法存放buffer 晋升老年代byte[] buffer = new byte[1024 * 1024 * 20];//20m}
}
日志输出:
简单解释一下为什么会出现这种情况: 因为给 allocation2 分配内存的时候 eden 区内存几乎已经被分配完了,我们刚刚讲了当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC.GC 期间虚拟机又发现 allocation1 无法存入 Survivor 空间,所以只好通过 分配担保机制:把新生代的对象提前转移到老年代中去,老年代上的空间足够存放 allocation1,所以不会出现 Full GC。执行 Minor GC 后,后面分配的对象如果能够存在 eden 区的话,还是会在 eden 区分配内存。可以执行如下代码验证:
public class GCTest {public static void main(String[] args) {byte[] allocation1, allocation2,allocation3,allocation4,allocation5;allocation1 = new byte[32000*1024];allocation2 = new byte[1000*1024];allocation3 = new byte[1000*1024];allocation4 = new byte[1000*1024];allocation5 = new byte[1000*1024];}
}
/*** 测试-XX:UseTLAB参数是否开启的情况:默认情况是开启的*/
public class TLABArgsTest {public static void main(String[] args) {System.out.println("我只是来打个酱油~");try {Thread.sleep(1000000);} catch (InterruptedException e) {e.printStackTrace();}}
}
在发生Minor Gc之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
注意:在JDK6 Update24之后(JDK7),HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察openJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。
JDK6 Update24之后的规则变为:只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。