Java内存结构
- HotSpot VM是一个最常用的Java虚拟机
- 垃圾回收机制,内存调优主要都是围绕着堆来谈的。
- 堆主要存放对象的实例,以及数组
- 堆还分为新生代和老年代,新生代又分为from区,S0区和S1区。
- 方法区主要存放一些永久数据,静态,常量信息都会在方法区中存在,垃圾回收机制一般不会在方法区回收,但不代表不去回收。方法区也是可以被JVM回收的。
- 运行常量池是方法区的一部分。
- 什么是方法区:
方法区与JAVA堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类的信息,常量,静态变量,即时编译器编译后的代码等数据。他有个别名叫做Non-Heap(非堆)。当方法区无法满足内存分配的需求的时候,抛出OutOfMemoryError异常。
Full GC会回收到方法区的内存。
所有线程共享
- 堆内存:
主要存放new对象出来的信息,以及数组信息
垃圾回收机制算法以及参数调优都是主要针对堆进行的。
内存溢出:OutOfMemory,系统不能再分配出你所需要的空间,比如你需要100M,系统只有90M,这就叫做内存溢出
内存泄漏:Memory Leak,就是使用资源的时候开辟了一段空间,当你用完资源的时候忘记释放,多次会导致内存泄漏。
Java堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动的时候创建。此内存区域的唯一目的就是存放对象实例(包括数组),这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配
Java堆是垃圾收集器管理的主要区域,因此也被称为GC堆。从内存回收的角度来看Java堆分为:新生代和老年代。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有分配缓冲区(TLAB)。无论如何划分都和存放的内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存
根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可以扩展的,通过==-Xmx和-Xms控制==,如果堆中没有内存完成实例分配,并且堆也无法扩展的时候,抛出OutOfMemoryError异常。
- 栈:
基本的数据类型,局部变量,形参等都会存放在栈中。每个线程都独立存在,不会发生线程安全问题
- 本地方法栈
相当于Java语言调用外部语言(Java地层是C语言)
安卓开发的应用层是JavaAPI,底层是C语言,JNI就是Java语言直接调用C语言,效率高
本地方法栈就是相当于Java语言调用外部语言(C语言)
本地方法栈使用native修饰的
本地方法栈底层会调用本地方法接口
CAS其实就是这样,CAS就是通过native修饰的。原子类就是用这个机制的。
- 本地方法接口就是用来走JNI和C语言打交道的,本地方法栈会调用本地方法接口。
垃圾回收机制算法分析
- 垃圾收集系统是JAVA的核心,也是不可少的,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理。
- 什么是垃圾回收机制?
JVM不定时的回收不可达的对象。完全是自动的。
- 什么是不可达对象?
相当于是对象没有被引用或者对象没有存活。表示没有继续被引用到的(彼此引用不算引用)对象,没有被继续使用的对象。
- System.gc()该方法的作用是提示JVM回收垃圾,并不是立即回收,只是提示给垃圾回收机制进行回收,只有在有空的时候才会回收,在GC线程可用的时候,空闲的时候才会回收
- finalize方法
垃圾回收机制之前会执行的方法
- gc线程是一个守护线程
- 垃圾回收机制不定时的去堆内清理不可达的对象。不可达的对象并不会马上就会直接回收,垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,即使程序员明确的判断出有一个内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存块,程序员唯一能做的就是通过调用System.gc方法来建议执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。这也是垃圾收集器最主要的缺点。
新生代与老年代
- 堆内存的划分:新生代和老年代。新生代又分为EDEN区,S0区,S1区,其中S0区和S1区大小是相等的。(目的就是为了复制算法),S0也叫作from Surviror,S1叫做To Surviror
- 新生代:老年代 = 1 : 2 默认是这个比例,我们也可以自己对其进行配置。
- 新生代存放的是刚出生不久的对象,不经常使用的对象。(经常进行垃圾回收的)
- 老年代存放的是活跃的对象,就是经常被引用的对象。(垃圾回收不常回收的)
- 垃圾回收一般是在新生代比较频繁。老年代也有但是不频繁,次数比较少进行垃圾回收
- 当有一个新的对象的时候,会先进入eden区,如果发现其经常使用,放到S0区,然后还是经常使用的话,会将其放到S1区。
如何判断对象存活之引用计数法
- 引用计数法
- 根搜索法(可达性分析法),就是GC Roots
引用计数法现在已经被淘汰掉了,因为会有循环依赖的问题。常用可达性分析法。
(1)引用计数法:
- 每个对象都有一个年龄,GC线程不定时进行回收的时候,如果对象被引用,年龄会加1,如果没有被继续回收,年龄会减一。每个对象默认是0岁。如果最后回收的时候年龄是0岁,就会被垃圾回收机制认为是不可达的对象,会被清理掉。
- 引用计数法就是如果一个对象没有被任何引用指向,则可视为垃圾。这种方法的确定就是不能检测到环的存在。
- 什么是引用计数法:给对象中添加一个引用计算器,每当有一个地方引用他,计数器加一;当引用失效的时候,计数器值减一,任何时刻计数器值为0的对象就是不可能再被调用的时候。会被垃圾回收清理掉、
- 注意一点。主流的JVM中没有选用引用计数法来管理内存。
- 循环依赖其实就是A只有B的引用,B只有A的引用,此时没有别的地方引用A和B,按道理应该被清除,但是引用计数法来管理内存的话,其A和B对象的技计数器不为0,所以无法用GC清理,这就是环形依赖
(2)根搜索算法
- 每个对象都有一个年龄。当对象的年龄小于等于15岁的时候就会存放在新生代中。如果大于15岁的时候,就会存放在老年代中。垃圾回收的时候,有引用就加1岁,没有引用减1岁,默认初始值是0岁,当最后回收的时候,其实0岁,该对象就会被清理掉。
如何判断对象是否存活之GC ROOTS
- 上面就是根搜索法,可达性分析法,GC Roots的原理图
- 根搜索算法的基本思路就是通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的时候,则证明此对象不可用。
- 可以作为GCRoots的对象有哪些?
(1)虚拟机栈(局部变量表)中引用的对象
(2)方法区中的类静态属性引用的对象
(3)方法区中常量引用的对象
(4)本地方法栈JNI引用的对象
垃圾回收机制之标记清除算法 —老年代使用该算法
- 标记清除算法
该算法有两个阶段:
(1)标记阶段:找到所有可以访问的对象,做个标记(利用可达性分析法)
(2)清除阶段:遍历堆,把未做标记的对象回收
- 该算法存在不足之处:
(1)效率问题: 执行的性能不是很高,删除的时候是一个一个删除的
(2)空间问题:标记清除算法之后会禅城大量的不连续的内存碎片。空间碎片太多可能导致以后再程序运行过程中需要分配较大的对象的时候,无法找到连续的内存而不得不触发一次垃圾收集动作。
- 标记清除算法应用场景:
在老年代中使用,因为回收不频繁
- 标记清除算法的优缺点:
- 优点:
- 是可以解决循环引用的问题
- 必要的时候才会进行回收(内存不足的时候),常在老年代中使用标记清除算法
- 缺点:
- 回收的时候,应用需要挂起,即stop the world
- 标记和清除的效率不高,尤其是要扫描的对象比较多的时候
- 会造成内存碎片(会导致明明有内存空间,但是由于不连续,申请稍微大一点的对象的时候无法做到)
标记压缩算法(也叫标记整理算法)—老年代使用该算法
- 标记清除算法和标记压缩算法非常类似,但是标记压缩算法在标记清除算法之上解决内存碎片化问题。
- 标记压缩算法相当于是在清理之前做了整理,将可达的对象放在了一端,不可达的对象放在了另一端,这样清除的时候可以进行批量清除,不会造成碎片化的问题
- 标记压缩算法的简单介绍:
- 任意顺序:即不考虑原先对象的顺序,也不考虑对象之间的引用关系,随意移动对象;
- 线性顺序:考虑对象的引用关系,例如a对象引用了b对象,则尽可能得到将a和b移动到一块;
- 滑动顺序:按照对象原来在堆中的顺序滑动到堆的一端;
- 优缺点:
- 优点: 解决了内存碎片的问题,删不干净
- 缺点:由于在压缩阶段,移动了可用的对象,需要去更新引用
- 主要针对老年代回收内存,存活率高,回收较少
复制算法—新生代使用该算法
- 堆内存的划分
(1)分为新生代和老年代
(2)新生代存活率低,老年代的存活率高
(3)新生代又分为eden区,S0区(from surviror),s1区(to surviror),其中s0区和s1区大小相等。
(4)eden区:s0+s1 = 1:1
(5)s0区和s1区大小相等的原因就是因为复制算法
(6)重要 : 新对象先进入Eden区,如果对象经常被使用再进入S0区,如果S0区的对象有的不可达,有的可达,那么可达的对象会进入到S1区,然后直接清除掉S0区的内存,此时S0区没有任何数据,如果这时候再新来一个对象,先进入Eden区,如果该对象长时间存活,注意,此时不会进入S0区,而是直接进入到S1区,因为这时候的S0区没有对象,这时候S1区有的对象可达有的不可达的时候,进行垃圾回收,会将可达的对象放到S0区,直接清除掉S1区中的内存。说白了就是S0区和S1区换着来回清理。
S0区和S1区有一个一定是空的,目的就是为了存放下一次复制(将可达的(存活的对象)复制到空的地方去)
- 优点:解决碎片化的问题,快捷,清理干净
- 缺点:浪费内存空间
- 复制算法常用在新生代中
- 下图是一次回收的过程
- 详细过程:
- 当Eden区满的时候,会触发第一次young gc,把活着的对象拷贝到Surviror From区;当Eden区再次触发young gc的时候,会扫描Eden区和Surviror From区域,对两个区域进行垃圾回收,经过这次回收后活着的对象,则直接复制到Surviror To区域,并将Eden区域和From区域清空。
- 当后续Eden又发生young gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域进行清空。
- 可见部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM的参数:MaxTenuringThreshold决定,这个参数默认是15)最终如果还是存活的话,就直接进入老年代
注意: 玩意存活的对象数量比较多,那么To区域的内存可能不够,会借助老年代的空间。
分代算法
- 这种算法,根据对象的存活周期的不同将内存划分成几块,新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。
- 新生代的对象存活率低,对象数量多,重点扫描这个区域,那么就可以大大提高垃圾收集器的效率。另外老年代对象存储久,无需经常扫描老年代,避免扫描导致的开销。
- 通常我们采用的是复制算法+标记整理算法,新生代采用复制算法,老年代采用标记整理算法。
Minor GC 和 Full GC的区别
- Minor GC
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具有朝生夕死的特性,所以Minor GC非常的频繁,一般回收速度也比较快
- Major GC/ Full GC
老年代GC,指的是发生在老年代里面的GC,出现了Major GC,通常会伴随着一次Minor GC(不是绝对的)。Major GC通常比Minor GC慢上10倍。
- Minor GC触发机制
当年轻代满时,会触发Minor GC,这里的年轻代指的是Eden满, Surviror不会引发,因为对象每次都是先进入的Eden区域。
- Full GC触发机制
当老年代满的时候,会触发Full GC。Full GC同时会回收年轻代,老年代,当永久代(永久代指的是方法区;年轻代,老年代都是堆)满的时候,也会触发Full GC。
补充:
- JVM的永久代会发生垃圾回收么?
==垃圾回收不会发生在永久代中。==如果永久代满了或者超过了临界值,会触发完全垃圾回收(Full GC),只是触发,并不发生在此。
Java8中移除了永久代,新加了一个叫nativr的内存区。