精华内容
下载资源
问答
  • Java 内存调整技巧

    2011-01-06 10:52:57
    Java 内存调整技巧
  • Tomcat调整java jvm内存大小

    千次阅读 2017-11-29 21:37:00
    原因:因为本人做的项目出现如下异常 ...所以尝试调整tomcat内存,是否能够解决问题不清楚,记录下调整tomcat内存的方法 ============================ 环境:windows7 tomcat7 ============================ ...

    原因:因为本人做的项目出现如下异常

    java.lang.OutOfMemoryError: GC overhead limit exceeded

    所以尝试调整tomcat内存,是否能够解决问题不清楚,记录下调整tomcat内存的方法

    ============================

    环境:windows7

    tomcat7

    ============================

    启动方式:

    1,把tomcat做成windows服务,通过windows服务启动(第一种居多)

    2,通过tomcat的startup.bat启动

    ============================

    通过

    1,把tomcat做成windows服务,通过windows服务启动

    -----

    1.1首先进入到E:\apache-tomcat-7.0.73\bin(路径以自己的tomcat为准,进入bin即可)

    1.2找到tomcat7w.exe(跟tomcat7版本有关,叫法相似)这个,双击点开,选择java,可以看到又两个值,修改这两个值即可

    如图:

    注意:如果需要具体的配置。可以百度,然后把详细配置黏贴在java options中,但是可能会启动不了tomcat服务了

    ========================================================

     通过

    2,通过tomcat的startup.bat启动

    可以将如下一行

     set JAVA_OPTS=-Xms1024m -Xmx2048m -XX:PermSize=256m -XX:MaxPermSize=512m

    复制黏贴到

    E:\apache-tomcat-7.0.73\bin目录下,找到catalina.bat,双击打开,把上一行复制黏贴到第一行即可。

    ========================================================

    如何判断自己的tomcat内存修改成功?

    1,通过tomcat的图形界面

    2,通过jvisualvm.exe

    ========================================================

    1,通过tomcat的图形界面(注意,必须要保留tomcat的webapps中的初始文件,可解压一个新的tomcat复制黏贴进去)

    1.1首先,进入E:\apache-tomcat-7.0.73\conf目录

    找到tomcat-users.xml文件,添加如下一行

    <role rolename="manager-gui"/>
    <user username="admin" password="admin" roles="manager-gui"/>

    1.2启动tomcat的windows服务

    1.3打开浏览器输入http://localhost:8080/manager/status

    1.4输入账号密码,也就是刚才复制黏贴的

    1.5观察tomcat的内存变化

    ==========================================================

    2,通过jvisualvm.exe(注意:通过windows服务启动的话,无法看到tomcat,需要通过startup.bat启动才可以看到)

    2.1进入jdk的安装目录,找到E:\Java\jdk1.8.0_121\bin(以自己的目录为准,进入bin文件夹)

    2.2找到jvisualvm.exe,双击打开,可以看到tomcat,双击它,观察内存的变化

     

    展开全文
  • 本来打算自己写个总结,但是看了一篇文章后,自觉很难超出其文,就直接copy过来啦 ,然后本地写了大概笔记,算是...Java垃圾回收概况Java内存区域Java对象的访问方式Java内存分配机制Java GC机制垃圾收集器 Java垃圾

    本来打算自己写个总结,但是看了一篇文章后,自觉很难超出其文,就直接copy过来啦 ,然后本地写了大概笔记,算是了解大笑大笑

    摘自 http://www.cnblogs.com/hnrainll/archive/2013/11/06/3410042.html

    更可参考 http://www.th7.cn/Program/java/201409/276272.shtml

    目录

    1. Java垃圾回收概况
    2. Java内存区域
    3. Java对象的访问方式
    4. Java内存分配机制
    5. Java GC机制
    6. 垃圾收集器

    Java垃圾回收概况

      Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代 码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。概括地说,该机制对 JVM(Java Virtual Machine)中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证JVM中的内存空间,放置出现内存泄露和溢出问题。

      关于JVM,需要说明一下的是,目前使用最多的Sun公司的JDK中,自从 1999年的JDK1.2开始直至现在仍在广泛使用的JDK6,其中默认的虚拟机都是HotSpot。2009年,Oracle收购Sun,加上之前收购 的EBA公司,Oracle拥有3大虚拟机中的两个:JRockit和HotSpot,Oracle也表明了想要整合两大虚拟机的意图,但是目前在新发布 的JDK7中,默认的虚拟机仍然是HotSpot,因此本文中默认介绍的虚拟机都是HotSpot,相关机制也主要是指HotSpot的GC机制。

      Java GC机制主要完成3件事:确定哪些内存需要回收,确定什么时候需要执行GC,如何执行GC。经过这么长时间的发展(事实上,在Java语言出现之前,就有 GC机制的存在,如Lisp语言),Java GC机制已经日臻完善,几乎可以自动的为我们做绝大多数的事情。然而,如果我们从事较大型的应用软件开发,曾经出现过内存优化的需求,就必定要研究 Java GC机制。

      学习Java GC机制,可以帮助我们在日常工作中排查各种内存溢出或泄露问题,解决性能瓶颈,达到更高的并发量,写出更高效的程序。

      我们将从4个方面学习Java GC机制,1,内存是如何分配的;2,如何保证内存不被错误回收(即:哪些内存需要回收);3,在什么情况下执行GC以及执行GC的方式;4,如何监控和优化GC机制。

    Java内存区域

      了解Java GC机制,必须先清楚在JVM中内存区域的划分。在Java运行时的数据区里,由JVM管理的内存区域分为下图几个模块:

    其中:

    1,程序计数器(Program Counter Register):程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。

      每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。

      如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写 完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区 域中唯一一个没有定义OutOfMemoryError的区域。

    2,虚拟机栈(JVM Stack):一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。

      局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占 用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定 好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。

      虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多 数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,知道内存不足,此时,会抛出 OutOfMemoryError(内存溢出)。

      每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的。

    3,本地方法栈(Native Method Statck):本地方法栈在作用,运行机制,异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。

      本地方法栈也是线程私有的。

    4,堆区(Heap):堆区是理解Java GC机制最重要的区域,没有之一。在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存(不过现代技术里,也不是这么绝对的,也有栈上直接分配的)。

      一般的,根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前主 流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。

      关于堆区的内容还有很多,将在下节“Java内存分配机制”中详细介绍。

    5,方法区(Method Area):在Java虚拟机规范中,将方法区作为堆的一个逻辑部分来对待,但事实 上,方法区并不是堆(Non-Heap);另外,不少人的博客中,将Java GC的分代收集机制分为3个代:青年代,老年代,永久代,这些作者将方法区定义为“永久代”,这是因为,对于之前的HotSpot Java虚拟机的实现方式中,将分代收集的思想扩展到了方法区,并将方法区设计成了永久代。不过,除HotSpot之外的多数虚拟机,并不将方法区当做永 久代,HotSpot本身,也计划取消永久代。本文中,由于笔者主要使用Oracle JDK6.0,因此仍将使用永久代一词。

      方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。

      方法区在物理上也不需要是连续的,可以选择固定大小或可扩展大小,并且方法区比堆还多了一个限制:可以选择是否执行垃圾收集。一般的,方法区上 执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一(HotSpot),但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对 常量池的内存回收和对已加载类的卸载。

      在方法区上进行垃圾收集,条件苛刻而且相当困难,效果也不令人满意,所以一般不做太多考虑,可以留作以后进一步深入研究时使用。

      在方法区上定义了OutOfMemoryError:PermGen space异常,在内存不足时抛出。

      运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译;运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)。

    6,直接内存(Direct Memory):直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是 JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区 (Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。 由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。

    Java对象的访问方式

    一般来说,一个Java的引用访问涉及到3个内存区域:JVM栈,堆,方法区。

      以最简单的本地变量引用:Object obj = new Object()为例:

    • Object obj表示一个本地引用,存储在JVM栈的本地变量表中,表示一个reference类型数据;
    • new Object()作为实例对象数据存储在堆中;
    • 堆中还记录了Object类的类型信息(接口、方法、field、对象类型等)的地址,这些地址所执行的数据存储在方法区中;

    在Java虚拟机规范中,对于通过reference类型引用访问具体对象的方式并未做规定,目前主流的实现方式主要有两种:

    1,通过句柄访问(图来自于《深入理解Java虚拟机:JVM高级特效与最佳实现》):

    通过句柄访问的实现方式中,JVM堆中会专门有一块区域用来作为句柄池,存储相关句柄所执行的实例数据地址(包括在堆中地址和在方法区中的地址)。这种实现方法由于用句柄表示地址,因此十分稳定。

    2,通过直接指针访问:(图来自于《深入理解Java虚拟机:JVM高级特效与最佳实现》)

    通过直接指针访问的方式中,reference中存储的就是对象在堆中的实际地址,在堆中存储的对象信息中包含了在方法区中的相应类型数据。这种方法最大的优势是速度快,在HotSpot虚拟机中用的就是这种方式。

    Java内存分配机制

    这里所说的内存分配,主要指的是在堆上的分配,一般的,对象的内存分配都是在堆上进行,但现代技术也支持将对象拆成标量类型(标量类型即原子类型,表示单个值,可以是基本类型或String等),然后在栈上分配,在栈上分配的很少见,我们这里不考虑。

      Java内存分配和回收的机制概括的说,就是:分代分配,分代回收。对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。如下图(来源于《成为JavaGC专家part I》,http://www.importnew.com/1993.html):

        

      年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接 被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消 亡的),这个GC机制被称为Minor GC或叫Young GC。注意,Minor GC并不代表年轻代内存不足,它事实上只表示在Eden区上的GC。

      年轻代上的内存分配是这样的,年轻代可以分为3个区域:Eden区(伊甸园,亚当和夏娃偷吃禁果生娃娃的地方,用来表示内存首次分配的区域,再 贴切不过)和两个存活区(Survivor 0 、Survivor 1)。内存分配过程为(来源于《成为JavaGC专家part I》,http://www.importnew.com/1993.html):

        

    1. 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快
    2. 当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);
    3. 此后,每次Eden区满了,就执行一次Minor GC,并将剩余的对象都添加到Survivor0;
    4. 当Survivor0也满的时候,将其中仍然活着的对象直接复制到Survivor1,以后Eden区执行Minor GC后,就将剩余的对象添加Survivor1(此时,Survivor0是空白的)。
    5. 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。

      从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着当前还活 着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方 式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下高效,如果在老年代采用停止复制,则挺悲剧的

      在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread- Local Allocation Buffers),这两种技术的做法分别是:由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对 象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;而对于TLAB技术是对于多线程而言的,将Eden区分为若干 段,每个线程使用独立的一段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每个线程都使用Eden区的一段,并快速的分配内 存。

      年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次 Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时, 将执行Major GC,也叫 Full GC。  

       可以使用-XX:+UseAdaptiveSizePolicy开关来控制是否采用动态控制策略,如果动态控制,则动态调整Java堆中各个区域的大小以及进入老年代的年龄。

      如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。

      可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。

    Java GC机制

    GC机制的基本算法是:分代收集,这个不用赘述。下面阐述每个分代的收集方法。

      

      年轻代:

      事实上,在上一节,已经介绍了新生代的主要垃圾回收方法,在新生代中,使用“停止-复制”算法进行清理,将新生代内存分为2部分,1部分 Eden区较大,1部分Survivor比较小,并被划分为两个等量的部分。每次进行清理时,将Eden区和一个Survivor中仍然存活的对象拷贝到 另一个Survivor中,然后清理掉Eden和刚才的Survivor。

      这里也可以发现,停止复制算法中,用来复制的两部分并不总是相等的(传统的停止复制算法两部分内存相等,但新生代中使用1个大的Eden区和2个小的Survivor区来避免这个问题)

      由于绝大部分的对象都是短命的,甚至存活不到Survivor中,所以,Eden区与Survivor的比例较大,HotSpot默认是 8:1,即分别占新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下来的内存超过了10%,则需要将一部分对象分配到 老年代。用-XX:SurvivorRatio参数来配置Eden区域Survivor区的容量比值,默认是8,代表Eden:Survivor1:Survivor2=8:1:1.

      老年代:

      老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效。一般,老年代用的算法是标记-整理算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。
         在发生Minor GC时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次Full GC,否则,就查看是否设 置了-XX:+HandlePromotionFailure(允许担保失败),如果允许,则只会进行MinorGC,此时可以容忍内存分配失败;如果不 允许,则仍然进行Full GC(这代表着如果设置-XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)。

      方法区(永久代):

      永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:

    1. 类的所有实例都已经被回收
    2. 加载类的ClassLoader已经被回收
    3. 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)
         永久代的回收并不是必须的,可以通过参数来设置是否对类进行回收。HotSpot提供-Xnoclassgc进行控制
         使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以查看类加载和卸载信息
         -verbose、-XX:+TraceClassLoading可以在Product版HotSpot中使用;
         -XX:+TraceClassUnLoading需要fastdebug版HotSpot支持

    垃圾收集器

    在GC机制中,起重要作用的是垃圾收集器,垃圾收集器是GC的具体实现,Java虚拟机规范中对于垃圾收集器没有任何规定,所以不同厂商实现的垃圾 收集器各不相同,HotSpot 1.6版使用的垃圾收集器如下图(图来源于《深入理解Java虚拟机:JVM高级特效与最佳实现》,图中两个收集器之间有连线,说明它们可以配合使用):

      

      

    在介绍垃圾收集器之前,需要明确一点,就是在新生代采用的停止复制算法中,“停 止(Stop-the-world)”的意义是在回收内存时,需要暂停其他所 有线程的执行。这个是很低效的,现在的各种新生代收集器越来越优化这一点,但仍然只是将停止的时间变短,并未彻底取消停止。

    • Serial收集器:新生代收集器,使用停止复制算法,使用一个线程进行GC,其它工作线程暂停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式运行进行内存回收(这也是虚拟机在Client模式下运行的默认值)
    • ParNew收集器:新生代收集器,使用停止复制算法,Serial收集器的多线程版,用多个线程进行GC,其它工作线程暂停,关注缩短垃圾收集时间。使用-XX:+UseParNewGC开关来控制使用ParNew+Serial Old收集器组合收集内存;使用-XX:ParallelGCThreads来设置执行内存回收的线程数。
    • Parallel Scavenge 收集器:新生代收集器,使用停止复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间,比如:JVM运行100分钟,其中运行用户代码99分钟,垃 圾收集1分钟,则吞吐量是99%,这种收集器能最高效率的利用CPU,适合运行后台运算(关注缩短垃圾收集时间的收集器,如CMS,等待时间很少,所以适 合用户交互,提高用户体验)。使用-XX:+UseParallelGC开关控制使用 Parallel Scavenge+Serial Old收集器组合回收垃圾(这也是在Server模式下的默认值);使用-XX:GCTimeRatio来设置用户执行时间占总时间的比例,默认99,即 1%的时间用来进行垃圾回收。使用-XX:MaxGCPauseMillis设置GC的最大停顿时间(这个参数只对Parallel Scavenge有效)
    • Serial Old收集器:老年代收集器,单线程收集器,使用标记整理(整理的方法是Sweep(清理)和Compact(压缩),清理是将废弃的对象干掉,只留幸存 的对象,压缩是将移动对象,将空间填满保证内存分为2块,一块全是对象,一块空闲)算法,使用单线程进行GC,其它工作线程暂停(注意,在老年代中进行标 记整理算法清理,也需要暂停其它线程),在JDK1.5之前,Serial Old收集器与ParallelScavenge搭配使用。
    • Parallel Old收集器:老年代收集器,多线程,多线程机制与Parallel Scavenge差不错,使用标记整理(与Serial Old不同,这里的整理是Summary(汇总)和Compact(压缩),汇总的意思就是将幸存的对象复制到预先准备好的区域,而不是像Sweep(清 理)那样清理废弃的对象)算法,在Parallel Old执行时,仍然需要暂停其它线程。Parallel Old在多核计算中很有用。Parallel Old出现后(JDK 1.6),与Parallel Scavenge配合有很好的效果,充分体现Parallel Scavenge收集器吞吐量优先的效果。使用-XX:+UseParallelOldGC开关控制使用Parallel Scavenge +Parallel Old组合收集器进行收集。
    • CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于获取最短回收停顿时间,使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。使用-XX:+UseConcMarkSweepGC进行ParNew+CMS+Serial Old进行内存回收,优先使用ParNew+CMS(原因见后面),当用户线程内存不足时,采用备用方案Serial Old收集。
    CMS收集的方法是:先3次标记,再1次清除,3次标记中前两次是初始标记和重新标记(此时仍然需要停止(stop the world)), 初始标记(Initial Remark)是标记GC Roots能关联到的对象(即有引用的对象),停顿时间很短;并发标记(Concurrent remark)是执行GC Roots查找引用的过程,不需要用户线程停顿;重新标记(Remark)是在初始标记和并发标记期间,有标记变动的那部分仍需要标记,所以加上这一部分 标记的过程,停顿时间比并发标记小得多,但比初始标记稍长。在完成标记之后,就开始并发清除,不需要用户线程停顿。
    所以在CMS清理过程中,只有初始标记和重新标记需要短暂停顿,并发标记和并发清除都不需要暂停用户线程,因此效率很高,很适合高交互的场合。
    CMS也有缺点,它需要消耗额外的CPU和内存资源,在CPU和内存资源紧张,CPU较少时,会加重系统负担(CMS默认启动线程数为(CPU数量+3)/4)。
    另外,在并发收集过程中,用户线程仍然在运行,仍然产生内存垃圾,所以可能产生“浮动垃圾”,本次无法清理,只能下一次Full GC才清理,因此在GC期间,需要预留足够的内存给用户线程使用。所以使用CMS的收集器并不是老年代满了才触发Full GC,而是在使用了一大半(默认68%,即2/3,使用-XX:CMSInitiatingOccupancyFraction来设置)的时候就要进行Full GC,如果用户线程消耗内存不是特别大,可以适当调高-XX:CMSInitiatingOccupancyFraction以降低GC次数,提高性能,如果预留的用户线程内存不够,则会触发Concurrent Mode Failure,此时,将触发备用方案:使用Serial Old 收集器进行收集,但这样停顿时间就长了,因此-XX:CMSInitiatingOccupancyFraction不宜设的过大
    还有,CMS采用的是标记清除算法,会导致内存碎片的产生,可以使用-XX:+UseCMSCompactAtFullCollection来设置是否在Full GC之后进行碎片整理,用-XX:CMSFullGCsBeforeCompaction来设置在执行多少次不压缩的Full GC之后,来一次带压缩的Full GC
     
    • G1收集器:在JDK1.7中正式发布,与现状的新生代、老年代概念有很大不同,目前使用较少,不做介绍。
     
         注意并发(Concurrent)和并行(Parallel)的区别:
         并发是指用户线程与GC线程同时执行(不一定是并行,可能交替,但总体上是在同时执行的),不需要停顿用户线程(其实在CMS中用户线程还是需要停顿的,只是非常短,GC线程在另一个CPU上执行);
         并行收集是指多个GC线程并行工作,但此时用户线程是暂停的;
    所以,Serial和Parallel收集器都是并行的,而CMS收集器是并发的.
     
    关于JVM参数配置和内存调优实例,见我的下一篇博客(编写中:Java系列笔记(4) - JVM监控与调优),本来想写在同一篇博客里的,无奈内容太多,只好另起一篇。
     
    说明:
      本文是Java系列笔记的第3篇,这篇文章写了很久,主要是Java内存和 GC机制相对复杂,难以理解,加上本人这段时间项目和生活中耗费的时间很多,所以进度缓慢。文中大多数笔记内容来源于我在网络上查到的博客和《深入理解 Java虚拟机:JVM高级特效与最佳实现》一书。
      本人能力有限,如果有错漏,请留言指正。
    参考资料:
    《JAVA编程思想》,第5章;
    《Java深度历险》,Java垃圾回收机制与引用类型;
    《深入理解Java虚拟机:JVM高级特效与最佳实现》,第2-3章;
    成为JavaGC专家Part II — 如何监控Java垃圾回收机制, http://www.importnew.com/2057.html
    JDK5.0垃圾收集优化之--Don't Pause,http://calvin.iteye.com/blog/91905
    【原】java内存区域理解-初步了解,http://iamzhongyong.iteye.com/blog/1333100



    展开全文
  • 下面我们详细了解Java内存区域:先说明JVM规范定义的JVM运行时分配的数据区有哪些,然后分别介绍它们的特点,并指出给出一些HotSpot虚拟机实现的不同点和调整参数。

    Java内存管理:Java内存区域 JVM运行时数据区

           在前面的一些文章了解到javac编译的大体过程、Class文件结构、以及JVM字节码指令。

           下面我们详细了解Java内存区域:先说明JVM规范定义的JVM运行时分配的数据区有哪些,然后分别介绍它们的特点,并指出给出一些HotSpot虚拟机实现的不同点和调整参数。

    1、Java内存区域概述

    1-2、C/C++与Java程序开发的内存管理

           在内存管理领域,C/C++程序开发与Java程序开发有着完全不同的理念:

    1、C/C++程序开发

           自己管理内存是一项基础的工作;

           自已分配内存,但也得自己来及时回收;

           比较自由,但多了些工作量,且容易出现内存泄露和内存溢出等问题;

    2、Java程序开发

           JVM管理内存,不需要自己手动分配内存和释放内存;

           不容易出现内存泄露和内存溢出;

           一旦出现问题不容易排查,所以得了解JVM是怎么使用内存;

    1-2、Java内存区域与JVM运行时数据区

           如上图, Java虚拟机规范定义了字节码执行期间使用的各种运行时数据区,即JVM在执行Java程序的过程中,会把它管理的内存划分为若干个不同的数据区域,包括:

          程序计数器、java虚拟机栈、本地方法栈、java堆、方法区、运行时常量池;

           从线程共享角度来说,可以分为两类:

    1、所有线程共享的数据区

           方法区、运行时常量池、java堆;

           这些数据区域是在Java虚拟机启动时创建的,只有当Java虚拟机退出时才会被销毁;

    2、线程间隔离的数据区

           程序计数器、java虚拟机栈、本地方法栈、

           这些数据区域是每个线程的"私有"数据区,每个线程都有自己的,不与其他线程共享;

           每个线程的数据区在创建线程时创建,并在线程退出时被销毁;

    3、另外,还一种特殊的数据区

          直接内存--使用Native函数库直接分配的堆外内存;

           Java内存区域 = JVM运行时数据区 +直接内存。

    2、Java各内存区域说明

           上面图片展示的是JVM规范定义的运行时数据概念模型,实际上JVM的实现可能有所差别,下面在介绍各内存数据区时会给出一些HotSpot虚拟机实现的不同点和调整参数

    2-1、程序计数器

           程序计数器(Program Counter Register),简称PC计数器;

    1、生存特点

           每个线程都需要一个独立的PC计数器,生命周期与所属线程相同,各线程的计数器互不影响;

    2、作用

          JVM字节码解释器通过改变这个计数器的值来选取线程的下一条执行指令;

    3、存储内容

           JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的;

           在任意时刻,一个线程只会执行一个方法的代码(称为该线程的当前方法(Current Method));        

    (A)、如果这个方法是Java方法,那PC计数器就保存JVM正在执行的字节码指令的地址;

    (B)、如果该方法是native的,那PC计数器的值是空(undefined)

    4、内存分配特点

           PC计数器占用较小的内存空间;

           容量至少应当能保存一个returnAddress类型的数据或者一个与平台相关的本地指针的值;

    5、异常情况

           唯一一个JVM规范中没有规定会抛出OutOfMemoryError情况的区域;

    2-2、Java虚拟机栈

           Java虚拟机栈(Java Virtual Machine Stack,JVM Stack),指常说的栈内存(Stack);

           和Java堆指的堆内存(Heap),都是需要重点关注的内存区域

    1、生存特点

           每个线程都有一个私有的,生命周期与所属线程相同;

    2、作用

           描述的是Java方法执行的内存模型,与传统语言中(如C/C++)的栈类似;

           在方法调用和返回中也扮演了很重要的角色;

    3、存储内容

           用于保存方法的栈帧(Stack Frame)

           每个方法从调用到执行结束,对应其栈帧在JVM栈上的入栈到出栈的过程;

               栈帧:

           每个方法执行时都会创建一个栈帧,随着方法调用而创建(入栈),随着方法结束而销毁(出栈)

           栈帧是方法运行时的基础结构;

           栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息;

    (A)、局部变量表

           局部变量表(Local Variables Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量

           这些都是在编译期可知的数据,所以一个方法调用时,在JVM栈中分配给该方法的局部变量空间是完全确定的,运行中不改变;

           一个方法分配局部变量表的最大容量由Class文件中该方法的Code属性的max_locals数据项确定;

    (B)、操作数栈

           操作数栈(Operand Stack)简称操作栈,它是一个后进先出(Last-In-First-Out,LIFO)栈;

           在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容(任意类型的值),也就是入栈/出栈操作;

           在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果;

           一个方法的操作数栈长度由Class文件中该方法的Code属性的max_stacks数据项确定;

    (C)、动态链接

           每一个栈帧内部都包含一个指向运行时常量池的引用,来支持当前方法的执行过程中实现动态链接 (Dynamic Linking);

           在 Class 文件里面,描述一个方法调用了其他方法,或者访问其成员变量是通过符号引用(Symbolic Reference)来表示的;

          动态链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用(除了在类加载阶段解析的一部分符号);

    4、内存分配特点

           因为除了栈帧的出栈和入栈之外,JVM栈从来不被直接操作,所以栈帧可以在堆中分配;

           JVM栈所使用的内存不需要保证是连续的;

           JVM规范允许JVM栈被实现成固定大小的或者是根据计算动态扩展和收缩的:

    (A)、固定大小

           如果JVM栈是固定大小的,则当创建新线程的栈时,可以独立地选择每个JVM栈的大小;

    (B)、动态扩展或收缩

           在动态扩展或收缩JVM栈的情况下,JVM实现应该提供调节JVM栈最大和最小内存空间的手段;

    两种情况下,JVM实现都应当提供调节JVM栈初始内存空间大小的手段;

          HotSpot VM通过"-Xss"参数设置JVM栈内存空间大小;

    5、异常情况

           JVM规范中对该区域,规定了两种可能的异常状况:

    (A)、StackOverflowError

           如果线程请求分配的栈深度超过JVM栈允许的最大深度时,JVM将会抛出一个StackOverflowError异常;

    (B)、 OutOfMemoryError

           如果JVM栈可以动态扩展,当然扩展的动作目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那JVM将会抛出一个OutOfMemoryError异常;

    该区域与方法执行的JVM字节码指令密切相关,这里篇幅有限,以后有时间会分析方法的调用与执行过程,再来详细介绍该区域。

    2-3、本地方法栈

           本地方法栈(Native Method Stack)与 Java虚拟机栈类似

     1、与Java虚拟机栈的区别

           Java虚拟机栈为JVM执行Java方法(也就是字节码)服务;

           本地方法栈则为Native方法(指使用Java以外的其他语言编写的方法)服务

    2、HotSpot VM实现方式

           JVM规范中没有规定本地方法栈中方法使用的语言、方式和数据结构,JVM可以自由实现;

          HotSpot VM直接把本地方法栈和Java虚拟机栈合并为一个;

    2-4、Java堆

           Java堆(Java Heap)指常说的堆内存(Heap);

    1、生存特点

           所有线程共享;

           生命周期与JVM相同;

    2、作用

          为"new"创建的实例对象提供存储空间

           里面存储的这些对象实例都是通过垃圾收集器(Garbage Collector)进行自动管理,所以Java堆也称"GC堆"(Garbage Collected Heap);

    对GC堆以及GC的参数设置调整,就是JVM调优的主要内容

    3、存储内容

          用于存放几乎所有对象实例;

           (随JIT编译技术和逃逸分析技术发展,少量对象实例可能在栈上分配,详见后面介绍JIT编译的文章)

    4、内存分配特点

    (A)、Java堆划分

           为更好回收内存,或更快分配内存,需要对Java堆进行划分:

    (I)、从垃圾收集器的角度来看

           JVM规范没有规定JVM如何实现垃圾收集器;

           由于很多JVM采用分代收集算法,所以Java堆还可以细分为:新生代、老年代和永久代

    (II)、从内存分配角度来看

           为解决分配内存线程不安全问题,需要同步处理;

           Java堆可能划分出每个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),减少线程同步;

    HotSpot VM通过"-XX:+/-UseTLAB"指定是否使用TLAB;

    (B)、分配调整

           和JVM栈一样,Java堆所使用的物理内存不需要保证是连续的,逻辑连续即可;

           JVM规范允许Java堆被实现成固定大小的或者是根据计算动态扩展和收缩的:

           两种情况下,JVM实现都应当提供调节JJava堆初始内存空间大小的手段;

           在动态扩展或收缩的情况下,还应该提供调节最大和最小内存空间的手段;

    (C)、HotSpot VM相关调整

           目前主流的JVM都把Java堆实现成动态扩展的,如HotSpot VM:

    (1)、初始空间大小

           通过"-Xms"或"-XX:InitialHeapSize"参数指定Java堆初始空间大小;

           默认为1/64的物理内存空间;

    (2)、最大空间大小

           通过"-Xmx"或"-XX:MaxHeapSize"参数指定ava堆内存分配池的最大空间大小;

           默认为1/4的物理内存空间;

           Parallel垃圾收集器默认的最大堆大小是当小于等于192MB物理内存时,为物理内存的一半,否则为物理内存的四分之一;

    (3)、各年代内存的占用空间与可用空间的比例

           通过"-XX:MinHeapFreeRatio"和"-XX:MaxHeapFreeRatio"参数设置堆中各年代内存的占用空间与可用空间的比例保持在特定范围内;

           默认:

           "-XX:MinHeapFreeRatio=40":即一个年代(新生代或老年代)内存空余小于40%时,JVM会从未分配的堆内存中分配给该年代,以保持该年代40%的空余内存,直到分配完"-Xmx"指定的堆内存最大限制;

           "-XX:MaxHeapFreeRatio=70":即一个年代(新生代或老年代)内存空余大于70%时,JVM会缩减该年代内存,以保持该年代70%的空余内存,直到缩减到"-Xms"指定的堆内存最小限制;

          这两个参数不适用于Parallel垃圾收集器(通过“-XX:YoungGenerationSizeIncrement”、“-XX:TenuredGenerationSizeIncrement ”能及“-XX:AdaptiveSizeDecrementScaleFactor”调节);

    (4)、年轻代与老年代的大小比例

           通过"-XX:NewRatio":控制年轻代与老年代的大小比例;

           默认设置"-XX:NewRatio=2"表新生代和老年代之间的比例为1:2;

           换句话说,eden和survivor空间组合的年轻代大小将是总堆大小的三分之一;

    (5)、年轻代空间大小

           通过"-Xmn"参数指定年轻代(nursery)的堆的初始和最大大小

           或通过"-XX:NewSize"和"-XX:MaxNewSize"限制年轻代的最小大小和最大大小

    (6)、定永久代空间大小

           通过"-XX:MaxPermSize(JDK7)"或"-XX:MaxMetaspaceSize(JDK8)"参数指定永久代的最大内存大小

           通过"-XX:PermSize(JDK7)"或"-XX:MetaspaceSize(JDK8)"参数指定永久代的内存阈值--超过将触发垃圾回收

           注:JDK8中永久代已被删除,类元数据存储空间在本地内存中分配

           详情请参考:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/considerations.html#sthref62

    (D)、调整策略

          关于这些参数的调整需要垃圾收集的一些知识(以后文章会介绍),先来简单了解:

          当使用某种并行垃圾收集器时,应该指定期望的具体行为而不是指定堆的大小;

          让垃圾收集器自动地、动态的调整堆的大小来满足期望的行为;

          调整的一般规则:

          除非你的应用程序无法接受长时间的暂停,否则你可以将堆调的尽可能大一些;

          除非你发现问题的原因在于老年代的垃圾收集或应用程序暂停次数过多,否则你应该将堆的较大部分分给年轻代;

    关于HotSpot虚拟机堆内存分代说明以及空间大小说明请参考:

    http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/generations.html#sthref16

    http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/sizing.html#sizing_generations

    5、异常情况

          如果实际所需的堆超过了垃圾收集器能提供的最大容量,那Java虚拟机将会抛出一个OutOfMemoryError异常;

    该部分的内存如何分配、垃圾如何收集,上面这些参数如何调整,将在以后的文章详细说明。

    2-5、方法区

          方法区(Method Area)是堆的逻辑组成部分,但有一个别名"Non-Heap"(非堆)用以区分;

    1、生存特点

          所有线程共享;

          生命周期与JVM相同;

    2、作用

          为类加载器加载Class文件并解析后的类结构信息提供存储空间

          以及提供JVM运行时常量存储的空间;

    3、存储内容

          用于存储JVM加载的每一个类的结构信息,主要包括:

    (A)、运行时常量池(Runtime Constant Pool)、字段和方法数据;

    (B)、构造函数、普通方法的字节码内容以及JIT编译后的代码;

    (C)、还包括一些在类、实例、接口初始化时用到的特殊方法;

    4、内存分配特点

    (A)、分配调整

          和Java堆一样,所使用的物理内存不需要保证是连续的;

          或以实现成固定大小的或者是根据计算动态扩展和收缩的;

    (B)、方法区的实现与垃圾回收

          JVM规范规定:

          虽然方法区是堆的逻辑组成部分,但不限定实现方法区的内存位置;

          甚至简单的虚拟机实现可以选择在这个区域不实现垃圾收集;

          因为垃圾收集主要针对常量池和类型卸载,效果不佳;

          但方法区实现垃圾回收是必要的,否则容易引起内存溢出问题;

    (C)、HotSpot VM相关调整

    (I)、在JDK7中

          使用永久代(Permanent Generation)实现方法区,这样就可以不用专门实现方法区的内存管理,但这容易引起内存溢出问题;

          有规划放弃永久代而改用Native Memory来实现方法区;

          不再在Java堆的永久代中生成中分配字符串常量池,而是在Java堆其他的主要部分(年轻代和老年代)中分配;

          更多请参考:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/enhancements-7.html

    (II)、在JDK8中

          永久代已被删除,类元数据(Class Metadata)存储空间在本地内存中分配,并用显式管理元数据的空间:

          从OS请求空间,然后分成块;

          类加载器从它的块中分配元数据的空间(一个块被绑定到一个特定的类加载器);

          当为类加载器卸载类时,它的块被回收再使用或返回到操作系统;

          元数据使用由mmap分配的空间,而不是由malloc分配的空间;

          通过"-XX:MaxMetaspaceSize" (JDK8)参数指定类元数据区的最大内存大小

          通过"-XX:MetaspaceSize" (JDK8)参数指定类元数据区的内存阈值--超过将触发垃圾回收;

          详情请参考:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/considerations.html#sthref62

    5、异常情况

          如果方法区的内存空间不能满足内存分配请求,那Java虚拟机将抛出一个OutOfMemoryError异常;

    2-6、运行常量池

          运行常量池(Runtime Constant Pool)是方法区的一部分

    1、存储内容

          是每一个类或接口的常量池(Constant_Pool)的运行时表示形式;

          包括了若干种不同的常量:

          (A)、从编译期可知的字面量和符号引用,也即Class文件结构中的常量池

          (B)、必须运行期解析后才能获得的方法或字段的直接引用

          (C)、还包括运行时可能创建的新常量(如JDK1.6中的String类intern()方法)

    2-7、直接内存

          直接内存(Direct Memory)不是JVM运行时数据区,也不是JVM规范中定义的内存区域;

    1、特点

          是使用Native函数库直接分配的堆外内存;

          被频繁使用,且容易出现OutOfMemoryError异常

    2、作用

          因为避免了在Java堆中来回复制数据,能在一些场景中显著提高性能;

    3、实现方式

          JDK1.4中新加入NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式;

          它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java椎中的DirectByteBuffer对象作为这块内存的引用进行操作;

    4、HotSpot VM相关调整

          可以通过"-XX:MaxDirectMemorySize"参数指定直接内存最大空间

          不会受到Java堆大小的限制,即"-Xmx"参数限制的空间不包括直接内存

          容易导致各个内存区域总和大于物理内存限制,出现OutOfMemoryError异常;

     

          到这里,我们大体了解Java各内存区域是什么,有些什么特点了,但方法执行的JVM字节码指令如何在Java虚拟栈中运作的,以及Java堆内存如何分配、垃圾如何收集,如何进行JVM调优,将在以后的文章详细说明。

          后面我们将分别去了解:方法的调用与执行、JIT编译--在运行时把Class文件字节码编译成本地机器码的过程、以及JVM垃圾收集相关内容……

     

    【参考资料】

    1、《The Java Virtual Machine Specification》Java SE 8 Edition:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

    2、《Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide》:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html

    3、《Memory Management in the Java HotSpot™ Virtual Machine》:http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

    4、HotSpot虚拟机参数官方说明:http://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

    5、《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版 第2章

    6、Java Class文件结构解析 及 实例分析验证

    展开全文
  • Java内存模型详解

    千次阅读 多人点赞 2018-04-19 00:07:55
    前几天,发了一篇文章,介绍了一下JVM内存结构、Java内存模型以及Java对象模型之间的区别。有很多小伙伴反馈希望可以深入的讲解下每个知识点。Java内存模型,是这三个知识点当中最晦涩难懂的一个,而且涉及到很多...

    前几天,发了一篇文章,介绍了一下JVM内存结构、Java内存模型以及Java对象模型之间的区别。有很多小伙伴反馈希望可以深入的讲解下每个知识点。Java内存模型,是这三个知识点当中最晦涩难懂的一个,而且涉及到很多背景知识和相关知识。

    网上有很多关于Java内存模型的文章,在《深入理解Java虚拟机》和《Java并发编程的艺术》等书中也都有关于这个知识点的介绍。但是,很多人读完之后还是搞不清楚,甚至有的人说自己更懵了。本文,就来整体的介绍一下Java内存模型,目的很简单,让你读完本文以后,就知道到底Java内存模型是什么,为什么要有Java内存模型,Java内存模型解决了什么问题等。

    本文中,有很多定义和说法,都是笔者自己理解后定义出来的。希望能够让读者可以对Java内存模型有更加清晰的认识。当然,如有偏颇,欢迎指正。

    为什么要有内存模型

    在介绍Java内存模型之前,先来看一下到底什么是计算机内存模型,然后再来看Java内存模型在计算机内存模型的基础上做了哪些事情。要说计算机的内存模型,就要说一下一段古老的历史,看一下为什么要有内存模型。

    内存模型,英文名Memory Model,他是一个很老的老古董了。他是与计算机硬件有关的一个概念。那么我先给你介绍下他和硬件到底有啥关系。

    CPU和缓存一致性

    我们应该都知道,计算机在执行程序的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道。而计算机上面的数据,是存放在主存当中的,也就是计算机的物理内存啦。

    刚开始,还相安无事的,但是随着CPU技术的发展,CPU的执行速度越来越快。而由于内存的技术并没有太大的变化,所以从内存中读取和写入数据的过程和CPU的执行速度比起来差距就会越来越大,这就导致CPU每次操作内存都要耗费很多等待时间。

    这就像一家创业公司,刚开始,创始人和员工之间工作关系其乐融融,但是随着创始人的能力和野心越来越大,逐渐和员工之间出现了差距,普通员工原来越跟不上CEO的脚步。老板的每一个命令,传到到基层员工之后,由于基层员工的理解能力、执行能力的欠缺,就会耗费很多时间。这也就无形中拖慢了整家公司的工作效率。

    可是,不能因为内存的读写速度慢,就不发展CPU技术了吧,总不能让内存成为计算机处理的瓶颈吧。

    所以,人们想出来了一个好的办法,就是在CPU和内存之间增加高速缓存。缓存的概念大家都知道,就是保存一份数据拷贝。他的特点是速度快,内存小,并且昂贵。

    那么,程序的执行过程就变成了:

    当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

    之后,这家公司开始设立中层管理人员,管理人员直接归CEO领导,领导有什么指示,直接告诉管理人员,然后就可以去做自己的事情了。管理人员负责去协调底层员工的工作。因为管理人员是了解手下的人员以及自己负责的事情的。所以,大多数时候,公司的各种决策,通知等,CEO只要和管理人员之间沟通就够了。

    而随着CPU能力的不断提升,一层缓存就慢慢的无法满足要求了,就逐渐的衍生出多级缓存。

    按照数据读取顺序和与CPU结合的紧密程度,CPU缓存可以分为一级缓存(L1),二级缓存(L3),部分高端CPU还具有三级缓存(L3),每一级缓存中所储存的全部数据都是下一级缓存的一部分。

    这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。

    那么,在有了多级缓存之后,程序的执行就变成了:

    当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。

    随着公司越来越大,老板要管的事情越来越多,公司的管理部门开始改革,开始出现高层,中层,底层等管理者。一级一级之间逐层管理。

    单核CPU只含有一套L1,L2,L3缓存;如果CPU含有多个核心,即多核CPU,则每个核心都含有一套L1(甚至和L2)缓存,而共享L3(或者和L2)缓存。

    公司也分很多种,有些公司只有一个大Boss,他一个人说了算。但是有些公司有比如联席总经理、合伙人等机制。

    单核CPU就像一家公司只有一个老板,所有命令都来自于他,那么就只需要一套管理班底就够了。

    多核CPU就像一家公司是由多个合伙人共同创办的,那么,就需要给每个合伙人都设立一套供自己直接领导的高层管理人员,多个合伙人共享使用的是公司的底层员工。

    还有的公司,不断壮大,开始差分出各个子公司。各个子公司就是多个CPU了,互相之前没有共用的资源。互不影响。

    下图为一个单CPU双核的缓存结构。

    CACHE

    随着计算机能力不断提升,开始支持多线程。那么问题就来了。我们分别来分析下单线程、多线程在单核CPU、多核CPU中的影响。

    **单线程。**cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。

    单核CPU,多线程。进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。

    多核CPU,多线程。每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。

    在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。

    如果这家公司的命令都是串行下发的话,那么就没有任何问题。

    如果这家公司的命令都是并行下发的话,并且这些命令都是由同一个CEO下发的,这种机制是也没有什么问题。因为他的命令执行者只有一套管理体系。

    如果这家公司的命令都是并行下发的话,并且这些命令是由多个合伙人下发的,这就有问题了。因为每个合伙人只会把命令下达给自己直属的管理人员,而多个管理人员管理的底层员工可能是公用的。

    比如,合伙人1要辞退员工a,合伙人2要给员工a升职,升职后的话他再被辞退需要多个合伙人开会决议。两个合伙人分别把命令下发给了自己的管理人员。合伙人1命令下达后,管理人员a在辞退了员工后,他就知道这个员工被开除了。而合伙人2的管理人员2这时候在没得到消息之前,还认为员工a是在职的,他就欣然的接收了合伙人给他的升职a的命令。

    一致

    处理器优化和指令重排

    上面提到在在CPU和主存之间增加缓存,在多线程场景下会存在缓存一致性问题。除了这种情况,还有一种硬件问题也比较重要。那就是为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理。这就是处理器优化

    除了现在很多流行的处理器会对代码进行优化乱序处理,很多编程语言的编译器也会有类似的优化,比如Java虚拟机的即时编译器(JIT)也会做指令重排

    可想而知,如果任由处理器优化和编译器对指令重排的话,就可能导致各种各样的问题。

    关于员工组织调整的情况,如果允许人事部在接到多个命令后进行随意拆分乱序执行或者重排的话,那么对于这个员工以及这家公司的影响是非常大的。

    并发编程的问题

    前面说的和硬件有关的概念你可能听得有点蒙,还不知道他到底和软件有啥关系。但是关于并发编程的问题你应该有所了解,比如原子性问题,可见性问题和有序性问题。

    其实,原子性问题,可见性问题和有序性问题。是人们抽象定义出来的。而这个抽象的底层问题就是前面提到的缓存一致性问题、处理器优化问题和指令重排问题等。

    这里简单回顾下这三个问题,并不准备深入展开,感兴趣的读者可以自行学习。我们说,并发编程,为了保证数据的安全,需要满足以下三个特性:

    原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。

    可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

    有序性即程序执行的顺序按照代码的先后顺序执行。

    有没有发现,缓存一致性问题其实就是可见性问题。而处理器优化是可以导致原子性问题的。指令重排即会导致有序性问题。所以,后文将不再提起硬件层面的那些概念,而是直接使用大家熟悉的原子性、可见性和有序性。

    什么是内存模型

    前面提到的,缓存一致性问题、处理器器优化的指令重排问题是硬件的不断升级导致的。那么,有没有什么机制可以很好的解决上面的这些问题呢?

    最简单直接的做法就是废除处理器和处理器的优化技术、废除CPU缓存,让CPU直接和主存交互。但是,这么做虽然可以保证多线程下的并发问题。但是,这就有点因噎废食了。

    所以,为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是——内存模型。

    为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。

    内存模型解决并发问题主要采用两种方式:限制处理器优化使用内存屏障。本文就不深入底层原理来展开介绍了,感兴趣的朋友可以自行学习。

    什么是Java内存模型

    前面介绍过了计算机内存模型,这是解决多线程场景下并发问题的一个重要规范。那么具体的实现是如何的呢,不同的编程语言,在实现上可能有所不同。

    我们知道,Java程序是需要运行在Java虚拟机上面的,Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

    提到Java内存模型,一般指的是JDK 5 开始使用的新的内存模型,主要由JSR-133: JavaTM Memory Model and Thread Specification 描述。感兴趣的可以参看下这份PDF文档(http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf

    Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

    而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。

    JAVA

    这里面提到的主内存和工作内存,读者可以简单的类比成计算机内存模型中的主存和缓存的概念。特别需要注意的是,主内存和工作内存与JVM内存结构中的Java堆、栈、方法区等并不是同一个层次的内存划分,无法直接类比。《深入理解Java虚拟机》中认为,如果一定要勉强对应起来的话,从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分。工作内存则对应于虚拟机栈中的部分区域。

    所以,再来总结下,JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。

    Java内存模型的实现

    了解Java多线程的朋友都知道,在Java中提供了一系列和并发处理相关的关键字,比如volatilesynchronizedfinalconcurrent包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字。

    在开发多线程的代码的时候,我们可以直接使用synchronized等关键字来控制并发,从来就不需要关心底层的编译器优化、缓存一致性等问题。所以,Java内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。

    本文并不准备把所有的关键字逐一介绍其用法,因为关于各个关键字的用法,网上有很多资料。读者可以自行学习。本文还有一个重点要介绍的就是,我们前面提到,并发编程要解决原子性、有序性和一致性的问题,我们就再来看下,在Java中,分别使用什么方式来保证。

    原子性

    在Java中,为了保证原子性,提供了两个高级的字节码指令monitorentermonitorexit。在synchronized的实现原理文章中,介绍过,这两个字节码,在Java中对应的关键字就是synchronized

    因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。

    可见性

    Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。

    Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。

    除了volatile,Java中的synchronizedfinal两个关键字也可以实现可见性。只不过实现方式不同,这里不再展开了。

    有序性

    在Java中,可以使用synchronizedvolatile来保证多线程之间操作的有序性。实现方式有所区别:

    volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。

    好了,这里简单的介绍完了Java并发编程中解决原子性、可见性以及有序性可以使用的关键字。读者可能发现了,好像synchronized关键字是万能的,他可以同时满足以上三种特性,这其实也是很多人滥用synchronized的原因。

    但是synchronized是比较影响性能的,虽然编译器提供了很多锁优化技术,但是也不建议过度使用。

    总结

    在读完本文之后,相信你应该了解了什么是Java内存模型、Java内存模型的作用以及Java中内存模型做了什么事情等。关于Java中这些和内存模型有关的关键字,希望读者还可以继续深入学习,并且自己写几个例子亲自体会一下。

    可以参考《深入理解Java虚拟机》和《Java并发编程的艺术》两本书。

    展开全文
  • 目录 1.介绍 1.1内存溢出和内存泄漏 ...作为一个Java开发者,想必大家都听说过:内存溢出和内存泄漏。但真正了解的人,也许寥寥无几。亦或是,认为两者就是同一种概念的人,也是大有人在。之前有...
  • Java 内存管理

    千次阅读 2016-11-21 01:52:14
    弄清JVM(Java Virtual Machine)的内存管理模型对了解Java GC工作原理是很有必要的。本文介绍Java内存管理的处理方式,包括JVM内存分配各个区域的含义,以及如何监测协调GC工作。
  • 希望能够让读者可以对Java内存模型有更加清晰的认识。当然,如有偏颇,欢迎指正。   为什么要有内存模型   在介绍Java内存模型之前,先来看一下到底什么是计算机内存模型,然后再来看Java内存模型在计算机...
  • Java内存区域

    千次阅读 2019-11-07 13:48:35
    Java内存区域 1.运行时数据区域 Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。 程序计数器 虚拟机栈 本地方法栈 Java堆 方法区( 运行时常量池) 直接内存 站在线程的角度看...
  • Java内存区域与内存溢出

    千次阅读 2020-02-09 17:27:55
    内容参考《深入理解JVM虚拟机》,本文JVM均指HotSpot虚拟机。 Java与C语言针对“内存管理”...而在Java中,内存由JVM管理,垃圾回收器GC会帮助开发者自动回收不再被引用的对象来释放内存,使得Java不太会像C语言那...
  • Java与C++之间有- -堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。 2.1 概述 对于从事C、C++程序开发的开发人员来说,在内存管理领域,他们既是拥有最高权力的“皇帝”...
  • java虚拟机内存调整

    千次阅读 2012-07-17 14:10:25
    用tomcat做服务器,总是会出现内存不够这样的问题,默认的虚拟机内存是64M,显然是不够的。 在windows中,可以添加环境变量JAVA_OPTS 值-Xms512m -Xmx1024m来指定 也可以在tomcat的catalina.bat中加上set JAVA_...
  • Java虚拟机内存调整

    千次阅读 2010-04-22 19:24:00
    Java虚拟机默认分配64M内存,如果你的应用比较大,超出64M内存Java虚拟机就会抛出OutOfMemoryError,并停止运行。不管是什么应用(Web应用、Application等),只需要修改你的机器上的运行Java命令,在java xxx...
  • Java内存模型

    千次阅读 2017-04-22 22:46:59
    Java内存模型JMM(Java Memory Model)JMM主要是为了规定了线程和内存之间的一些关系。根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有实例变量都储存在主存中,对于所有线程都是共享的。每条线程都...
  • java虚拟机内存大小调整:

    千次阅读 2014-11-21 01:05:48
    java虚拟机内存大小调整:  一、设置JVM内存设置 1. 设置JVM内存的参数有四个: -Xmx Java Heap最大值,默认值为物理内存的1/4,最佳设值应该视物理内存大小及计算机内其他内存开销而定; -Xms Java...
  • 调整Java虚拟机JVM的最大内存使用大小
  • Java.lang.OutOfMemoryError: Java heap spaceEclipse内存调整(修改JDK使用内存) ** 今天项目启动时,出现异常Java.lang.OutOfMemoryError: Java heap space 内存溢出异常。因为我是启动两个tomcat ,所以 需要 修改...
  • java内存参数调整

    2018-12-28 20:10:32
    1.java内存参数  我们为什么需要进行jvm堆内存调整?  因为jvm垃圾回收机制,主要发生在堆内存区域,从堆的内存结构划分,里面存在伸缩区概念,那么频发的伸缩会影响jvm性能,所以我们需要去掉伸缩区。只需要-...
  • Java内存模型与Java线程的实现原理

    万次阅读 2016-07-15 23:52:18
    Java内存模型与Java线程的实现原理
  • java内存优化

    千次阅读 2016-06-23 15:35:37
    1、java内存如何优化 了解jvm内存管理看这里:jvm是如何管理内存的 了解堆内存看这里:java堆内存是什么样的 Java内存的优化主要是通过合理的控制GC来实现,主要原则: 1. 不能只看操作系统级别Java进程所占用的...
  • java内存区域与内存溢出

    千次阅读 2017-02-24 16:27:56
     Java虚拟机在执行java程序的过程中把他所管理的内存划分为若干个不同的数据区域。包括:程序计数器、java虚拟机栈、本地发放栈、java堆、方法区。 内存模型如图:  程序计数器 一块较小的内存空间...
  • 详解 Java 内存区域

    千次阅读 2021-01-18 14:39:18
    目录1. 概述2. 运行时数据区域2.1. 程序计数器2.2. Java 虚拟机栈2.3. 本地方法栈2.4 堆2.5 方法区直接内存方法区和永久代的关系为什么要将永久代...正是因为 Java 程序员把内存控制权利交给 Java 虚拟机,一旦出现内存
  • java内存分配

    千次阅读 2016-08-21 16:29:29
    1、JVM简介Java虚拟机(Java Virtual Machine 简称JVM)是运行所有Java程序的抽象计算机,是Java语言的运行环境,它是Java 最具吸引力的特性之一。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还...
  • java内存机制

    千次阅读 2019-11-22 22:23:42
    java内存分成:栈内存,堆内存,方法区(常量池,静态池)。 Java中常用的内存区域: 1、栈内存空间:保存所有的对象的名称(准确的说是保存了引用的堆内存空间的地址)。 2、堆内存空间:保存每个对象的具体的...
  • java 虚拟机内存大小的调整方法

    千次阅读 2014-10-11 18:44:44
    java虚拟机内存大小调整:  一、设置JVM内存设置 1. 设置JVM内存的参数有四个: -Xmx Java Heap最大值,默认值为物理内存的1/4,最佳设值应该视物理内存大小及计算机内其他内存开销而定; -Xms Java ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 194,127
精华内容 77,650
关键字:

怎么调整java的内存

java 订阅