JVM学习02-虚拟机的基本结构
1.虚拟机的基本结构
虚拟机的结构可以参考 java虚拟机规范 进行理解
- 类加载子系统
加载类信息(class)到方法区
- 方法区
存放类信息、运行时常量池信息(字符串和数字常量)
- java堆
虚拟机启动时建立的内存工作区域,存放几乎所有java对象实例,所有线程共享。
- 直接内存(NIO)
java堆外的直接向系统申请的内存,可以由java的NIO库进行操作。速度优于java堆,适用于频繁读写,不受Xmx最大内存限制,受操作系统最大内存限制
- 垃圾回收系统
可以对方法区、java堆和直接内在进行回收。重点回收java堆。
- java栈
每个java虚拟机线程都有一个私有的java栈,在线程被创建的时候创建。java栈中保存着栈帧信息,还保存着局部变量、方法参数,与java方法的调用 、返回密切相关。
- 本地方法栈
java虚拟机的重要扩展,本地方法(通常C编写 )
- Program Counter 程序计数器/PC寄存器
程序计数器也是线程私有空间。
- 执行引擎
核心组件,执行虚拟机字节码。
2.堆空间结构
堆空间分为新生代和老年代
-
新生代:新生对象或年龄不大的对象
-
eden区
对象首先分配在eden区,新生代回收后,如果对象还存活,则会进入 s0或者s1区
-
s0区(from区)和s1区(to区),大小相等的两块,可以互换角色的内存空间
每经一次新生代回收,存活的对象的年龄会加1,达到一定条件后会被认为是老年对象进入老年区。
(问题,判定老年对象的条件是什么)
-
-
老年代
堆、方法区、栈的关系
3.函数调用与java栈
线程的执行的基本行为是函数调用,每次函数调用的数据都是通过java栈传递。java栈的主要内容是栈帧,每次函数调用都会有栈帧压入java栈,函数调用结束栈帧被弹出java栈。
函数返回时,栈帧从java栈中弹出。java方法有两种返回函数的方式,一种是return,一种是抛出异常。一个栈帧中至少要包含局部变量表、操作数栈和栈帧数据区几个部分。
由于每次函数调用都会生成对应的栈帧,所以会占用栈空间,因此,如果栈空间不足,那函数调用无法继续进行下去,当请求的栈尝试大于最大可用栈深度时,系统就会抛出StackOverFlowError栈溢出错误。可以使用参数 -Xss来指定线程的最大栈空间,这个参数决定了函数的最大的调用深度。函数嵌套调用的层次,取决于栈空间的大小,栈越大,函数就可以嵌套越多次。
-
局部变量表
保存函数的参数及局部变量。
ps:相同的栈容量下,局部变量少的函数,可以支持更深的函数调用,如果函数的参数和局部变量较多,会使局部变量表膨胀,导致函数嵌套次数变少。
栈帧中的局部量变量中的槽位是可以重用的,局部变量过了作用域,后续新申请的局部变量可能会复用过作用域的局部变量的槽位,用以节约资源。
以下代码来测试局部变量与垃圾回收关系
/** * * 测试变量的GC结果 * 执行时增加jvm参数 -XX:+PrintGC * @author domino * @date 2021/5/17 */ public class TestLocalVariable { /** * 有引用指向变量不会被回收 */ public void localVarGc1() { byte[] a = new byte[6 * 1024 * 1024]; System.gc(); } /** * 无引用指向变量被回收 */ public void localVarGc2() { byte[] a = new byte[6 * 1024 * 1024]; a = null; System.gc(); } /** * 虽然变量超过作用域,但是变量仍存在于局部变量表中,并且也指向数组,所以不会被立即回收 */ public void localVarGc3() { { byte[] a = new byte[6 * 1024 * 1024]; } System.gc(); } /** * a变量超过了作用域,a变量失效,新建变量c,使用了a变量的字,a变量被销毁,垃圾回收可以正常回收数组 */ public void localVarGc4() { { byte[] a = new byte[6 * 1024 * 1024]; } int c = 10; System.gc(); } /** * 调用 localVarGc1 方法以后,localVarGc1的栈帧被销毁,栈帧中的局部变量被销毁,所以可以正常垃圾回收 */ public void localVarGc5() { localVarGc1(); System.gc(); } public static void main(String[] args) { TestLocalVariable t1 = new TestLocalVariable(); // t1.localVarGc1(); // t1.localVarGc2(); // t1.localVarGc3(); // t1.localVarGc4(); t1.localVarGc5(); } }
方法1到方法5的运行结果如下,可以结合方法注释来看结果原因。
---------------------- [GC (System.gc()) 10080K->6824K(251392K), 0.0048977 secs] [Full GC (System.gc()) 6824K->6599K(251392K), 0.0056204 secs] ---------------------- [GC (System.gc()) 16676K->6831K(251392K), 0.0008493 secs] [Full GC (System.gc()) 6831K->411K(251392K), 0.0045246 secs] ---------------------- [GC (System.gc()) 10487K->6619K(251392K), 0.0010462 secs] [Full GC (System.gc()) 6619K->6555K(251392K), 0.0041690 secs] ---------------------- [GC (System.gc()) 14010K->6555K(251392K), 0.0003860 secs] [Full GC (System.gc()) 6555K->411K(251392K), 0.0036868 secs] ---------------------- [GC (System.gc()) 9177K->6651K(251392K), 0.0011968 secs] [Full GC (System.gc()) 6651K->6555K(251392K), 0.0050845 secs] [GC (System.gc()) 6555K->6555K(251392K), 0.0005586 secs] [Full GC (System.gc()) 6555K->411K(251392K), 0.0042518 secs]
使用工具jclasslib可以查看函数的中变量的作用域、槽位的索引、变量名、数据类型等等,这个文档写着比较麻烦,后面补充
-
操作数栈
用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
-
帧数据区
栈帧用来支持常量池解析、正常方法返回和异常处理等的数据
- 帧数据区中保存着常量池的指针,方便程序访问常量池。(疑问:常量池在什么区?)
- 异常处理表是帧数据区中重要部分,在处理异常时,虚拟机必须有一个异常处理表方便发生异常时找到处理的代码。
-
栈上分配
java虚拟机优化技术,将线程私有对象分配到栈上,不分配到堆,函数调用后自行销毁,不需要垃圾回收提高性能。
栈上分配的技术基础是逃逸分析,判断对象的作用域是否能逃出函数体。如果不能,可能被分配到栈上而不在堆上。
测试代码:
/** * 本类测试栈上分配,逃逸分析,局部变量范围未超出函数体可能会被分配到栈而不是堆 * * 执行方法要加以下jvm参数 * -server -Xmx10m -Xms5m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations * 说明: * -server server模式下才会启用逃逸分析 * -Xmx10m 指定堆空间最大内存为10M * -XX:+DoEscapeAnalysis 启用逃逸分析 * -XX:+PrintGC 打印GC日志 * -XX:-UseTLAB 关闭TLAB Thread Local Allocation Buffer 线程本地分配缓存 * -XX:+EliminateAllocations 开启标量替换(默认是打开的),允许将对象打散分配到栈上,如对象拥有id和name * 这两人个变量将被视为两个独立的局部变量进行分配 * 执行结果 * [GC (Allocation Failure) 1023K->565K(5632K), 0.0012699 secs] * [GC (Allocation Failure) 1589K->613K(6656K), 0.0015624 secs] * 9 * GC (Allocation Failure) : 发生了一次垃圾回收,若前面有Full则表明是Full GC,没有Full的修饰表明这是一次Minor GC * 。注意它不表示只GC新生代,括号里的内容是gc发生的原因,这里的Allocation Failure的原因是年轻代中没有足够区域能够存放需要分配的数据而失败,。 * 参考:https://blog.csdn.net/topdeveloperr/article/details/88874957 * * * @author dmn * @date 2021/5/19 */ public class OnStackTest { public static class User { public int id = 0; public String name = ""; } public static void alloc() { User user = new User(); user.id = 5; user.name = "dmn"; } public static void main(String[] args) throws InterruptedException { long a = System.currentTimeMillis(); for (int i = 0; i < 200000000; i++) { alloc(); } long b = System.currentTimeMillis(); System.out.println(b - a); } }
以上代码执行结果如下,我们可以看到,执行完了,中间只出现过3次GC
[GC (Allocation Failure) 1023K->519K(5632K), 0.0006984 secs] [GC (Allocation Failure) 1543K->567K(5632K), 0.0011059 secs] [GC (Allocation Failure) 1591K->583K(5632K), 0.0010251 secs] 8
而作为对比,我们如果将上述代码中的main方法改为如下:
public static void main(String[] args) throws InterruptedException { List<User> userList = new ArrayList<User>(); for (int i = 0; i < 200000000; i++) { User user = new User(); user.id = 5; user.name = "dmn"; userList.add(user); System.out.println(i); } }
执行的结果为如下,加了输出以后,可以看到,当集合加入了第240096个user对象以后,就发生了OutOfMemoryError了,说明内存最多放下240096个对象,不是内存够用。
[GC (Allocation Failure) 1023K->558K(5632K), 0.0007154 secs] [GC (Allocation Failure) 1580K->937K(5632K), 0.0015253 secs] [GC (Allocation Failure) 1961K->1667K(5632K), 0.0011996 secs] [GC (Allocation Failure) 2691K->2705K(6656K), 0.0015517 secs] [GC (Allocation Failure) 4753K->4378K(6656K), 0.0023053 secs] [Full GC (Ergonomics) 4378K->3657K(9728K), 0.0302245 secs] [GC (Allocation Failure) 4790K->4833K(8704K), 0.0056917 secs] [GC (Allocation Failure) 5857K->5907K(9216K), 0.0018717 secs] [GC (Allocation Failure) 6931K->6971K(9216K), 0.0022074 secs] [Full GC (Ergonomics) 6971K->6211K(9216K), 0.0428260 secs] [GC (Allocation Failure) 6981K->7139K(9216K), 0.0035979 secs] [Full GC (Ergonomics) 7139K->6981K(9216K), 0.0411045 secs] [GC (Allocation Failure) 6981K->6981K(9216K), 0.0008322 secs] [Full GC (Allocation Failure) 6981K->6963K(9216K), 0.0573462 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:265) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231) at java.util.ArrayList.add(ArrayList.java:462) at cn.shutdown.demo.OnStackTest.main(OnStackTest.java:57) Process finished with exit code 1
但是很奇怪的是我去掉了
-XX:+DoEscapeAnalysis
和-XX:+EliminateAllocations
两个jvm参数以后,执行的结果也还是一样,在配置较低的Linux服务器上面执行也是一样,没有像书中介绍的出现大量的GC日志。去掉参数使用idea执行结果 [GC (Allocation Failure) 1023K->533K(5632K), 0.0008148 secs] [GC (Allocation Failure) 1557K->597K(5632K), 0.0015174 secs] [GC (Allocation Failure) 1621K->597K(5632K), 0.0021861 secs] 9
在centos中执行去掉参数的结果 [root@hdss7-188 temp]# java -server -Xmx10m -Xms10m -XX:+PrintGC -XX:-UseTLAB OnStackTest [GC (Allocation Failure) 2751K->255K(9920K), 0.0006507 secs] [GC (Allocation Failure) 3007K->255K(9920K), 0.0003628 secs] [GC (Allocation Failure) 3007K->255K(9920K), 0.0001592 secs] 16
4.方法区
方法区是所有线程共享的内存区域,用于保存系统的类信息,类的字段、方法、常量池等。方法区大小决定系统可以保存多少个类。如果类太多可能导致方法区溢出而抛出内在溢出错误。
jdk1.6、jdk1.7中,方法区理解为永久区Perm。使用-XX:PermSize和-XX:MaxPermSize指定,默认-XX:MaxPermSize为64MB。如果使用了动态代理 ,可能会在运行时生成 大量的类,那就需要设置一个合理的永久区大小。
测试方法区代码:
import java.util.HashMap;
/**
* 测试方法区/永久区,
* 用1.7的jdk运行加以下参数
* -XX:PermSize=5m -XX:+PrintGCDetails -XX:MaxPermSize=5m
* 1.8的jdk运行加以下参数
* -XX:MaxMetaspaceSize=10m -XX:+PrintGCDetails
*
* @author domino
* @date 2021/5/20
*/
public class PermTest {
public static void main(String[] args) {
int i = 0;
try {
Map<String, Object> map = new HashMap<>();
map.put("id", Integer.class);
map.put("name", String.class);
//书上的数量是100000,但很快就正常运行完了,所以我把数量提升到了2000000000
for (i = 0; i < 2000000000; i++) {
CglibBean bean = new CglibBean("cn.shutdown.demo.Permtest" + i, map);
}
} catch (Throwable e) {
System.out.println("total create count" + i);
} finally {
System.out.println("total create count" + i);
}
}
}
import net.sf.cglib.beans.BeanGenerator;
import net.sf.cglib.beans.BeanMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class CglibBean {
@SuppressWarnings("unchecked")
public CglibBean(Map propertyMap) {
Object object = generateBean(propertyMap);
BeanMap beanMap = BeanMap.create(object);
}
@SuppressWarnings("unchecked")
private Object generateBean(Map propertyMap) {
BeanGenerator generator = new BeanGenerator();
Set keySet = propertyMap.keySet();
for (Iterator i = keySet.iterator(); i.hasNext(); ) {
String key = (String) i.next();
generator.addProperty(key, (Class) propertyMap.get(key));
}
return generator.create();
}
}
因为我的本机的jdk是1.8版本,已经移除了永久区,所以使用-XX:MaxMetaspaceSize=10m -XX:+PrintGCDetails
的参数去运行,但是100000的数量很快就执行完了,执行的结果如下。
[GC (Allocation Failure) [PSYoungGen: 65536K->1042K(76288K)] 65536K->1050K(251392K), 0.0025167 secs] [Times: user=0.01 sys=0.01, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 66578K->656K(76288K)] 66586K->672K(251392K), 0.0028107 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
total create count100000
Heap
PSYoungGen total 76288K, used 30148K [0x00000007aa900000, 0x00000007b3e00000, 0x00000007ffe00000)
eden space 65536K, 45% used [0x00000007aa900000,0x00000007ac5cd130,0x00000007ae900000)
from space 10752K, 6% used [0x00000007af380000,0x00000007af424010,0x00000007afe00000)
to space 10752K, 0% used [0x00000007ae900000,0x00000007ae900000,0x00000007af380000)
ParOldGen total 175104K, used 16K [0x00000006ffe00000, 0x000000070a900000, 0x00000007aa900000)
object space 175104K, 0% used [0x00000006ffe00000,0x00000006ffe04000,0x000000070a900000)
Metaspace used 3822K, capacity 5028K, committed 5120K, reserved 1056768K
class space used 418K, capacity 458K, committed 512K, reserved 1048576K
Process finished with exit code 0
这不行啊,没整出来活啊,我得要他溢出啊,他没溢啊,然后我就将循环的数量提升到了2000000000(多少个零自己数去)。然后用Run With VisualVM
来运行,在监控的图里面去看元数据区里面的内存占用效果。结果跑了好长的时间,分配 的10M元数据区的内存也还是没溢出(这里书上要求是设置5M的,但估计是因为我的系统和jdk都是64位的,所以IDE提示5M内存太小,然后我试着的结果是最小10M)。
然后就想试试1.7是什么效果,因为电脑只装了1.8,所以就去官网去下载jdk1.7,结果,好家伙,jdk1.7不提供下载了,官网的大概意思是,jdk1.7只提供给付了支持费的用户下载,还要提单子,于是找了别的下载,安装到电脑上面。jdk1.7百度盘下载链接
Idea上面配置了jdk1.7以后,将demo项目的编译jdk配置为1.7,PermTest类的jre也指定1.7进行运行。
然后再运行项PermTest
代码
public class PermTest {
public static void main(String[] args) {
int i = 0;
try {
for (i = 0; i < 10000000; i++) {
CglibBean bean = new CglibBean("cn.shutdown.demo.Permtest" + i, new HashMap());
}
} catch (Throwable e) {
System.out.println("total create count" + i);
} finally {
System.out.println("total create count" + i);
}
}
}
好家伙,按 -XX:PermSize=5m -XX:+PrintGCDetails -XX:MaxPermSize=5m
这个参数去执行竟然正常的执行完了。也没有出现永久区溢出的情况。也许是现在的电脑比起作者写书的时候的电脑的性能强悍的太多了?
[GC [PSYoungGen: 66048K->1003K(76800K)] 66048K->1011K(251392K), 0.0021120 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[GC [PSYoungGen: 67051K->576K(76800K)] 67059K->592K(251392K), 0.0020370 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC [PSYoungGen: 66624K->624K(76800K)] 66640K->640K(251392K), 0.0008780 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC [PSYoungGen: 66672K->640K(142848K)] 66688K->656K(317440K), 0.0014140 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[GC [PSYoungGen: 132736K->640K(142848K)] 132752K->656K(317440K), 0.0042810 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[GC [PSYoungGen: 132736K->528K(265216K)] 132752K->552K(439808K), 0.0020670 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC [PSYoungGen: 196752K->320K(265216K)] 196776K->816K(439808K), 0.0026410 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 320K->0K(265216K)] [ParOldGen: 496K->489K(95232K)] 816K->489K(360448K) [PSPermGen: 4095K->4094K(4096K)], 0.0140030 secs] [Times: user=0.03 sys=0.01, real=0.02 secs]
....略过一部分日志....
Heap
PSYoungGen total 563200K, used 5749K [0x00000007aaa80000, 0x00000007cd780000, 0x0000000800000000)
eden space 562176K, 1% used [0x00000007aaa80000,0x00000007ab01d460,0x00000007ccf80000)
from space 1024K, 0% used [0x00000007ccf80000,0x00000007ccf80000,0x00000007cd080000)
to space 1024K, 0% used [0x00000007cd680000,0x00000007cd680000,0x00000007cd780000)
ParOldGen total 1104896K, used 427K [0x0000000700000000, 0x0000000743700000, 0x00000007aaa80000)
object space 1104896K, 0% used [0x0000000700000000,0x000000070006ae98,0x0000000743700000)
PSPermGen total 4096K, used 4094K [0x00000006ffc00000, 0x0000000700000000, 0x0000000700000000)
object space 4096K, 99% used [0x00000006ffc00000,0x00000006fffffa90,0x0000000700000000)
Process finished with exit code 0
然后我再调小参数,使用-XX:PermSize=3m -XX:+PrintGCDetails -XX:MaxPermSize=1m
,然后内存终于溢出了,报出了PermGen space
,这也就是方法区(永久区)溢出了。整活完毕。
[GC [PSYoungGen: 2642K->384K(76800K)] 2642K->384K(251392K), 0.0015940 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 384K->0K(76800K)] [ParOldGen: 0K->182K(122368K)] 384K->182K(199168K) [PSPermGen: 2047K->2046K(2048K)], 0.0079460 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC [PSYoungGen: 1321K->32K(94208K)] 1503K->214K(216576K), 0.0008100 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 32K->0K(94208K)] [ParOldGen: 182K->182K(232448K)] 214K->182K(326656K) [PSPermGen: 2047K->2047K(2048K)], 0.0051070 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[GC [PSYoungGen: 0K->0K(120832K)] 182K->182K(353280K), 0.0012080 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 0K->0K(120832K)] [ParOldGen: 182K->172K(351744K)] 182K->172K(472576K) [PSPermGen: 2047K->2047K(2048K)], 0.0058910 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[GC [PSYoungGen: 2201K->32K(132608K)] 2374K->204K(484352K), 0.0004310 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC [PSYoungGen: 32K->0K(132608K)] [ParOldGen: 172K->172K(537600K)] 204K->172K(670208K)
....省略部分日志....
Error occurred during initialization of VM
java.lang.OutOfMemoryError: PermGen space
at sun.misc.Launcher.<init>(Launcher.java:71)
at sun.misc.Launcher.<clinit>(Launcher.java:57)
at java.lang.ClassLoader.initSystemClassLoader(ClassLoader.java:1489)
at java.lang.ClassLoader.getSystemClassLoader(ClassLoader.java:1474)