Java栈的生命周期和线程相似;在每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、指向运行时常量池的引用和返回地址(方法出口)等信息,每个方法从调用到执行完毕的过程都对应着一个栈帧JVM中入栈到出栈到过程。(栈的大小与具体虚拟机的实现有关,一般在256~756之间)
特性:
a. 生命周期与线程相似且线程属于私有;
b. 当线程请求的栈深度超过了JVM所允许的最大深度就会发生StackOverflowError异常;
c. 若是栈的扩展无法申请到足够的内存则会产生OutOfMemmoryError异常;
3、本地方法栈(Native Method Stack)
本地方法栈的原理和作用与Java栈类似,不同的是Java栈是为执行Java方法服务的,而本地方法栈是为执行本地方法服务的。
4、堆(Heap)
Java中的堆事要来存储对象本身以及数组(数组引用存储在Java栈中),Heap事JVM所管理的内存中最大的一块,它在虚拟机启动事创建且在JVM中只有一个堆;由于JVM垃圾收集器采用的基本都是分代收集算法,So堆还可以划分为Young Generation(年轻代)、Old Generation(年老代)以及Perm Generation(永久代)【JDK7之后,Hotspot虚拟机便将永久代这个概念移除了】,其中的Young Generation又分为Eden、From和To,而From和To又统称为Survivor Spaces(幸存区);正常情况下,一个对象从创建到销毁,应该是从Eden开始,然后到Survivor Spaces,再到Old Generation,最后在某次GC下消失。
特性:
a. 堆是被所有线程共享的,且JVM中只有一个堆;
b. 存储对象实例;
c. 当在堆中没有完成实例的分配且无法再扩展内存则会有OutOfMemory异常;
5、方法区(Method Area)
方法区主要用于存储每个类的信息(类的名称、方法信息、字段信息)、静态变量、常量和编译器编译后的代码等;此外,在Class文件中除了类的字段、方法和接口等描述外,还有常量池,用来存储编译期间生成的字面量和符号引用;在方法区中还有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后对应的运行时常量池就被创建出来,非Class文件常量池的内容也可以将新的常量放入运行事常量池中,如String的intern方法(如果常量池中存在当前字符串就会直接返回当前字符串,若是常量池中无此字符串则会将其放入常量池中再返回)。
在大概了解了Java运行时环境JVM内存的划分后,个人感觉要进入BAT还有必要了解下对象的创建和定位,这应该对自己写代码也有莫大的助力。
对象的创建:
1、JVM接收到一条new指令后,首先会去检查这个指令的参数能否再常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化,如若没有,则会先执行类的初始化;
2、类加载检查通过后,JVM会为新生对象分配内存,而对象所需内存大小在类加载完成后便可完全确定,随后就在Java堆中划分出一块确定大小的内存为对象分配空间;
case1: 若内存是规整的,则JVM将采用指针碰撞发来为对象分配内存;指针碰撞发会将所有用过的内存放在一边,空闲的内存放置于另一边,中间放着一个指针作为分界点的指示器,分配内存的时候只需把指针向空闲内存的那边挪动一段与对象大小相等的距离;如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的机制,则虚拟机采用指针碰撞发分配内存;
case2: 弱内存时不规整的,已使用的内存和未使用的内存相互交错,那么JVM采用的是空闲列表发来为对象分配内存;就是虚拟机维护了一个列表,记录下那些内存块是可用的,然后在分配内存的时候就从列表中找到一块足够大的空间划分给对象实例,并更新列表上维护的内容;若是垃圾回收器选择CMS这种基于标记-清除算法的机制,则JVM采用这种方式分配内存;
case3: 在JVM中可能会出现虚拟机正在给对象A分配内存,但指针还没有来得及修改,此时对象B又同时使用了原来的指针来分配内存的情况;为了及时保证new对象时候线程的安全性,JVM采用了CAS配上失败重试的方式保证更新更新操作的原子性和TLAB两种方式来解决这个问题;