一、概述
我们的代码运行过程中的,虚拟机管理着内存的分配和使用。我们今天先了解内存区域,使我们深入了解JVM的第一步。
二、运行时数据区域
根据java虚拟机规范,虚拟机内存区域结构大概如图,我们详细介绍每个区域:
1.程序计数器
- 我们简单想象有个helloworld程序运行。代码最终是一步一步解释为机器码,所以有一个程序计数器,记录当前的代码执行位置,也就是行号。
- 假如有两个线程执行helloworld,那每个线程执行到第几行都需要各自保存,每个线程都有独立的计数器。
- 如果是循环打印,字节码需要改变程序计数器的值取到下一条指令。
- 如果是java方法,计数器指向的是代码位置,如果是native方法,计数器为空。
- 程序计数器是唯一一个没有 outofmermoryError情况的区域。
2. java虚拟机栈
- 和程序计数器一样,java虚拟机栈也是线程私有。
- 我们debug代码的时候,debugger会显示某个正在运行的线程,然后自上而下一次为每个方法对应的栈帧,每个栈帧保存局部变量等,每个方法执行结束,就有一个栈帧入栈到出栈:
- 局部变量表存储的是各自基本数据类型(8种)和引用。64位的long和double占用两个变量空间,其余的是一个,一般来说,局部变量表的大小是固定的。
- 栈有两种异常,Stack OverflowError 和 outofmermoryError,前者是方法调用栈太多,例如递归,后者是内存不够。
3.本地方法栈
和栈类似,主要负责本地方法,实现上很自由,有的直接和栈合二为一。也有两种异常。
4.java 堆
- 内存最大,线程共享,作用就是存放实例(几乎所有的实例,但是技术发展导致没这么绝对)。
- 垃圾回收采用分带收集,所以堆包括了新生代、老年代。还可以细分为:eden、from survivor、to survivor。
- 可能也有线程私有的内存缓冲区,只是为了更好分配和回收。
- 只要逻辑上连续即可,无需物理连续。大小可以调节(-Xmx和-Xms)
- 无法扩展并且没有内存分配示例,会有OutOfmermoryError。
5.方法区
- 我们运行了一个helloworld方法,对应的主类和常量、静态变量、编译后的代码都需要放在方法去,逻辑上和堆的一个部分。
- 基本上不需要垃圾回收,所以有人叫他永久带,实际上只是一开始的JVM将它放在了永久代的而已。但是从1.7开始,已经把原本放在永久代的字符串常量池移出, 放在堆中。
- 方法去无法满足内训分配需求,也会有OutOfmermoryError,但是从1.7开始,不在这样。
- 类的元数据, 字符串池, 类的静态变量将会从永久代移除, 放入Java heap或者native memory.其中建议JVM的实现中将类的元数据放入 native memory, 将字符串池和类的静态变量放入java堆中. 这样可以加载多少类的元数据就不在由MaxPermSize控制, 而由系统的实际可用空间来控制.
6. 运行时常量池
- 是方法区的一部分,.class 文件中除了有类的版本、字段、方法等信息,还有常量池,用于存放编译器的自变量和符号引用。这部分在加载后会进入方法去的运行时常量池。
- .class文件的每一部分格式都很严格。但是常量池很宽松。
- java语言并不要求一定要常量只能在编译时候产生,运行期间也可以将新的常量放入常量池。这种特性用的最多的是String的intern()方法。
- 也会有OutOfmermoryError。
7. 直接内存
这部分是由于java的NIO引起的
三、Hotspot对象探秘
1.对象的创建
- 从写代码看,对象的创建(例如克隆,反序列化)只是一个new关键字,然后我们调试可以看到其实还执行了 初始化的
方法。 - 从虚拟机角度看,首先是检查对应的引用能否在常量池中定位到一个类的符号引用,并检查是否已经加载解析和初始化过,如果没有,就要开始加载。
- 类的加载我们以后讨论,加载完后需要分配内存。对象大小在加载完成后就已经完全确定了,如果java堆内存是绝对规整的,那么需要维护一个指针指向当前分配到的位置。如果不连续需要维护一个空闲列表。
- 分配内存可能是多线程的,有安全问题。要么加锁,要么给每个内存一个预先分配的小内存,成为本地分配缓存。
- 内存分配完成后需要初始化为0值,然后进行元数据设置,例如是那个类的实例,GC代等。
- 这时候对象创建才刚刚开始,执行
方法。
2.对象的内存布局
- 对象在内存中的存储布局可以分为3部分,对象头,实例数据、对象填充。对象头第一部分存储运行时数据,第二部分是类型指针。运行时数据hash码、分带年龄等。类型指针指向类元数据,数组还要记录数组长度。
- 第二部分为实例数据,就是代码里面定义的数据内容
- 第三部分没什么含义,仅仅是占位符。
3.对象的访问
1.对象的访问有两种方式,第一种是句柄。java堆会有一块专门的内存作为句柄池,栈存储的是句柄地址,句柄包含了对象示例数据(堆)和类型数据(方法区)各自的指针。 2. 第二中方法是指针访问,栈存储的直接是对象地址,堆的对象布局必须考虑如何放置访问类型数据。 3. 句柄最大的好处是对象改变时不需要改变栈的地址,使用直接内存好处是访问速度快,节省时间。
四、实战 OutOfmermoryError
除了程序计数器,都会有OutOfMermoryError异常,我们实战一下,在IDEA编写代码,并学习几个参数。
1.堆溢出
/**
* Created by dengziming on 17/04/2018.
* VM ARGS: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject{}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true){list.add(new OOMObject());}
}
}
VM ARGS: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError 马上报错:
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid98722.hprof ...
Heap dump file created [27798040 bytes in 0.363 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at io.github.dengziming.session2.HeapOOM.main(HeapOOM.java:21)
如何查看对文件和分析,我们后续有内容。简单分析两点: 1. 如果是内存泄露,通过GC工具查看泄露对象的GC引用链,定位代码位置 2. 如果内存溢出,可以考虑调大参数。 -Xmx -Xms
2.虚拟机栈和本地方法溢出
/**
* Created by dengziming on 18/04/2018.
* VM ARGS -Xss128k
*/
public class JavaVMStackSOF {
private int stackLenth = 1;
public void stackLeak(){
stackLenth ++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF oom = new JavaVMStackSOF();
try{
oom.stackLeak();
}catch (Exception e){
System.out.println("stackLenth: " + oom.stackLenth);
throw e;
}
}
}
Exception in thread “main” java.lang.StackOverflowError 结果表明,单线程下,无论是栈帧太大还是栈容量太小,内存无法分配的时候,都是Stack Overflow,如果多线程到不太一样。
/**
* Created by dengziming on 18/04/2018.
* VM ARGS: -Xss20M
*/
public class JavaVMStackOOM {
private void dontStop(){
while (true){}
}
public void stackLeakByStack(){
while (true){
Thread thread = new Thread() {
@Override
public void run() {
dontStop();
}
};
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByStack();
}
}
运行完电脑卡死了,算了。这个内存越大反而容易耗尽资源,因为机器内存是固定的,减少容量可以获得更多的线程数。 注意:这时候通过减少内存解决内存溢出的方法,没有经验是不知道的。
3. 方法区和运行时常量池溢出
String.intern() 的含义是返回在代表常量池中这个字符串的对象。如果没有,就将这个字符串放进常量池,并返回引用。
/**
* Created by dengziming on 18/04/2018.
*
* vm args: -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i=0;
while(true){
list.add(String.valueOf(i++).intern());
}
}
}
不好意思这个方法没有达到效果: Java HotSpot™ 64-Bit Server VM warning: ignoring option PermSize=10M; support was removed in 8.0 Java HotSpot™ 64-Bit Server VM warning: ignoring option MaxPermSize=10M; support was removed in 8.0 jdk1.7 已经把原本放在永久代的字符串常量池移出, 放在堆中。
public static void main(String[] args) {
while(true){
Enhancer enhancer = new Enhancer();
enhancer.setSupperClass(OOMObject.class);
enhancer.setUserCache(false);
enhancer.setCallBack(new MethodInterceptor(){
public Object intercept(Object obj , Method method , Object []args , MethodProxy proxy)throw Throwable{
return proxy.invokeSuper(obj , args);
}
});
enhancer.create();
}
}
static class OOMObject{
}
这个也是一样。因为类的元数据, 字符串池, 类的静态变量从永久代移除, 放入Java heap或者native memory.其中建议JVM的实现中将类的元数据放入 native memory, 将字符串池和类的静态变量放入java堆中.
String.intern() 在1.6和1.7有不同的实现。
public class RuntimeConstantPoolOOM2 {
public static void main(String[] args) {
String str1 = new StringBuilder().append("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder().append("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}
1.6输出为false和false 1.7输出为true和false 原因是:1.6 的 intern返回在永久代的实例,如果是第一次遇到,会先复制到永久代。1.7不会复制到永久代,只是记录首次出现的实例的引用。 所以1.6的时候两个intern返回的是永久代的引用而不是字符串,1.7的时候 java 这个串已经出现过了。