实战java虚拟机 学习笔记
看穿Java虚拟机的架构
- 类加载子系统:负责从文件系统或者网络中加载class信息。
- 方法区:也称(永久代(PermGen))加载的类信息放在方法区,方法区还会包括运行时常量信息,如:字符串字面量和数字常量。
java8的时候去除PermGen,将其中的方法区移到non-heap中的Metaspace。
Metaspace与PermGen之间最大的区别在于: Metaspace并不在虚拟机中,而是使用本地内存。
- java堆:在虚拟机启动时建立,几乎所有的java对象都放在java堆中。堆空间是线程共享的。
- 直接内存:是java堆外的,直接向系统申请的内存区间。访问直接内存速度会优于java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。(如NIO中:ByteBuffer.allocateDirect()).直接内存不受-Xmx指定最大堆内存影响,但是系统内存是有限的, -Xmx+直接内存 <= 系统内存。
- 垃圾回收系统:垃圾回收器可以对方法区、java堆、直接内存进行回收。
- java栈:每一个虚拟机线程都有一个私有的java栈,java栈中保存着栈帧的信息,java栈中保存着局部变量表、方法参数,同时和java方法的调用、返回密切相关
- 本地方法栈:类似于java栈,最大的不同在于本地方法栈用于本地方法的调用。
- PC程序计数器:是每个线程的私有空间,它的作用可以看作是当前线程所执行字节码的行号指示器。一个java线程总是在执行一个方法,这个被执行的方法被称作当前方法。如果当前方法是一个java方法(非本地方法):程序计数器记录的是正在执行的虚拟机字节码指令的地址,如果执行的是本地方法,则计数器值为空(Undefined)。此内存区域是在java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域。
执行引擎:是java虚拟机的组件之一,它负责执行虚拟机的字节码。
学会设置Java虚拟机的参数
java堆
java堆是和java应用程序关系最为密切的内存空间,几乎所有的对象都放在堆中。并且java堆完全是自动化管理,通过垃圾回收机制,垃圾对象会被自动的清理,不需要显示的释放。
根据垃圾回收机制的不同,java堆可能拥有不同的结构,最常见的是将整个java堆分为新生代(young generation)和老年代(old generation)。
新生代分为:1个Eden区和2个Survivor区(默认比例为8:1),被称为s0区,s1区; s0和s1也称为from和to区域,它们两块是大小相等、可以互换角色的内存空间。
一般情况下,新创建的对象都会被分配到Eden区,这些对象经过第一次新生代回收(Minor GC,老年代的回收是:MajorGC)后,如果对象还存活,则进入s0或者s1,之后每一次新生代回收,它的年龄就会+1,当达到一定年龄后,他会被认为是老年对象,从而进入老年代。
-XX:MaxTenuringThreshold
设置晋升到老年代的最大年龄;如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。
java中堆、方法区、栈的关系
public class SimpleHeap {
private int id;
public SimpleHeap(int id) {
this.id = id;
}
public void show(){
System.out.println("My Id is:"+id);
}
public static void main(String[] args) {
SimpleHeap s1 = new SimpleHeap(1);
SimpleHeap s2 = new SimpleHeap(2);
s1.show();
s2.show();
}
}
java栈
java栈是一块线程私有的内存空间。线程执行的基本行为是函数调用, 每次函数调用的数据都是java栈传递的。
java栈与数据结构上栈有着类似的含义,它是一块先入后出的数据结构,支持支持入栈和出栈两种操作,在java栈中保存的主要内容为栈帧。
如上图所示:函数1对应栈帧1,函数2对应栈帧2,依次类推;函数1调用函数2,函数2调用函数3,函数3调用函数4。当函数n被调用时,函数n入栈;
当前正在执行的函数对应的帧就是当前的帧(位于栈顶):它保存着局部变量表,中间运算结果等数据;
当函数返回时,栈帧从java栈顶弹出.java有两种返回函数的方式:1.正常返回,return指令;2.抛出异常;
由于每次函数调用都会生成对应的栈帧,从而占用一定的栈空间,如果栈空间不足,则会抛出异常StackOverflowError栈溢出错误;
java提供-Xss来指定线程最大栈空间,该参数决定了函数调用的最大深度,下面代码会抛出StackOverflowError异常;
private static int count = 0;
public static void recursion(){
count++;
recursion(); //递归死循环--StackOverflowError
}
局部变量表
局部变量表是栈帧的重要组成部分,它用于保存函数的入参和局部变量表,局部变量表中的变量只在当前函数调用中有效,当函数调用结束后,随着函数栈帧的销毁,局部变量表也会随之销毁。如果函数的入参和局部变量较多,会导致局部变量表膨胀,每一次函数调用占用的栈空间就会变多,导致函数可以正确的嵌套调用次数减少。
操作数栈
操作数栈,主要保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。- 帧数据区
除了局部变量表和操作数栈,java栈帧还需要一些数据来支持常量池解析,正确方法返回和异常处理等。大部分java字节码指令需要进行常量池访问,在帧数据区保存着访问常量池的指针, 方便程序访问常量池。此部分称之为动态连接(Dynamic linking)
当函数返回或者出现异常时,虚拟机必须恢复调用者函数的栈帧,并让调用者继续执行下去。对于异常处理,虚拟机必须要有一个异常处理表, 方便在发生异常时,找到处理异常的代码,因此异常处理表也是帧数据区的重要部分;
它表示在字节码偏移量4-16字节遇到的异常,都跳至19处执行;
如果无法在异常表中找到合适的处理方法,则结束当前函数调用,返回给调用函数,并在调用函数中抛出相同的异常,再在调用函数的异常表来处理 - 栈上分配
栈上分配是java虚拟机提供的一项优化技术,它的基本思想是,对于那些线程私有的对象,可以将他们打散分配在栈上(几乎所有的java对象都放在java堆中),而不是分配在堆上;分配在栈上的好处是:函数调用结束后,自动销毁,无需gc,从而提高系统性能;
public static void alloc(){
User u = new User();
u.id = 5;
u.name = "xxx";
}
上述代码中,对象User是以局部变量表存在的,也没有被alloc()函数返回,所以它可能被分配在了栈上;
对于大量的零散的小对象,栈上分配是一种很好的分配优化策略,栈上分配速度快,并可以有效的避免垃圾回收带来的负面影响,但是栈空间相对较小,因此大对象无法也不适合分配在栈上分配.
方法区
和java堆一样,方法区也是一块线程共享的内存区域;它用于保存系统的类信息:比如类的字段,方法,常量池等。方法区的大小决定了系统可以保存多少个类。
在jdk1.6/jdk1.7,方法区可以理解为永久区(Perm).,如果系统定义了太多类,会导出方法区溢出,虚拟机会抛出内存溢出异常;
-XX:PermSize=xxx -XX:MaxPermSize=xxx
指定永久区的大小和最大永久区大小, MaxPermSize默认为64M;
在jdk1.8,永久区被彻底移除,取而代之的是元数据区,它的大小可以用参数
-XX:MaxMetaspaceSize 指定最大元数据区大小,
,它是一个堆外的直接内存,如果不指定大小,默认情况下,虚拟机可能会耗尽所有的可用系统内存