内存划分
java虚拟机的内存划分主要包括以下几个部分:
- 程序计数器:记录当前线程执行的下一条指令的地址,很小的空间
- 虚拟机栈(stack): 每个方法执行时候在栈里面创建一个栈帧,用于存储局部变量表,操作数栈,动态链接等信息。通过入栈和出栈完成指令操作,当方法执行结束,栈帧销毁,对于的局部变量内存相应释放。
- 本地方法栈:执行navive方法相关用到的内存,在Oracle Hotspot中,虚拟机栈和本地方法栈合二为一。
- 堆(heap):最大的一块内存,存放对象实例,对象的生命周期由JVM的垃圾收集器统一管理。由于现代垃圾收集器采用的分代回收算法,所以相应地划分为年轻代和老年代两块空间,年轻代包括Eden区和2快大小相等的Suvivor区
- 方法区:JDK7以前的概念,它是一块紧挨着堆的连续内存空间,存放类的元信息、静态变量和运行时常量池。在JDK8中,取而代之的是元空间。
- 运行时常量池:方法区的一部分
上图中,灰色的区域方法区和堆是线程共享的,其他部分是线程独享的。
new关键字创建对象
通过new创建对象经历3个步骤:
- 在堆中创建这个对象实例
- 为对象的成员变量赋初值
- 返回该对象的引用
解析new指令时,首先去运行时常量池定位new指令的参数代表的符号引用,并检查该符号引用是否已经被正确加载、解析和初始化了,如果没有,先会执行类的加载过程。如果加载OK了,根据class的信息可以确定要创建的实例所需要的内存空间大小,于是虚拟机就会为该实例分配内存空间。
堆上分配内存通常有指针碰撞和空闲列表两种方式:
- 指针碰撞:假设Java堆是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放一个指针作为分界点的指示器。为对象分配内存时把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
- 空闲列表:Java堆中的内存不是规整的,以使用的内存和空闲的内存相互交错,无法使用指针碰撞。这时虚拟机就必须维护一个列表,记录上哪些内存是可用的,在分配内存的时候从列表中找一块足够大的空间划分给对象实例,并更新列表上的记录。
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
对象的内存布局和访问
对象的内存布局主要包括三块:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
对象的访问定位主要包括句柄和直接的指针访问:
- 句柄:Java堆将会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。
- 直接指针:reference中存储的就是对象地址。速度快,Hotspot采用的是这一种。
运行时常量池
运行时常量池是方法区中的一块内存区域,class字节码文件的常量池在类被加载后,进入运行时常量池中存放。每个类被加载后都有一个运行时常量池,字节码文件中的常量池是静态存储结构,而运行时常量池在运行期间会把符号引用解析为直接引用,具有动态性。
字符串常量池
字符串常量池是全局的,JVM 中独此一份。“使用常量池”对应的字节码是一个ldc指令,在给 String 类型的引用赋值的时候会先执行这个指令,看常量池中是否存在这个字符串对象的引用,若有就直接返回这个引用,若没有,就在堆里创建这个字符串对象并在字符串常量池中记录下这个引用(jdk1.7)。String 类的intern()方法还可在运行期间把字符串放到字符串常量池中。
字符串常量池的位置和存放内容:
- 在 jdk1.6(含)之前也是方法区的一部分,并且其中存放的是字符串的实例;
- 在 jdk1.7(含)之后是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中;
- jdk1.8 已移除永久代,字符串常量池是在本地内存当中,存储的也只是引用。
元空间(metaspace)
JDK8中,hotspot vm彻底取消了永久代,取而代之的是元空间(方法区是规范,永久代和元空间是实现,规范没有改变)。元空间属于本地内存(native memory)。相应地,运行时常量池也由永久代移到元空间中。因此,元空间中主要包含类的元信息,常量池和方法信息。
为什么要用Metaspace替代方法区?
原来的永久代实现中,使用连续的堆空间,通过-XX:MaxPermSize来设定永久代最大可分配空间,当JVM加载的类信息容量超过了这个值,会报OOM:PermGen错误。随着动态类加载的情况越来越多,这块内存变得不太可控,如果设置小了,系统运行过程中就容易出现内存溢出,设置大了又浪费内存。
元空间的内存管理:
- 在metaspace中,类和其元数据的生命周期与其对应的类加载器相同,只要类的类加载器是存活的,在Metaspace中的类元数据也是存活的,不能被回收。
- 每个加载器有专门的存储空间
- 只进行线性分配
- 如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉
默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
- -XX:MetaspaceSize:初始空间大小
- -XX:MaxMetaspaceSize:最大空间大小,超过此值就会触发Full GC,此值默认没有限制,取决于系统内存大小,JVM动态改变此值
- -XX:MinMetaspaceFreeRatio GC之后,最小的Metaspace剩余空间百分比,减少为分配空间所导致的垃圾收集
- -XX:MaxMetaspaceFreeRatio GC之后,最大的Metaspace剩余空间百分比,减少为释放空间所导致的垃圾收集
通过CGlib动态代理来模拟元空间溢出。
1package test.metaspace;
2
3import net.sf.cglib.proxy.Enhancer;
4import net.sf.cglib.proxy.MethodInterceptor;
5import net.sf.cglib.proxy.MethodProxy;
6
7import java.lang.reflect.Method;
8
9public class TestMetaSpace {
10 public static void main(String[] args) {
11 while (true) {
12 Enhancer enhancer = new Enhancer();
13 enhancer.setSuperclass(TestMetaSpace.class);
14 enhancer.setUseCache(false);
15 enhancer.setCallback(new MethodInterceptor() {
16 @Override
17 public Object intercept(Object arg0, Method arg1,
18 Object[] arg2, MethodProxy arg3) throws Throwable {
19 return arg3.invokeSuper(arg0, arg2);
20 }
21
22 });
23 enhancer.create();
24 }
25 }
26}
添加的cglib依赖为:
1 <dependency>
2 <groupId>cglib</groupId>
3 <artifactId>cglib</artifactId>
4 <version>3.2.10</version>
5 </dependency>
将元空间的最大大小设置为10m:-XX:MaxMetaspaceSize=10m,则运行程序很快出现元空间的OOM:
1Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
2 at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)
3 at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
4 at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117)
5 at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
6 at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
7 at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
8 at test.metaspace.TestMetaSpace.main(TestMetaSpace.java:23)
9
10Process finished with exit code 1
如果将元空间的最大大小设置为200M,通过Jvisualvm可以看到元空间的变化情况。
虚拟机参数
- -Xms5m 堆的初始大小为5m
- -Xmx5m 堆的最大大小为5m
- -XX:+HeapDumpOnOutOfMemoryError 当发生OOM时生成堆的内存快照