【声明】

欢迎转载,但请保留文章原始出处→_→

文章来源:【http://www.cnblogs.com/smyhvae/p/4748392.html】

文章来源:【http://www.cnblogs.com/smyhvae/p/4810168.html】

【正文】

声明:本文只针对Java虚拟机的内存划分、以及垃圾回收机制的一些总结,对于有可能涉及到Java中的四中引用状态、累的加载机制这里不做探讨。

先把本文的目录画一个思维导图:

  

一、Java中的内存划分:

  Java程序在运行时,需要在内存中的分配空间。为了提高运算效率,就对数据进行了不同空间的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。

具体划分为如下5个内存空间:(非常重要)

  

  1、Java虚拟机栈内存(线程私有): 

      栈内存为虚拟机执行java方法服务:方法被调用时创建栈帧-->局部变量表->局部变量、对象引用,

      局部变量表存放了基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(slot),其余数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配。

        

      上方代码中,静态方法有6个形参,实例方法有3个形参。其对应的局部变量表如下:

        

      每个方法被调用的时候都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。局部变量表存放的是:编译期可知的基本数据类型、对象引用类型。

      每个方法被调用直到执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。

      在Java虚拟机规范中,对这个区域规定了两种异常情况:

        (1)如果线程请求的栈深度太深,超出了虚拟机所允许的深度,就会出现StackOverFlowError(比如无限递归。因为每一层栈帧都占用一定空间,而 Xss 规定了栈的最大空间,超出这个值就会报错)

        (2)虚拟机栈可以动态扩展,如果扩展到无法申请足够的内存空间,会出现OOM

  2、堆内存(线程共享):

      存放所有new出来的对象,以及数组

        (1)堆是java虚拟机所管理的内存区域中最大的一块,java堆是被所有线程共享的内存区域,在java虚拟机启动时创建,堆内存的唯一目的就是存放对象实例几乎所有的对象实例都在堆内存分配。

        (2)堆是GC管理的主要区域,从垃圾回收的角度看,由于现在的垃圾收集器都是采用的分代收集算法,因此java堆还可以初步细分为新生代和老年代

        (3)Java虚拟机规定,堆可以处于物理上不连续的内存空间中,只要逻辑上连续的即可。在实现上既可以是固定的,也可以是可动态扩展的。如果在堆内存没有完成实例分配,并且堆大小也无法扩展,就会抛出OutOfMemoryError异常。

  3、方法区(线程共享):

      存放被存jvm加载的类的信息,静态变量,常量,静态常量、静态方法、(static或final修饰的变量、方法)

        (1)用于存储已被虚拟机加载的类信息、常量、静态变量、静态常量、静态方法、即时编译器编译后的代码等数据。

        (2)Sun HotSpot虚拟机把方法区叫做永久代(Permanent Generation),方法区中最终要的部分是运行时常量池。

  4、本地方法栈:

      为虚拟机执使用到的Native方法服务

        (1)本地方法栈与java虚拟机栈作用非常类似,其区别是:java虚拟机栈是为虚拟机执行java方法服务的,而本地方法栈则为虚拟机执使用到的Native方法服务

        (2)Java虚拟机没有对本地方法栈的使用和数据结构做强制规定,Sun HotSpot虚拟机就把java虚拟机栈和本地方法栈合二为一。

        (3)本地方法栈也会抛出StackOverFlowError和OutOfMemoryError。

  5、程序计数器(线程私有):

      每个线程拥有一个程序计数器,在线程创建时创建,指向下一条指令的地址执行本地方法时,其值为undefined

      说的通俗一点,我们知道,Java是支持多线程的,程序先去执行A线程,执行到一半,然后就去执行B线程,然后又跑回来接着执行A线程,那程序是怎么记住A线程已经执行到哪里了呢?这就需要程序计数器了。因此为了线程切换后能够恢复到正确的执行位置,每条线程都有一个独立的程序计数器,这块儿属于“线程私有”的内存。

  6、常量池:

       Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池,当常量池无法再申请到内存时就会抛出OutOfMemoryError异常(注:基本数据类型中的flat、doublell类型的包装类,并没有实现常量池)。

      静态常量池:

        即class文件中的常量池,静态常量池不仅仅包含了字符串,还包含了类、方法的信息、占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:

        (1)类和接口的全限定名

        (2)字段名称和描述符

        (3)方法名称和描述符  

      运行时常量池:

        指jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池

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

        String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。
二、Java对象在内存中的状态:
  1、可达的/可触及的:

    Java对象被创建后,如果被一个或多个变量引用,那就是可达的。即从根节点可以触及到这个对象。

    其实就是从根节点扫描,只要这个对象在引用链中,那就是可触及的。

  2、可恢复的:

    Java对象不再被任何变量引用就进入了可恢复状态。

    在回收该对象之前,该对象的finalize()方法进行资源清理。如果在finalize()方法中重新让变量引用该对象,则该对象再次变为可达状态,否则该对象进入不可达状态

  3、不可达的:

    Java对象不被任何变量引用,且系统在调用对象的finalize()方法后依然没有使该对象变成可达状态(该对象依然没有被变量引用),那么该对象将变成不可达状态。

    当Java对象处于不可达状态时,系统才会真正回收该对象所占有的资源。

三、判断对象死亡的两种常用算法:

  在Java中,当对象不被引用的时候,这个对象就是死亡的,等待GC进行回收。

  1、引用计数算法

    概念:

      给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

    但是:

      主流的java虚拟机并没有选用引用计数算法来管理内存,其中最主要的原因是:它很难解决对象之间相互循环引用的问题

    优点:

      算法的实现简单,判定效率也高,大部分情况下是一个不错的算法。很多地方应用到它

    缺点:

      引用和去引用伴随加法和减法,影响性能,致命的缺陷:对于循环引用的对象无法进行回收

  2、根搜索算法:(jvm采用的算法)

    概念:

      设立若干种根对象,当任何一个根对象(GC Root)到某一个对象均不可达时,则认为这个对象是可以被回收的。

    注:

      这里提到,设立若干种根对象,当任何一个根对象到某一个对象均不可达时,则认为这个对象是可以被回收的。我们在后面介绍标记-清理算法/标记整理算法时,也会一直强调从根节点开始,对所有可达对象做一次标记,那什么叫做可达呢?

    可达性分析:

      从根(GC Roots)的对象作为起始点,开始向下搜索,搜索所走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连(用图论的概念来讲,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

        

      如上图所示,ObjectD和ObjectE是互相关联的,但是由于GC roots到这两个对象不可达,所以最终D和E还是会被当做GC的对象,上图若是采用引用计数法,则A-E五个对象都不会被回收。

    根(GC Roots):

      说到GC roots(GC根),在JAVA语言中,可以当做GC roots的对象有以下几种:

      (1)、(栈帧中的本地变量表)中引用的对象

      (2)、方法区中的静态成员。

      (3)、方法区中的常量引用的对象(全局变量)

      (4)、本地方法栈中JNI(一般说的Native方法)引用的对象。

      注:第一和第四种都是指的方法的本地变量表,第二种表达的意思比较清晰,第三种主要指的是声明为final的常量值。

      在根搜索算法的基础上,现代虚拟机的实现当中,垃圾搜集的算法主要有三种,分别是标记-清除算法复制算法标记-整理算法。这三种算法都扩充了根搜索算法,不过它们理解起来还是非常好理解的。

四、垃圾回收的算法:

  1、标记-清除算法:

    概念:

      标记阶段:先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象;

      清除阶段:清除所有未被标记的对象。

    缺点:

      标记和清除的过程效率不高(标记和清除都需要从头遍历到尾)

      标记清除后会产生大量不连续的碎片。 

  2、复制算法:(新生代的GC)

    概念:

      将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,然后清除正在使用的内存块中的所有对象。

    优点:

      这样使得每次都是对整个半区进行回收,内存分配时也就不用考虑内存碎片等情况

      只要移动堆顶指针,按顺序分配内存即可,实现简单,运行效率高

    缺点:空间的浪费

      从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行。

      现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%都是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块比较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是说,每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的空间会被浪费。

      

  3、标记-整理算法:(老年代的GC)

    复制算法在对象存活率高的时候要进行较多的复制操作,效率将会降低,所以在老年代中一般不能直接选用这种算法。

    概念:

      标记阶段:先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象

      整理阶段:将所有的存活对象压缩到内存的一端;之后,清理边界外所有的空间

    优点:

      不会产生内存碎片。

    缺点:

      在标记的基础之上还需要进行对象的移动,成本相对较高,效率也不高。

    它们的区别如下:(>表示前者要优于后者,=表示两者效果一样)

      (1)效率:复制算法 > 标记/整理算法 > 标记/清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。

      (2)内存整齐度:复制算法=标记/整理算法>标记/清除算法。

      (3)内存利用率:标记/整理算法=标记/清除算法>复制算法。

      注1:标记-整理算法不仅可以弥补标记-清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。

      注2:可以看到标记/清除算法是比较落后的算法了,但是后两种算法却是在此基础上建立的。

      注3:时间与空间不可兼得。

  4、分代收集算法:

    当前商业虚拟机的GC都是采用的“分代收集算法”,这并不是什么新的思想,只是根据对象的存活周期的不同将内存划分为几块儿。一般是把Java堆分为新生代和老年代:短命对象归为新生代,长命对象归为老年代。

    存活率低:少量对象存活,适合复制算法:在新生代中,每次GC时都发现有大批对象死去,只有少量存活(新生代中98%的对象都是“朝生夕死”),那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成GC。

    存活率高:大量对象存活,适合用标记-清理/标记-整理:在老年代中,因为对象存活率高、没有额外空间对他进行分配担保,就必须使用“标记-清理”/“标记-整理”算法进行GC。

    注:老年代的对象中,有一小部分是因为在新生代回收时,老年代做担保,进来的对象;绝大部分对象是因为很多次GC都没有被回收掉而进入老年代。

java 虚拟机内存模型的更多相关文章

  1. Java虚拟机内存模型及垃圾回收监控调优

    Java虚拟机内存模型及垃圾回收监控调优 如果你想理解Java垃圾回收如果工作,那么理解JVM的内存模型就显的非常重要.今天我们就来看看JVM内存的各不同部分及如果监控和实现垃圾回收调优. JVM内存 ...

  2. Java虚拟机--内存模型与线程

    Java虚拟机--内存模型与线程 高速缓存:处理器要与内存交互,如读取.存储运算结果,而计算机的存储设备和处理器的运算速度差异巨大,所以加入一层读写速度和处理器接近的高速缓存来作为内存和处理器之间的缓 ...

  3. Java虚拟机 - 内存模型

    本文主要介绍Java虚拟机的内存分布以及对象的创建过程. 一.Java虚拟机的内存分布 文章开始前读者需要了解Java虚拟机的运行时数据区是怎样划分的.如下图所示: 1.程序计数器(Program C ...

  4. JAVA虚拟机内存模型

    一.对于Java程序员来说,在虚拟机的自动内存管理机制下,我们不需要为每一个new操作去写匹配的delete/free操作 但是当我们对于内存的管理了解有能够帮助我们理解Java虚拟机的垃圾回收机制. ...

  5. 深入理解Java虚拟机内存模型

    前言 本文中部分内容引用至<深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)>第12章,如果有兴趣可自行深入阅读,文末放有书籍PDF版本连接. 一.物理机中的并发 物理机遇到的并 ...

  6. 面试常问的Java虚拟机内存模型,看这篇就够了!

    一.虚拟机 同样的java代码在不同平台生成的机器码肯定是不一样的,因为不同的操作系统底层的硬件指令集是不同的. 同一个java代码在windows上生成的机器码可能是0101.......,在lin ...

  7. Java虚拟机03(Java虚拟机内存模型)

    根据 JVM 规范,JVM 内存共分为虚拟机栈.堆.方法区.程序计数器.本地方法栈五个部分. 其实最需要Java程序员关注的是堆,栈,还有方法区,因为啊: 如果代码又问题的话,可能回出现栈溢出 然后说 ...

  8. 作业1:java虚拟机内存模型图示

    看了很多篇文章,整理成一幅图,但仍然有许多不解的地方,以后再接着完善,哪位大神看到不正确的地方,请指出,谢谢.

  9. 栈帧 2.6. Frames 虚拟机内存模型

    https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-2.html#jvms-2.6 小结: 1. https://docs.oracle. ...

随机推荐

  1. PHP Composer 依赖管理的用法

    1:下载 1.1:方法一: 通过PHP来安装 cd G:\web\es6 curl -sS https://getcomposer.org/installer | php #这个命令会下载compos ...

  2. openjduge 求简单表达式的值

    表达式求值 总时间限制:  10000ms  单个测试点时间限制:  1000ms  内存限制:  131072kB 给定一个只包含加法和乘法的算术表达式,请你编程计算表达式的值. 输入 输入仅有一行 ...

  3. windows环境下手动安装Mysql8

    1.下载zip包,解压到c:\java_env_mysql8下,并把c:\java_env_mysql8\bin放入到系统path变量中 2.编写my.ini文件,放置于 c:\java_env_my ...

  4. Spring笔记02_注解_IOC

    目录 Spring笔记02 1. Spring整合连接池 1.1 Spring整合C3P0 1.2 Spring整合DBCP 1.3 最终版 2. 基于注解的IOC配置 2.1 导包 2.2 配置文件 ...

  5. USSD 杂记

    Android Oreo允许应用程序读取来自运营商的USSD消息. 利用emoney执行话费充值,需要执行USSD代码,尝试编写apk执行ussd代码进行充值. 尝试在Android8的系统上进行US ...

  6. prufer序列笔记

    prufer序列 度娘的定义 Prufer数列是无根树的一种数列.在组合数学中,Prufer数列由有一个对于顶点标过号的树转化来的数列,点数为n的树转化来的Prufer数列长度为n-2. 对于一棵确定 ...

  7. Django的urls.py加载静态资源图片,TypeError: view must be a callable or a list/tuple in the case of include().

    Django的urls.py加载静态资源图片,TypeError: view must be a callable or a list/tuple in the case of include(). ...

  8. JS table内容转成二维数组,支持colspan和rowspan

    思路:1.先初始化colspan的数据到数组2.根据rowspan和colspan计算th和td的矩阵二次填充数组 说明:需要引用到第三方库jQuery,table中的th和td行和列跨度必须正确 & ...

  9. 请不要在广州.NET俱乐部群里谈郑智话题,谢谢!

    收到私聊说代码无国界,但是程序员是有国界的,程序员也有关心国家大事的权力!   我是极度赞同“代码无国界,但是程序员是有国界的,”我也极度赞同程序员也要爱国. 因为我们新兴县六祖镇叶氏家族就有四位爱国 ...

  10. Jmeter设置代理,抓包之app请求

    步骤: 1. Jmeter选择测试计划,添加线程组,添加http请求,添加监听器-察看结果树 2. 添加http代理服务器,右键添加非测试元件-添加http代理服务器 3. 端口改为8889,目标控制 ...