JAVA内存区

内存区域

方法区:(共享)存储加载的类信息,常量,静态常量等。
—-运行时常量池:存放常量(具备动态性,不只是编译时期才能产生)
虚拟机栈:(线程私有)每一个JAVA方法从调用直至执行完成,就对应一个栈帧在虚拟机栈中入栈到出栈。
本地方法栈:Native方法。和虚拟机栈其他相同。
堆:(共享)存放对象实例。GC主要处理区域。
程序计数器:(线程私有)当前线程所执行的字节码的行号指示器。
如果线程正在执行JAVA方法,计数器记录的是正在执行的虚拟机字节码指令的地址

JVM中的堆,一般分为三大部分:新生代、老年代、永久代:

一.新生代

主要是用来存放新生的对象。一般占据堆的1/3空间。
由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。

  • 新生代又分为 Eden区、ServivorFrom、ServivorTo三个区。
    1.Eden区:Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
    2.ServivorTo:保留了一次MinorGC过程中的幸存者。
    3.ServivorFrom:上一次GC的幸存者,作为这一次GC的被扫描者。
    当JVM无法为新建对象分配内存空间的时候(Eden满了),Minor GC被触发。因此新生代空间占用率越高,Minor GC越频繁。

    ——MinorGC的过程:采用复制算法。

    1.首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年龄以及达到了老年的标准,一般是15,则赋值到老年代区)
    2.同时把这些对象的年龄+1(如果ServicorTo不够位置了就放到老年区)
    3.然后,清空Eden和ServicorFrom中的对象;
      最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区。
    

二.老年代

老年代的对象比较稳定,所以MajorGC不会频繁执行。
在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。

-----MajorGC采用标记—清除算法:
    1.首先扫描一次所有老年代,标记出存活的对象
    2.然后回收没有标记的对象。
  MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。
  当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。

三.永久代

    指内存的永久保存区域,主要存放Class和Meta(元数据)的信息。
    Class在被加载的时候被放入永久区域。它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。

   **在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
   元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory, 字符串池和类的静态变量放入java堆中. 这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。

-------Major GC和Full GC区别
       Full GC:收集young gen、old gen、perm gen
       Major GC:有时又叫old gc,只收集old gen

对象的创建过程

1.虚拟机遇到一条new指令,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过。如果没有,那必须先执行响应的类加载过程。

2.当加载通过后,接下来虚拟机将为新生对象分配内存。
堆内存分配方式
1.指针碰撞:如果堆中内存是绝对规整的,一边是用过的内存,一边是空闲内存,中间有一个指针分割,那么只需要移动指针就可以分配内存。
2.空闲列表:如果堆中内存不规整,虚拟机需要维护一个列表,记录那些内存块是可用的,然后通过列表分配。
*注意:内存分配需要考虑线程安全!!!
解决方案:1.对分配内存空间的动作进行同步处理
2.本地线程分配缓冲(TLAB):把每个线程在堆中的内存各自分配

3.分配完成后,虚拟机将内存空间初始化为零(不包括对象头)。然后对对象进行必要的设置(对象的哈希码,GC分代年龄等等)。

对象的内存是如何布局的

对象在内存中存储的布局可以分为:
1.对象头
2.示例数据
3.对齐填充

一.对象头:
包括两部分
第一部分:存储对象自身的运行时数据(哈希码,GC分代,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳)
第二部分:类型指针,虚拟机通过指针来确定这个对象是那个类的实例。

二.实例数据:
对象真正存储的有效信息。

三.对齐填充:
该部分并非一定存在。仅仅是占位符的作用。
因为HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍。

如何访问对象?

在栈上有一个reference数据来操作堆上的具体对象
访问方式有两种:
1.句柄访问:稳定
reference–>堆中的句柄池:存储着对象实例数据(在堆的实例池中)的指针和对象类型数据的指针
2.直接指针访问:速度快
reference–>对象实例数据(其中存储着对象类型数据的指针)

详情看《深入了解JAVA虚拟机》的49页

OOP异常

内存溢出 out of memory

是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;
比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

内存泄露 memory leak

是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。