美文网首页
Java内存机制

Java内存机制

作者: 叶安得广 | 来源:发表于2019-11-27 20:18 被阅读0次

    版权声明:本文为博主原创文章,未经博主允许不得转载,如转载请标明出处

    一,序言

        了解java虚拟机是如何分配回收内存的,如有不对之处请大家多多指教

    二,Java内存分配

        Java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。

    图1

    1.方法区(公有)

        用户存储已被虚拟机加载的类信息常量静态常量,即时编译器编译后的代码等数据,其中包含常量池:用户存放编译器生成的各种字面量符号引用以及运行时添加的常量。异常状态 OOM(OutOfMemoryError)

        注意:JDK 6 时,String等字符串常量的信息是置于方法区中的,但是到了JDK 7 时,已经移动到了Java堆。所以,方法区也好,Java堆也罢,到底详细的保存了什么,其实没有具体定论,要结合不同的JVM版本来分析。

    1.1类信息

    类型全限定名
    类型的直接超类的全限定名(除非这个类型是java.lang.Object,它没有超类)
    类型是类类型还是接口类型
    类型的访问修饰符(public、abstract或final的某个子集)
    任何直接超接口的全限定名的有序列表
    类型的常量池
    字段信息
    方法信息
    除了常量以外的所有类(静态)变量
    一个到类ClassLoader的引用
    一个到Class类的引用

    1.2常量池

    1.2.1class文件中常量池

    public class HelloWorld { 
        public static void main(String[] args) { 
            // TODO Auto-generated method stub 
            System.out.println("Hello World");
         } 
    }

    用winhex打开HelloWorl的class文件,如下

    图2

    以上是class文件结构,挨着看一下

        魔数(Megic Number):开头4个字节,唯一作用是来确定该文件是否被JVM接受

        版本号:4个字节,前两个为次版本号,后两个为主版本号。这里为0x0034,十进制为52。Java的版本是从45开始的然而从1.0 到1.1 是45.0到45.3, 之后就是1.2 对应46, 1.3 对应47 … 1.6 对应50,我的是1.8刚好对应52

        常量池入口:0x0022,34个常量

        常量池:主要存放两类常量:字面量和符号引用。字面量比较接近Java语言层面的常量概念。就是我们什么提到的常量。而符号引用则属于编译原理的方面的概念。包括以下三类常量:

        类和接口的全限定名;字段的名称和描述符;方法的名称和描述符

    1.2.2运行时常量池:

        上述class文件中的常量池的内容会在类加载后进入方法区中的运行时常量池中存放。运行时常量池相对于class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。

    public class Test_1 { 
        public static void main(String[] args) {
             String s1 = new String("计算机"); //创建了2个对象
             String s2 = s1.intern(); 
             String s3 = "计算机"; 
             System.out.println("s1 == s2? " + (s1 == s2)); //false
             System.out.println("s3 == s2? " + (s3 == s2)); //true
        }
    }

        String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。

    图3

    2.堆(公有)

        是JVM所管理的内存中最大的一块。唯一目的就是存放实例对象,几乎所有的对象实例都在这里分配。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。异常状态OutOfMemoryError

    3.虚拟机栈(私有)

        描述的是java方法执行的内存模型:每个方法在执行时都会创建一个栈帧,用户存储局部变量表,操作数栈,动态连接,方法出口等信息。每一个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。 对这个区域定义了两种异常状态 OutOfMemoryError StackOverflowError(比如在递归算法中,经常会写一个层级控制

    4.本地方法栈(私有)

        与虚拟机栈所发挥的作用相似。它们之间的区别不过是虚拟机栈为虚拟机执行java方法,而本地方法栈为虚拟机使用到的Native方法服务。

    5.程序计数器(线程私有)

        一块较小的内存,当前线程所执行的字节码的行号指示器。字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

    三,Java内存回收机制

    这里是针对堆来讲的,我们要弄清楚三个问题

     哪些内存需要回收?
     回收的时机 ?
     如何回收?

    1.回收对象

        对象是否可以被回收的两种经典算法: 引用计数法 和 可达性分析算法

      1.1引用计数法

    图4

        原理很简单,引用为0则回收。如图所示,对象A有3个引用,B为0

        1.2可达性分析算法

        通过一些列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(就是从GC Roots 到这个对象是不可达),则证明此对象是不可用的,所以它们会被判定为可回收对象。

    图5

    在Java语言中,可以作为GC Roots的对象很多,详见传送门:Help - Eclipse Platform

    在这里主要列一下我们会经常碰到的4种

    虚拟机栈(栈帧中的本地变量表)中引用的对象;
    方法区中类静态属性引用的对象;
    方法区中常量引用的对象;
    本地方法栈中JNI(即一般说的Native方法)引用的对象

    1.3引用类型

        在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。

    强引用: 就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

    软引用: 用来描述一些还有用但并非必须的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。

    弱引用: 用户描述非必须对象的。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

    虚引用: 一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时刻得到一个系统通知。

    2.回收时机

        JVM中分为年轻代(Young generation)和老年代(Tenured generation),年轻代中的对象朝生夕死的,老年代中的对象都是常青树。HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)默认比例为8:1。 同年轻代和老年代分别对应的回收机制是Minor GC和Full GC。

        Minor GC:指发生在新生代的垃圾收集动作,该动作非常频繁。

        Full GC/Major GC:指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC。Major GC的速度一般会比Minor GC慢10倍以上。

        一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

     /**
     * -XX:+PrintGCDetails 
    * @author Administrator
     * 
    */ 
    public class GcTest1 { 
    private static final int _1MB = 1024*1024; 
    private byte[] mBigSize = new byte[2 * _1MB];//这个成员属性的意义是占点内存,以便能在GC日志中看清楚是否被回收过 
        public static void main(String[] args) { 
            //test1(); 
           // System.gc();
         } 
        private static void test1() { 
            GcTest1 rcg1 = new GcTest1();
         }
     }

    打印一下GC日志

    图6

    在这里,我们看到只有年轻代使用了3871kb,接着调用一下test1方法,打印GC日志

    图7

    可以看到年轻代刚好增加了2048kb

    3.回收方法

    三种经典垃圾回收算法(标记清除算法、复制算法、标记整理算法)及分代收集算法

    3.1标记-清除算法

    首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

    缺点:效率问题:标记和清除两个过程的效率都不高

               空间问题:标记清除之后产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序运 行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

    3.2复制算法

    将可用内存按容量大小划分为大小相等的两块,每次只使用其中的一块。当一块内存使用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。

    缺点: 将内存缩小为了原来的一半。

    3.3标记-整理算法

    复制收集算法在对象存活率较高时,就要进行较多的复制操作,效率就会变低。 根据老年代的特点,提出了“标记-整理”算法。标记过程仍然与”标记-清除“算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。

    3.4分代收集算法

    一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清除”或“标记-整理”算法来进行回收。

    四,总结

    到这里,java内存的分配和回收大致就讲完了,在这基础之上我们可以去处理内存泄漏之类的问题

    五,参考文章

    JVM解读-方法区 - 简书

    图解Java 垃圾回收机制

    Android内存泄漏场景及解决方法 - 简书

    ps:除上三文,本文还参考了部分的文章,不过忘了=-= 如有作者发现,请联系本人

    相关文章

      网友评论

          本文标题:Java内存机制

          本文链接:https://www.haomeiwen.com/subject/edzvgqtx.html