程序计数器:线程所执行字节码的行号指示器,通过改变这个计数器的值来选取下一条需要执行的字节码指令
虚拟机栈:每个方法执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、方法出口等信息
本地方法栈:与虚拟机栈类似,虚拟机是为java方法服务的,本地方法栈是对虚拟机使用到的native方法服务
本地方法栈和虚拟机栈都会抛出StackOverflowError和OutOfMemoryError异常
堆:堆就是存放创建的对象实例的,几乎所有的对象实例都在堆上分配内存,堆是java垃圾收集器的主要管理区域。从内存回收的角度来看,现在收集器基本上采用分代回收算法,java堆还分为新生代和老年代
方法区:用于存储已经被虚拟机加载的类信息、常量、静态变量等数据,方法区也被称为永久代
运行时常量池:是方法区的一部分,用于存放编译期生成的各种字面量和符号引用
java对象创建:new指令,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号的引用代表的类是否已经被加载、解析和初始化过。如果没有则必须先执行类加载过程。类加载过后,将为新生代对象分配内存空间,内幕才能大小在类加载完成时就能确定。
内存分配与回收策略:
java中提倡的自动内存管理用于解决两个问题:对象的内存分配以及回收分配给对象的内存。
内存分配规则:
1、对象优先在Eden分配:Eden是堆中的新生代分区,当eden没有空间进行新对象的分配时,虚拟机会发起一次minorGC
例如需要给3个2mb大小和1个4mb大小的对象分配空间,在运行时通过-Xms20M、-Xmx20M、-Xmn10M这三个参数限制java堆的大小为20M,其中10M分配给新生代,剩下10M分配给老年代。(新生代又由一个eden区,两个survivor区组成,通过-XX:SurvivorRatio=xx指定eden区与survivor区的占比,例如为8时,eden与一个survivor区的占比为8:1)
2、大对象直接进入老年代
大对象是指需要大量连续空间的java对象,最典型就是很长的字符串及数组
3、长期存活的对象进入老年代
虚拟机给每个对象设置了一个年龄计数器,如果对象在eden出生并且经过第一次minorGC后任然存活,并且能够被survivor容纳的话,对象年龄设为1,每经过一次minorGC后依然存活的对象,年龄加1,当对象年龄达到15(默认是15)时,会晋升到老年代
4、动态对象年龄判定
虚拟机不是永远要求对象必须达到年龄才能进入老年代,如果survivor空间中相同年龄所有对象大小的总和大于survivor空间的一般,年龄大于或等于该年龄的对象可以直接进入老年代。
5、空间分配担保
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,
-
如果大于,则此次Minor GC是安全的
-
如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。
如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。
上面提到了Minor GC依然会有风险,是因为新生代采用复制收集算法,假如大量对象在Minor GC后仍然存活(最极端情况为内存回收后新生代中所有对象均存活),而Survivor空间是比较小的,这时就需要老年代进行分配担保,把Survivor无法容纳的对象放到老年代。老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更多空间。
取平均值仍然是一种概率性的事件,如果某次Minor GC后存活对象陡增,远高于平均值的话,必然导致担保失败,如果出现了分配担保失败,就只能在失败后重新发起一次Full GC。虽然存在发生这种情况的概率,但大部分时候都是能够成功分配担保的,这样就避免了过于频繁执行Full GC。
垃圾收集算法: