目录
Java运行时数据区结构如下:
存储数据:堆和方法区
存储指令:程序计数器、虚拟机栈、本地方法栈
程序计数器
线程私有。
作用:记录当前线程所执行字节码的行号。
原因:在多线程情况下,由于是采用时间片轮转的执行方式,一次只能执行一个线程,因此需要记录上一个线程的行号,保护现场,以便恢复执行。在执行的方法是Java方法时是程序计数器中保存的是行号,在执行的是本地方法(底层C、C++),程序计数器的值是undefined。PC寄存器也是唯一的没有定义任何异常的内存空间。
Java虚拟机栈
可以使用XSS设置虚拟机栈的大小。
线程私有,每一个线程启动时都会创建一个Java虚拟机栈,生命周期和线程相同。
作用:先进后出的数据结构,存储当前线程运行方法时所需要的数据、指令及返回地址。
执行方法的过程就是栈帧入栈和出栈的过程,运行方法时会创建一个栈帧进行入栈,当方法执行结束时又会出栈。当入栈的深度超过规定时(-XSS可以指定深度),就会抛出StackOverFlow异常,特别注意递归的调用。
栈帧的结构:局部变量表、操作数栈等。
局部变量表:用来存放方法中的局部变量和对象引用。
局部变量表的特点:①局部变量表的第一个数据是this引用。②局部变量表中的大小是在编译时就已经确定了的,局部变量越多的话,入栈的栈帧数量就会更少,即递归的次数就越少。③局部变量表中的槽位可以复用,当某个局部变量的作用域失效时,新的局部变量可以复用失效数据的槽位,而不必占用新的槽位,达到节省资源的目的。④局部变量表的访问方式是通过索引。
操作数栈:是Java虚拟机的工作空间,通常用于存储计算的中间变量,通过压栈和出栈操作访问。当需要计算数据时,首先从局部变量表中取出两个数据依次进入操作数栈,之后依次弹栈进行相加运算,再将结果压入操作数栈。之后弹出结果,将结果保存在局部变量表中。
本地方法栈
执行本地方法,结构与虚拟机栈相同。
堆
使用xms、xmx可以设置堆空间的大小
线程共享的区域。主要用来存储对象。
堆分为新生代和老年代,新生代与老年代的比例为1:2(-XX:NewRatio可以设置),即新生代占1/3的堆空间大小,老年代占用2/3的堆空间大小。其中新生代又分为1个Eden区和2个survivor区(from和to)比例为eden:from:to=8:1:1(可以通过-XX:SurvivorRatio来设定)。新生代实际可用的内存空间为90%,因为总有一块from或者to区域为空。当new一个对象时,对象首先分配在Eden,当Eden区的内存空间不足时,会进行一次MinorGC,要是Eden的对象还存活,它会被移动到其中的某个survior区(由于采用复制回收算法),每次经过一次GC,对象还存活,将它的年龄加1,默认到达15次之后,该对象会从新生代进入老年代。大对象(数组等)直接在分配时就进入老年代。当老年代内存不足的时候会进行一次FullGC,其中的一次FullGC也会伴随着一次MinorGC。
方法区
线程共享的区域,保存类的信息、静态变量、常量池等。
问题:
什么时候进行垃圾回收?
当新生代或老年代在分配对象内存空间不足时,进行GC。
哪些内存需要回收?
根搜索算法判断对象不可达时就要进行回收。对象不可达:①从根开始没有到达该对象的引用②没有覆盖finallize方法或者已经执行过finallize方法,确认对象没有被复活。可以将这些内存空间回收。
哪些对象可以进入老年代?
- GC次数超多15次的对象可以晋升到老年代
- 大对象直接可以进入老年代
新生代和老年代的算法?
新生代采用复制算法,老年代采用标记回收算法。
1个Eden区和2个survivor区,每次只用Eden区和其中的一个survivor区,当要回收内存时,会将正在使用的Eden区和from区的存活对象,复制到另一个to区中,之后将Eden和from区全部清除,当前工作内存设置位to。
新生代特点:对象生命周期短,存活对象少采用复制算法,性价比高。
老年代大多是存活对象,因此采用标记清除或者标记压缩,将存活的对象进行标记,未存活的对象进行回收。这种性价比高。
垃圾回收算法:
- 引用计数法 缺点:无法解决互相引用
- 标记清除法 缺点:内存碎片
- 复制算法 缺点:内存减小了一半
- 标记压缩法 即解决了内存碎片又解决了内存减半
- 分代回收算法 新生代(复制)老年代(标记清除)