《深入理解Java虚拟机》学习笔记之内存分配
JVM在执行Java程序的过程中会把它所管理的内存划分若干个不同的数据区域,如下图:
大致可以分为两类:线程私有区域和线程共享区域。
线程私有区域
- 程序计数器(Program Counter Register): 是一块很小的内存,可以看做是当前线程所执行的字节码行号指示器,虚拟机根据计数器值获取吓一条要执行的指令。
- JVM栈:虚拟机栈(JVM stacks),每个方法被执行时都会同时创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 本地方法栈(Native Method Stacks):与虚拟机栈的作用类似,但区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用的Native方法服务。有些虚拟机如Sun HotSpot把它与虚拟机栈合二为一。
线程共享区域
- Java堆(Java Heap):是Java编程最频繁使用和最大的内存区域,也是垃圾收集器管理的主要区域,此区域唯一存在的目的就是存放对象实例和数组。
- 方法区(Method Area):用于储存已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。
- 运 行时常量池(Runtime Constant Pool):是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法去的运行时常量池中。
其实还一块 内存即直接内存(Direct Memory),它并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但这部分内存也被频繁地使用,并且可能导致OOM异 常。如NIO可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用 进行操作,这显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
内存异常
程 序计数器是唯一一个在Java虚拟机规范中没有规定任何OOM(OutOfMemoryError)情况的区域,其他区域在无法再申请到足够的内存时就会 抛出OOM异常,而虚拟机栈和本地方法栈除了OOM异常外,当线程请求的栈深度大于虚拟机所允许的深度时将抛出StackOverflowError异 常。
对象访问
即使最简单的对象访问,都会涉及到Java栈、Java堆和方法区这三个最重要的区域。如:
Object obj = new Object();
“Object obj”将反映到JVM栈的局部变量表中,作为一个reference类型数据出现;而“new Object()”将反映到Java堆中,形成一块储存了Object类型所有实例数据值的结构化内存;另外在Java堆中还必须包含能查找到此对象类型 数据信息(如对象类型、父类、实现的接口、方法等),这些类型数据则储存在方法区。
由于Java虚拟机规范之规定了reference类型指向对象的引用,并没有定义寻址方式,因此目前有两种主流的寻址方式:使用句柄和直接指针。
如果使用句柄访问方式,Java 堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息,如下图所示
如果使用直接指针访问方式,Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的就是对象地址,如下图所示
异常示例
- Java堆溢出OOM
package net.oseye.demo; import java.util.ArrayList; import java.util.List; /** * VM Args:-Xms20m -Xmx20m */ public class App { public static void main( String[] args ) { List<OOMObject> list=new ArrayList<App.OOMObject>(); while(true){ list.add(new OOMObject()); } } static class OOMObject{} }
java -Xms20m -Xmx20m net.oseye.demo.App
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Unknown Source)
at java.util.Arrays.copyOf(Unknown Source)
at java.util.ArrayList.grow(Unknown Source)
at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
at java.util.ArrayList.add(Unknown Source)
at net.oseye.demo.App.main(App.java:15) -
虚拟机栈StackOverFlowError
package net.oseye.demo; /** * VM Args:-Xss256k */ public class App { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) throws Throwable { App oom = new App(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + oom.stackLength); throw e; } } }
java -Xss256k net.oseye.demo.App
stack length:1890
Exception in thread "main" java.lang.StackOverflowError
at net.oseye.demo.App.stackLeak(App.java:11)
at net.oseye.demo.App.stackLeak(App.java:11)
at net.oseye.demo.App.stackLeak(App.java:11)
PS:这里扑捉异常使用的是Throwable,如果使用Exception就不能显示出println的信息,此处不解,有待学习。 - 虚拟机栈 OOM
可以开多个线程让虚拟机栈OOM,但其实这不是虚拟机栈抛出的,只是由于分给栈的内存多了自然会让虚拟机进程内存少了。这里不妨把XSS设置大一些。注意这里有风险哦,会造成操作系统假死,我在ubuntu下执行不仅ubuntu死了,我直接按电源启动ubuntu还让操作系统崩溃了,启动时报的异常“No init found. Try passing init= bootarg”,然后按照这个方法才修复了操作系统。package net.oseye.demo; /** * VM Args:-Xss10m */ public class App { public static void main(String[] args) throws Throwable { App app = new App(); app.stackLeakByThread(); } private void dontStop() { while (true) { System.out.println(Thread.currentThread().getId()); } } private void stackLeakByThread() { while (true) { Thread thread = new Thread(new Runnable() { @Override public void run() { dontStop(); } }); thread.start(); } } }
java.lang.OutOfMemoryError:unable to create new native thread
- 运行时常量池溢出
如果向要运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含了一个等于此String对象的字符串,则返回idaibiao池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量:package net.oseye.demo; import java.util.ArrayList; import java.util.List; /** * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M */ public class App { public static void main(String[] args) throws Throwable { List<String> list=new ArrayList<String>(); int i=0; while(true){ list.add(String.valueOf(i).intern()); } } }
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
PS:google了之后才知道在jdk6及之前都会报上述异常,但jdk7就不会,而我用的是jdk7.简单来说就是在JDK 7里String.intern生成的String不再是在perm gen分配,而是在Java Heap中分配。 -
方法区溢出
访法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这个区域的测试,基本思路是运行时产生大量的类去填满访法区,直到溢出。书中是借助CGLib框架直接操作字节码,生成大量的动态类:package net.oseye; import java.lang.reflect.Method; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; /** * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M */ public class App { public static void main(String[] args) throws Throwable { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable { return arg3.invokeSuper(arg0, arg2); } }); enhancer.create(); } } static class OOMObject { } }
java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
at java.lang.ClassLoader.defineClass(ClassLoader.java:616)Exception in thread "main"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main" -
本机直接内存溢出
DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样,代码清单2-9越过了DirectByteBuffer类,直接通过反射获取Unsafe实例进行内存分配(Unsafe类的getUnsafe()方法限制了只有引导类加载器才会返回实例,也就是设计者希望只有rt.jar中的类才能使用Unsafe的功能)。因为,虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配,于是手动抛出异常,真正申请分配内存的方法是unsafe.allocateMemory()。package net.oseye; import java.lang.reflect.Field; import sun.misc.Unsafe; /** * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M */ public class App { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws Exception { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1MB); } } }
Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at net.oseye.App.main(App.java:19)