JVM由以下几个管理区组成:
程序计数器(存放于CPU很小的一块内存中):简单来讲,程序计数器是jvm用来记录程序的运行位置的,在JVM中,程序也是通过指令来一步一步运行的,在多个线程中,同一个方法在同一个时间内,一个内核只能执行一个指令,所以,对于每一个程序来说,必须要有记录他运行的执行进度,所以说,在每一个线程中,都会有他的一个程序计数器,如果线程是在执行java方法,那么程序计数器指向的就是字节码的指令位置,如果是执行(Native)本地的方法,那么计数器为空。这块内存区域是一块不会存在超出内存的唯一一块区域。(线程私有)
JVM虚拟机栈(JVM stacks):JVM虚拟机栈就是我们经常说的堆栈的栈,在每一个方法执行的时候,都会有一产生一个栈帧(底下有介绍),用于存放方法的局部变量表,方法出口,动态链接,操作数等信息,方法的执行过程就是栈帧在JVM中出栈入栈的过程,局部变量表中存放的是各种基本数据类型,如boolean、byte、char、等8种,及引用类型(存放的是指向各个对象的内存地址),因此,它有一个特点:内存空间可以在编译期间就确定,运行期不在改变。这个内存区域会有两种可能的Java异常:StackOverFlowError和OutOfMemoryError。 (线程私有)
本地方法栈:用来执行一些本地的方法的,如Java的祖先类object中有众多Native方法,如hashCode()、wait()等,他们的执行很多时候是借助于操作系统,但是JVM需要对他们做一些规范,来处理他们的执行过程.(线程私有)
堆:堆内存是内存中最重要的一块,所有的对象实例和数组的分配都是在堆中分配的,比如我们在执行一个jar的时候,就利用了-Xmx -Xms来控制了堆的大小,由于存放了一些对象的信息,所以它是垃圾回收机制GC中回收的主要区域。32位系统中最大为2G,64位无限制。
方法区:方法区是所有线程共享的内存区域,用于存储已经被JVM加载的类信息、常量、静态变量等数据,一般来说,方法区属于持久代(关于持久代,会在GC部分详细介绍,除了持久代,还有新生代和旧生代),此处引入方法区中一个重要的概念:运行时常量池。主要用于存放在编译过程中产生的字面量(字面量简单理解就是常量)和引用。一般情况,常量的内存分配在编译期间就能确定,但不一定全是,有一些可能就是运行时也可将常量放入常量池中
栈帧:
- 函数的返回地址和参数
- 临时变量: 包括函数的非静态局部变量以及编译器自动生成的其他临时变量
- 函数调用的上下文
垃圾回收:
在jvm的内存区域中,JVM虚拟机栈,程序计数器,本地方法栈这3个线程私有的东西,都是随着线程的销毁而销毁的,所以主要的GC区域就在于堆和方法区。
GC的时机:sun的JVM没有采用传统的引用计数功能来标记对象(使用加1,没使用-1,但是存在相互引用就不会被释放),而是采用根搜索算法:
根搜索算法:
GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。
从上图,reference1、reference2、reference3都是GC Roots,可以看出:
reference1-> 对象实例1;
reference2-> 对象实例2;
reference3-> 对象实例4;
reference3-> 对象实例4 -> 对象实例6;
可以得出对象实例1、2、4、6都具有GC Roots可达性,也就是存活对象,不能被GC回收的对象。
而对于对象实例3、5直接虽然连通,但并没有任何一个GC Roots与之相连,这便是GC Roots不可达的对象,这就是GC需要回收的垃圾对象。
GC Roots”的对象:
(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2). 方法区中的类静态属性引用的对象。
(3). 方法区中常量引用的对象。
(4). 本地方法栈中JNI(Native方法)引用的对象。
Native方法: 就是一个java调用非java代码的接口
引用的概念:
强引用:即刚new出来的对象,不会被回收
软引用:可被回收的对象,但是如果JVM内存不紧张,不会被回收,内存紧张的情况就会被回收,主要指的是缓存中的对象。
弱引用:一定会被回收
虚引用:JVM不会管虚引用,因为他主要是做一些跟踪记录,辅助finalize函数的使用。
如何进行垃圾回收:
标记清除法:先把可回收的对象标记,然后清理掉,但是容易造成内存碎片,因为内存空间是连续的,如果你分配了一个1M的空间,那么你下次分配4M的内存,就会导致内存碎片的产生。
复制算法:将内存分为两块,当A用完了,那么将A这一块的存活对象复制到另外一块B,然后在全部清理掉A的内存,最后在从B搬到A中
标记整理算法:在标记清除的算法上进一步升级,先标记,然后在进行整理,将所有的存活对象移动在一块,在清理掉内存外的区域。
内存模型与回收策略:
java堆中分有两个区域,分别为老年代与新生代,有的还有持久代(这边不讨论),新生代中分有Eden区域与survival区域,survival区域又有From与TO两个区域,比例为8:1:1。
Eden 区:
大多数情况下,对象会在Eden区域中进行分配,如果区域不足了,会触发一次minor GC,具体的过程为,采用复制算法,将Eden区域中存活的对象放在Form区域中,(如果超出去,那么将会放在old区域),接着在把Eden区域清理完毕,如果在满,那么就会在触发第二次GC,将Eden和From的区域存活对象复制到TO区域,然后清理Eden与From,以此类推,第三次就是将Eden与To放到From,。。。直到两者区域都满了,那么就会去占用老年代的区域,如果老年代区域也满了,那么就会触发一次大的GC。(注意,在每次的minorGC中,会有一个计数器,添加GC的回收次数,如果次数超过15,那么对象将会被放到老年代中。或者如果在survival区域中有相同年龄所有对象大小的总合大于 Survivor 空间的一半,也会被放到老年代。)
Survivor 区
Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,Survivor 又分为2个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(如果 To 区不够,则直接进入 Old 区)
Old 区(大对象会直接放入Old区域)
老年代占据着2/3的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记 --- 整理算法。