1、jvm虚拟机内存模型
程序计数器:是线程的私有空间,每个线程都有一个独立的程序计数器,用于记录下一指令。
java虚拟机栈:和线程创建的时候一起创建的,也是线程的私有空间,它保存了线程的局部变量、执行结果、调用和返回的情况。
在虚拟机栈中会返回栈溢出的错误,如果一个线程的栈太深会返回此错误。
如下一个例子:
private int count = 0;
public static void main(String[] args) {
TestStack testStack = new TestStack();
try {
testStack.testStack();
} catch (Exception e) {
System.out.println("stack:" + testStack.getCount());
}
}
private void testStack() {
count++;
testStack();
}
public int getCount() {
return count;
}
此时会报栈溢出。java.lang.StackOverflowError,其实此时堆里面可能还够用,但超过了栈的最大深度就报错了。
栈桢
在java虚拟机栈中有一种结构是保存运行的上下文数据的,在栈桢中有如下数据:
局部变量表、操作数栈、动态链接方法、返回地址。如果一个函数传入的参数越多,里面的执行方法局部变量也多话那么栈桢就越大了,理论上递归的次数就越小了。
java本地方法栈:管理调用本地方法的空间,和java虚拟机栈类似,java虚拟机栈是管理java函数调用的,而本地方法栈则管理本地方法调用的,只不过本地方法调用是用c语音去实现的,这个栈也会发生栈溢出。
java堆:
几乎java的所有对象和数组的内存分配都是在java堆中去完成的。在java的堆中分为新生代和老年代。新生代是存放刚刚新生成的对象的,当新生代对象没有被回收幸存下来后,会移动到老年代。
新生代其实有划分3个区域:enden区、survivor space0(s0或是from space)和survivor space1(s1,或是to space区域)enden区是对象的出生地,是刚刚初始化的对象的地方,当幸存活下来后会存放到s0和s1区。也就说经过一次gc回收后,enden区的存活的对象会进入到s0和s1区,当再次经过gc的时候,那么对象就会放入到老年代了
Tenured generation。
s0区域和s1区域一样大。
堆的划分如下:
通常eden/s0的比值是8:1,新生代的最大利用空间为新生代的90%
测试使用如下程序:
public static void main(String[] args) {
byte[] b1 = new byte[1024 * 1024 / 2];
byte[] b2 = new byte[1024 * 1024 * 8];
b2 = null;
b2 = new byte[1024 * 1024 * 8];
//System.gc();
}
jvm的运行参数是:
-Xms40m -Xmx40m -Xmn20m -XX:SurvivorRatio=8 -XX:+PrintGCDetails -Xloggc:./gclogs
初始化堆为40m,最大的堆为40m,新生代分配为20m,新生代的eden/s0的比值为8,并打印出详细是gc日志如下:
0.477: [GC 0.478: [ParNew: 10362K->849K(18432K), 0.0040941 secs] 10362K->849K(38912K), 0.0042710 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
par new generation total 18432K, used 9696K [7f8600000, 7f9a00000, 7f9a00000)
eden space 16384K, 54% used [7f8600000, 7f8ea3e60, 7f9600000)
from space 2048K, 41% used [7f9800000, 7f98d4498, 7f9a00000)
to space 2048K, 0% used [7f9600000, 7f9600000, 7f9800000)
concurrent mark-sweep generation total 20480K, used 0K [7f9a00000, 7fae00000, 7fae00000)
concurrent-mark-sweep perm gen total 21248K, used 4735K [7fae00000, 7fc2c0000, 800000000)
上面是没有开启的system.gc的日志。从上可以看出,进行性了一次新生代的gc。
b1变量由eden区域移动到了from区域,后面生成的b2是分配在了eden区域。
开启调用gc的日志如下:
0.303: [GC 0.303: [ParNew: 10362K->846K(18432K), 0.0025869 secs] 10362K->846K(38912K), 0.0027647 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
0.309: [Full GC (System) 0.309: [CMS:
0K->9026K(20480K), 0.0204103 secs] 9365K->9026K(38912K), [CMS Perm : 4677K->4676K(21248K)], 0.0205271 secs] [Times: user=0.05 sys=0.01, real=0.02 secs]
Heap
par new generation total 18432K, used 655K [7f8600000, 7f9a00000, 7f9a00000)
eden space 16384K, 4% used [7f8600000, 7f86a3df0, 7f9600000)
from space 2048K, 0% used [7f9800000, 7f9800000, 7f9a00000)
to space 2048K, 0% used [7f9600000, 7f9600000, 7f9800000)
concurrent mark-sweep generation total 20480K, used 9026K [7f9a00000, 7fae00000, 7fae00000)
concurrent-mark-sweep perm gen total 21248K, used 4734K [7fae00000, 7fc2c0000, 800000000)
如上新生代的被回收了,enden区和from区都回收了,其存活对象被回收到了老年代(红色标记)
方法区:
方法区也是一个重要的java内存区域,和java堆类似也是所有线程共享的,只是方法区存放的是类信息和元数据。(类信息、常量池、域信息、方法信息等)
方法区也被称为永久区、持久代。
Hot spot虚拟机对于常量池的回收是很明确的,只要常量池对象没有被其他地方引用就可以被回收。
下面是测试的代码:
public static void main(String[] args) {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
String t = String.valueOf(i).intern();
}
}
jvm参数:
-XX:MaxPermSize=64m -XX:+PrintGCDetails -Xloggc:./gclogs
这里只是分配了64m的空间,可以看到gc的日志可以正常回收方法区的内存。
244.919: [Full GC 244.919: [CMS: 289K->289K(63872K), 0.3033899 secs] 10165K->289K(83008K), [CMS Perm
: 65535K->4713K(65536K)], 0.3034787 secs] [Times: user=0.21 sys=0.07, real=0.31 secs]
250.831: [GC 250.831: [ParNew: 17024K->2K(19136K), 0.0068389 secs] 17313K->291K(83008K), 0.0069196 secs] [Times: user=0.03 sys=0.00, real=0.00 secs]
当方法区快要满的时候最终还是被回收了。
注意,如果设置为最大的为4mb,此时会报错。这是因为默认设置应该是:
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 132120576 (126.0MB)
NewSize = 21757952 (20.75MB)
MaxNewSize = 87228416 (83.1875MB)
OldSize = 65404928 (62.375MB)
NewRatio = 7
SurvivorRatio = 8
PermSize = 21757952 (20.75MB)
MaxPermSize = 85983232 (82.0MB)
即是默认的方法区最小是21mb的。可以通过jmap -heap pid来进行查看。