1.8以后
**虚拟机栈描述的是Java方法执行的内存模型:线程执行期间,每个方法被执行时,都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。**每个方法从被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定,并且写入方法表的Code属性之中。因此栈帧的内存只取决于具体的虚拟机实现。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与该栈帧相关联的方法称为当前方法,执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。
JVM对此区域规定了两个异常:StackOverflowEror和OutOfMemoryError
StackOverflowEror:如果当前线程请求栈所需要的大小大于当前所允许的最大大小,就会抛出java.lang.StackOverflowError。
OutOfMemoryError:若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出java.lang.OutOfMemoryError异常。
从虚拟机的角度看创建对象(new发生了什么):
对象的内存布局:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象的访问定位:Java程序通过栈上的reference数据来操作堆上的具体对象。reference只是一个指向对象的引用。
句柄访问:Java堆中划分出一块内存来作为句柄池,reference中存储的就是Java对象的句柄地址。句柄中则包含了对象实例数据与类型数据各自的具体地址信息。速度快,节省了一次指针定位的时间开销。
直接指针:Java堆对象的布局中必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。
程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭,而栈中的栈帧分配多少内存基本上是在类结构确定下来就知道的,所以这三个区域内存分配和垃圾回收具有确定性。所以GC一般讨论的是Java堆和方法区。
GC的第一步就是判断对象是否已经死了(不可能再被任何途径使用的对象)
引用计数器方法:给对象中添加一个引用计数器,每当有一个地方引用它时,就加1,引用失效就减1,任何时刻计数器都为0的对象就是不可能再被引用的。但该方法并不能解决对象之间互相循环引用的问题,例如两个对象互相引用,但没有其他地方引用这两个对象,理应来说应该被回收,但是由于引用计数器还是存在,就不会回收。
可达性分析:通过一系列的GC Roots对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达),证明该对象不可用。
对象宣告死亡的两个标记过程
根据对象存活周期将内存划分为新生代和老年代,然后根据每个代的不同特点去选择合适的收集算法。
标记出所有需要回收的对象,在标记完成后同一回收所有被标记的对象(前面说的两个标记)然后统一回收。
将可用内存按照容量划分为大小相等的两块,每次只使用其中一块。当这块内存用完了就将还存活的对象复制到另一块上面,然后把已使用过的内存空间一次清理掉。这样每次都是对整个半区进行内存回收,而且把其他存活的对象都整个复制过去,不会考虑内存分配时的不连续的问题。只需要移动堆顶指针按顺序分配即可。
一般多用于老年代,过程与标记-清除一样,只不过后续不是直接对可回收对象进行整理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
给特定位置上的指令生成对应的OopMap,暂停进行GC的位置也是安全点。
一段代码片段中,引用关系不会发生变化,在这个区域中的任何地方GC都是安全的,扩展的安全点。
垃圾收集器有两个概念,并行和并发:
最基本、历史最悠久的,jdk1.3.1之前是虚拟机新生代唯一的收集器。
该垃圾收集器的关注点是达到一个可控制的吞吐量,又称吞吐量优先收集器,采用复制算法实现。停顿时间短适合于与用户交互多的程序,良好的响应速度提升了用户体验,高吞吐量则可以高效率的利用CPU时间,尽快的完成程序的运算任务。
获取最短回收停顿时间为目的的收集器,并发收集且低停顿。
面向服务端的垃圾收集器,目标是代替CMS。
G1收集器把整个Java堆划分为多个大小相等的独立区域(Region),新生代老年代不再物理隔离。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间的大小以及所需要的时间),在后台维护一个优先列表,每次根据允许的时间,优先回收价值最大的Region。
G1收集器分为以下几个步骤收集:
对象主要(优先)分配在新生代的Eden区上,当Eden区空间不足进行一次Minor GC(新生代垃圾清理,比较频繁,速度较快),大对象(需要大量连续内存)则直接进入老年代。
长期存活的对象将进入老年代。虚拟机给每个对象定义一个年龄计数器,对象在Eden出生并经过第一次Minor GC依然存活且能被Survivor收纳,就移动到Survivor中。年龄+1,每过一次Minor GC就加一岁,默认15岁进入老年代。这个阈值可以通过-XX:MaxTenuringThreshold来设置。并不是非得达到MaxTenuringThreshold才到达:如果Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,则大于等于该年龄的对象直接进入老年代。
空间分配担保:主要是针对老年代进行的。Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间:
是则表明Minor GC是安全的。
否则虚拟机查看HandlePromotionFailure设置是否允许担保失败:
为什么进行空间担保?:新生代采用复制算法,但为了内存利用率只使用一个Survivor空间作为轮换备份。如果出现大量对象在Minor GC后仍然存活(极端是都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代,前提是老年代本身要有容纳这些对象的剩余空间。一共有多少存活在实际完成内存回收之前是无法知道的,所以只能取之前的回收晋升到老年代的对象容量的平均大小来与老年代剩余空间比较决定是否Full GC。
JVM中引入了垃圾回收机制,该机制会自动回收一些不再使用的对象。不管是引用计数法还是可达性分析都是判断对象是否是不再被使用的,即是否还被引用。那么如果有些对象其实没用了,因为代码编写的关系而导致JVM误以为这些对象还在使用而无法回收,造成内存泄露。即不再被使用的对象的内存不能被回收。
HashMap、LinkedList等这些容器如果是静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前都不会被释放,从而造成内存泄露。即长生命周期对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则对Connection、Statement等不显示关闭,将会造成大量的对象无法被回收,从而引起内存泄露。
如果一个变量定义的作用范围大于其使用范围,就有可能造成内存泄露,如果不及时的把对象设置为null,就有可能导致内存泄露的发生。一般常见于大量生成只用一次的成员变量或者静态变量。
如果一个外部类的实例对象的方法返回了一个内部类的实例对象,那么这个内部类对象就会被长期引用,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
当一个对象被存储进HashSet以后,就不能修改这个对象中参与计算哈希值的字段。否则对象修改的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下即使在contains方法使用该对象的当前引用作为为参数区HashSet中检索对象,也是检索不到的。这就会导致无法从HashSet中单独删除对象,造成内存泄露。
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
如果stack是先入栈后出栈,那么出栈的元素会留在内存里不被释放。
魔数:class的头四个字节,作用是确定该文件是否是能被虚拟机接受的class文件。
版本号:紧接着模式的四个字节,56是次版本号,78是主版本号。
常量池:存放字面量与符号引用,每个量都是一个表。
访问标志:识别类或者接口层次的访问信息。
类索引、父索引、接口索引集合:确认类的继承关系。
字段表集合:描述接口或者类中声明的变量,包括类级变量以及实例级变量,但不包括方法内部声明的局部变量。
方法表集合:描述方法,跟字段表集合差不多,没有volatile和transient。
属性表集合:用于描述某些场景专有的信息,例如具体的字段,方法体。
除了程序计数器,JVM的其他分区:方法区,虚拟机栈,本地方法栈,Java堆都有可能发生OOM。
public class SimpleExample {
public static void main(String args[]) {
a();
}
public static void a() {
int x = 0;
b();
}
public static void b() {
Y y = new Y();
c();
}
public static void c() {
float z = 0f;
}
}
当main()方法被调用后,执行线程按照代码执行顺序,将它正在执行的方法、基本数据类型、对象指针和返回值包装在栈帧中,逐一压入其私有的调用栈。则此时的栈应该是c()->b()->a()->main()。
程序启动后main方法入栈,然后a方法入栈,局部变量a被声明为int类型,且初始值为0,x和0都被包含在栈帧中。
然后b方法入栈,创建一个Y对象,并赋给变量y。实际的Y对象是在Java堆内存中创建的,不是线程栈,只是Y对象的引用以及变量y在栈帧里。
最后c方法入栈,变量z被声明为float类型,初始化为0f,z和0f都被包含在栈帧里。
当执行方法完成后,所有的线程栈帧按照LIFO的顺序出栈,直到栈空。
上述是正常的,我们改一下这个Example:
public class SimpleExample {
public static void main(String args[]) {
a();
}
public static void a() {
a();
}
}
这通过无限递归就发生了StackOverflowError,因为不停的把a往进压。
综上,JVM线程栈存储了方法的执行过程,基本数据类型,局部变量,对象指针和返回值等信息,这些都是要消耗内存,一旦线程栈的大小增长超过了允许的内存限制,就抛出了StackOverflowError。
使用-Xss参数减少栈的内存容量,以及定义大量的本地变量去增大某方法帧中本地变量表的长度,或者是不停的递归调用方法,均产生的是StackOverflowError。这表明在单个线程下,无论是栈帧太大还是虚拟机容量太小,当内存无法分配都产生StackOverflow。