JVM内存模型以及分区
1.方法区:
线程共享的区域,存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
不需要连续的内存和可以选择固定大小或者可扩展,可以选择不实现垃圾收集
当方法区无法满足内存分配需求时,将抛出OutOfMemoryErroy异常
方法区包含一个运行时常量池
存放class中各种符号引用及翻译出来的直接引用
运行期间也可以将新的常量放入池中 例如String类的intern()方法
常量池无法申请到内存时抛出OutOfMemoryErroy异常
2.堆:线程共享的区域,存储对象实例,以及给数组分配的内存区域也在这里。
垃圾收集器管理的主要区域
Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
可固定大小也可以是可扩展的
如果在堆中没有完成实例分配,并且堆也无法在扩展时将会抛出OutOfMemoryError异常
3.Java虚拟机栈:线程隔离的区域,每个线程都有自己的虚拟机栈,生命周期和线程相同。
局部变量表就是通常所说的栈内存,存基本数据类型,和对象引用。
虚拟机栈描述Java方法执行的内存模型,以栈帧为单位,每个栈帧存储和方法运行有关的局部变量表、操作数栈、动态链接、方法出口等信息。方法调用到执行完成对应栈帧在虚拟机栈中的入栈到出栈的过程
线程请求的栈深度大于虚拟机允许深度,抛出StackOverflowError
扩展时无法申请到足够的内存,抛出OutOfMemoryError
4.程序计数器:线程隔离的区域,每个线程都有自己的程序计数器,存储程序当前执行的字节码的行号。
线程切换后需要找到正确执行位置
如果执行的是Java方法,计数器就记录者正在执行的虚拟机字节码指令的地址。如果是native 方法,计数器为空
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemotyError情况的区域
5.本地方法栈:线程隔离,和Java虚拟机栈类似,是虚拟机调用Native方法时使用的。同样会抛出StackOverflowError和OutOfMemoryError
对象的创建
1.检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程
2.分配内存
接下来将为新生对象分配内存,为对象分配内存空间的任务等同于把一块确定的大小的内存从Java堆中划分出来。
假设Java堆中内存是绝对规整的,所有用过的内存放在一遍,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针指向空闲空间那边挪动一段与对象大小相等的距离,这个分配方式叫做“指针碰撞”
如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式成为“空闲列表”
选择那种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
分配内存同步处理:CAS+失败重试 保证原子性
本地线程缓冲Thread local allocation buffer ,TLAB,哪个线程分配内存就在哪个线程的TLAB上分配,TLAB耗尽重新分配时才需要同步锁定
3.内存空间初始化为零值(不包括对象头)
若开启了TLAB则这一动作提前至分配内存时进行
4.设置对象头
存储这个对象属于哪个类实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
根据虚拟机运行状态不同,如 :是否启用偏向锁对象头会有不同的设置方式
5.init
java中的初始化
对象的内存布局
对象头+实例数据+对象填充
对象头:存储对象自身的运行时数据,包括哈希吗,GC分代年龄,锁状态标识,线程持有的锁,偏向线程ID,偏向时间戳等;对象头的另外一部分是类型指针,即对象指向它在方法区中的类元数据的指针,虚拟机通过这个指针来确定该对象是哪个类的实例。如果对象是一个数组,对象头还有一块用语记录数组长度的数据。
实例数据:对象真正存储的有效信息,是在类中定义的各种类型的字段内容。
对齐填充:虚拟机要求对象的大小必须是8字节的整数倍,对齐填充起占位符的作用,保证对象大小为8字节的整数倍。
对象的访问定位
句柄访问:Java堆划分出一块区域用作句柄池,栈中引用存储对象的句柄地址,句柄中才实际包含着对象实例数据和对象类型数据各自的具体地址信息。
直接指针访问:栈中的引用直接指向对象在堆中的地址,对象在头数据中指向方法区中其类元数据的地址。
使用句柄的好处是引用中存储的是稳定的句柄地址,在对象被移动(垃圾回收导致对象的移动)时只会改变句柄中的实例数据指针。使用直接指针访问的好处是速度更快,节省了一次指针定位的开销。
垃圾回收判定方法
引用链法(可达性分析):通过GC Roots作为起点,当一个对象到到GC Roots没有任何引用链相连时,证明对象时不可用的。
可作为GC Roots的对象是
虚拟机栈中引用的对象
本地方法栈中JNI(Native方法)引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象(执行上下文和全局性引用)
Java的四种引用类型及特点:
1.强引用:程序中普遍存在的,类似“String s=”hello wold””这类的引用,强引用的对象不会被回收。
2.软引用:有用但是非必须的对象在系统将要发生内存溢出之前会对软引用的对象进行垃圾回收,SoftReference类实现软引用。
3.弱引用:非必须的对象,被弱引用关联的对象只能存活到下一次垃圾收集发生之前。
4.虚引用:最弱的引用关系,不能通过虚引用取得对象的实例,为对象设置虚引用的唯一目的就是在这个对象被收集器回收时收到一个系统通知。
四种引用强度依次减弱,强软弱虚。
三种垃圾收集算法:复制算法,标记-清除算法、标记-整理算法
标记-清除算法:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。缺点:标记和清除两个过程效率都不高;标记清除后会产生空间碎片,空间碎片导致分配较大对象时可能提前出发垃圾回收。
复制算法:将可用内存分为两个区域,每次只使用其中一块,当使用的那一块内存用完时,将还存活的对象复制到另外一块内存中,然后把已使用过的内存空间一次清理掉。优点:解决的空间碎片问题,实现简单。缺点:将内存缩小为两块,内存使用率不高。复制操作频繁效率变低。
标记-整理算法:可回收对象标记后,让所有存活的对象向一端移动,然后清理掉边界以外的内存。优点:不会产生空间碎片,比复制算法提高了内存空间利用率。
复制算法用在年轻代的垃圾回收中,标记整理和标记清除算法用在老年代垃圾回收的收集器中。
回收的过程?——双重标记
具体的回收过程是,当在GC时发现一个对象可被回收,就会先对他做一次标记,这是第一次标记。之后会筛选一下,判断一个对象的finalized()方法是否有必要被执行。如果有,那么就会被放置到一个队列中,之后虚拟机会单独的处理这一队列中的对象,依次调用他们的finalized()方法,这里是对象复活的唯一机会。之后又会统一进行一次标记,如果这次标记标记成功,那么对象就会被认定为死亡,会立刻被回收。
一个对象的finalized() 只会被系统调用一次,finalized()方法的优先级很低。并不一定会等待finalized()执行完才回收。
内存分配
虚拟机针对对内存回收,又把堆分为了两个区,新生代和老年代。新生代又分为一个Eden区和两个Survivor区。
每次分配内存,如果对象比较大的话直接进入老年代。否则,先进入Eden区和一个Survivor区,同时会为每一个对象设一个年龄值。
之后会周期性的在某个安全点检查一下,对于新生代的对象,将可回收的对象回收掉,将剩余的对象复制到另一个Survivor区,这一过程中会对年龄值加一。
这一过程叫做Minor GC,是属于新生代的GC。年龄超过tenuring threshold的移动到老年代中。MinorGC移动时Survivor空间不够则通过分配担保机制移动到老年代,老年代还不够则Full GC。
每次MinorGC后,都会计算一个合理的tenuring threshold和各年代区的size,以及适时地调整size
如果不能满足,就会对老年代进行一次GC,这一过程叫做Full GC。而这个检查对象是否可GC得时机,也就是GC的时机,一般是确定的被称作“安全点”。在这一时机进行检查,是不会影响程序正常运行的。