JVM内存区域
内存模型图:
堆
线程共享。所有的对象实例以及数组都要在堆上分配。回收器主要管理的对象。
它的目的是存放对象实例。同时它也是GC所管理的主要区域,因此常被称为GC堆,又由于现在收集器常使用分代算法,Java堆中还可以细分为新生代和老年代
Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。
主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存,完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)
方法区
存储类对象
方法区是被所有线程共享的内存区域,用来存储已被虚拟机加载的类信息、常量、静态变量、JIT(just in time,即时编译技术)编译后的代码等数据。运行时常量池是方法区的一部分,用于存放编译期间生成的各种字面常量和符号引用。
通过反射获取到的类型、方法名、字段名称、访问修饰符等信息就是从方法区获取到的。在使用到CGLib对类进行增强时,增强的类越多,就需要越大的方法区类存储动态生成的Class信息,当存放方法区数据的内存溢出时,会报OutOfMemoryError异常。
栈区,在方法结束自动清空,又可细分为:
程序计数器
程序计数器,又叫PC寄存器,是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。
程序计数器?
为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。
虚拟机栈
线程私有,生命周期与线程相同,就是我们平时说的栈,栈描述的是Java方法执行的内存模型。
每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接(对象的内存地址),方法出口(返回值的内存)等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈(JVM执行本地方法)
本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。
- 动态链接: 每个栈帧都保存了 一个 可以指向当前方法所在类的 运行时常量池, 目的是: 当前方法中如果需要调用其他方法的时候, 能够从运行时常量池中找到对应的符号引用, 然后将符号引用转换为直接引用,然后就能直接调用对应方法, 这就是动态链接
- 不是所有方法调用都需要动态链接的, 有一部分符号引用会在类加载解析阶段, 将符号引用转换为直接引用, 这部分操作称之为: 静态解析. 就是编译期间就能确定调用的版本, 包括: 调用静态方法, 调用实例的私有构造器, 私有方法, 父类方法
垃圾回收
Garbage Collection(GC)
判断对象已死
1.引用计数算法
引用计数算法是在对象中加入一个计数器,当对象被引用,计数器+1,当引用失效,计数器-1,当计数器的值编程0,就是没有任何一个变量来引用这个对象,那么这个对象就是垃圾
这种算法实现简单,效率高,但是有一个严重的问题会导致内存泄漏,那就是对象之间循环引用,比如说A对象持有B对象的引用,B对象持有A对象的引用,那么A和B的计数器值永远>=1,也就是说这两个对象永远不会被回收,这是一堆垃圾。
内存溢出:指程序在运行过程中,因为申请的内存超过了可用的内存空间而导致程序崩溃或异常结束。java程序内存不够,程序就结束了,OOM
内存泄漏:指程序在运行过程中,申请的内存空间无法被回收或释放,导致系统中的可用内存逐渐减少,最终耗尽可用内存。
2.可达性分析算法(Java使用的这一种)
Java中定义了一些起始点,称为GC Root(正在使用的对象或量),当有对象引用它的时候,就把对象挂载在它下面,形成一个树状结构,当一个对象处于一个这样的树里时,就认为此对象是可达的,反之是不可达
GC ROOT 四种对象:
- 虚拟机栈中引用的对象
- 方法区类的静态成员引用的对象
- 方法区常量引用的对象
- 本地方法栈中JNI(Java Native Interface的缩写)引用的对象
垃圾收集算法
1.标记清除(Mark-Sweep)
算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象(引用计数法或者可达性分析),在标记完成后统一回收掉所有被标记的对象。它是最基础的收集算法,后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。
它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2.标记复制(Copying)
复制算法在标记清除算法的基础上,针对内存碎片问题做了一下优化,此算法把内存分为大小相同的两块,每次在使用的时候只使用其中的一块。当一块内存用完的时候。把存活对象复制到另外的一块中,然后清除当前这块中的所有的对象,如此反复。
缺点:使用当前算法,解决了内存碎片化严重的问题,但是存在缺陷就是每次只使用一半的空间,空间利用率受到影响。同时对于存活周期长的对象,复制次数多。
复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
3.标记整理(Mark-Compact)
也叫标记压缩法
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,缺点是时间稍慢
4.分代收集算法(Generational Collection)
GC分代的基于一个假设:绝大部分对象的生命周期都非常短暂,存活时间短。
“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
永久代,指方法区,这里不做讨论。
新生代,又分为伊甸园区和幸存区,使用标记复制算法。所有new出来的对象,都先存入伊甸园区,较大的对象伊甸园区可能没有足够的空间,可能会直接放到老年代区。
幸存区又可分为工作区和等待区,当伊甸园区存满垃圾回收(minorGC),会将伊甸园区域和幸存区的工作区A进行标记,将不是垃圾的对象,复制到幸存区的等待区B,之后B区就是工作区,A区变成等待区。
当对象经过多次(15次)新生代垃圾回收,依然存活,这个对象就会存入老年代。
如果老年代满了(触发FullGC,将老年代和新生代一起GC),会进行垃圾回收,使用标记整理(压缩)算法。