同样,根据摩尔定律,我们知道单核CPU的主频不可能无限制的增长,要想很多的提升新能,需要多个处理器协同工作, Intel总裁的贝瑞特单膝下跪事件标志着多核时代的到来。
基于高速缓存的存储交互很好的解决了处理器与内存之间的矛盾,也引入了新的问题:缓存一致性问题。在多处理器系统中,每个处理器有自己的高速缓存,而他们又共享同一块内存(下文成主存,main memory 主要内存),当多个处理器运算都涉及到同一块内存区域的时候,就有可能发生缓存不一致的现象。为了解决这一问题,需要各个处理器运行时都遵循一些协议,在运行时需要将这些协议保证数据的一致性。这类协议包括MSI、MESI、MOSI、Synapse、Firely、DragonProtocol等。如下图所示
Java虚拟机内存模型中定义的访问操作与物理计算机处理的基本一致!
java中通过多线程机制使得多个任务同时执行处理,所有的线程共享JVM内存区域main memory,而每个线程又单独的有自己的工作内存,当线程与内存区域进行交互时,数据从主存拷贝到工作内存,进而交由线程处理(操作码+操作数),因此在jvm中,有的区域是以线程为单位,而有的区域则是整个JVM进程唯一的.如下图所示:
其中java栈,本地方法栈还有程序计数器为线程私有,而堆内存和方法区内存为线程共享的部分.
Java 堆:Java 堆是被所有线程共享的一块内存区域,在JVM启动时创建,随着JVM的结束而消亡。此内存区域所占的面积最大,他主要是用于存放对象实例,几乎所有的对象实例都在这里分配内存。因此他也是垃圾回收器主要关注的区域,当创建的对象实例特别多导致顿内存空间不够时,此区域会发上OOM异常。堆内空间还会被不同的垃圾收集器进行进一步的细分,最有名的就是新生代、老年代的划分。
方法区:这也是所有线程共享的一块内存区域,用于存储对象的类型数据,例如类结构信息,静态变量,以及常量池、字段、方法代码等。由于早期的Hotspot JVM实现,很多人习惯于将方法区称为永久代。注意:Oracle JDK 8中将永久代移除,同时增加了元数据区(Metaspace)。JVM对永久代垃圾回收(如,常量池回收、对象类型的卸载)非常不积极,所以当我们不断添加新类型的时候,永久代会出现OutOfMemoryError,尤其是在运行时存在大量动态类型生成的场合;类似Intern字符串缓存占用太多空间,也会导致OOM问题
(注:运行时常量池:是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。有关具体内容可参见https://www.cnblogs.com/dingyingsi/p/3760447.html)
以上介绍的是最常谈到的内存区域,那我们在看一下在真正的JVM运行中还会需要占用那些空间,为了有个更加直观、清晰的印象,我画了一个简单的内存结构图:
这张图反映了实际中JVM的内存占用,它还额外划分出了直接内存等区域。毕竟理论上的视角和现实中的视角是有区别的,规
范侧重的是通用的、无差别的部分,而对于应用开发者来说,只要是Java进程在运行时会占用,都会影响到我们的工程实践。
本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存的大小及处理器寻址空间的限制。因此在分配JVM空间的时候应该考虑直接内存所带来的影响,特别是应用到NIO的场景。
Code Cache:JVM本身是个本地程序,还需要其他的内存去完成各种基本任务,比如,JIT 编译器在运行时对热点方法进行编译,就会将编译后的方法储存在Code Cache里面;GC等功能。需要运行在本地线程之中,类似部分都需要占用内存空间。这些是实现JVM JIT等功能的需要,但规范中并不涉及。
2.java中JVM的内存优化
由于此文主要讲解的是JVM的内存区域划分,因此关于JVM内存回收的知识,不在重点说明,可点击链接跳转到以前的文章
JVM内存的调优主要的目的是减少Minor GC的频率和Full GC的次数,过多的Minor GC和Full GC是会占用很多的系统资源,影响系统的吞吐量。
1. 年轻代分三个区,一个Eden区,两个Survivor区(from和to区),可以通过-XXSurvivorRatio调整比例
2. 大部分对象在先在Eden区中申请内存。
3. 当Eden区满时,无法为新的对象分配内存时,会进行Minor GC对其回收无用对象占用的内存,如果还有存活对象,则将存活的对象复制到Survivor From区(两个中Survivor对称);然后从Eden区存活下来的对象,就会被复制到From,当这个From区满时,此区的存活对象将被复制到To区,接下来Eden区存活下来的对象就会被复制到To区,经历一定的次数Minor GC后,还存活的对象,将被复制“年老区(Tenured)”。
4. 年轻代和年老代的默认比例为1:2,即年轻代占堆内存的1/3,年老代占2/3,可调整-XX:NewRatio的大小设置年轻和年老的比例。
其余性能调优常用参数设置
总结:**上面的内容太多总结下面这些事必须要记住的**
-xms 堆内存的初始化大小
-xmx 堆内存的最大空间
-xmn 年轻代的空间
-xx:PermSize 方法区初始化大小
-xx:MaxPermSize 方法区最大空间
-XX:PretenureSizeThreshold 令大于这个设置值的对象直接在老年代分配
-MaxTenuringThreshold 年龄大于这个值的直接进入老年代
3.深入理解
1.Java对象是不是都创建在堆上的呢?
我注意到有一些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JVM设计者的选择。据我所知,Oracle Hotspot JVM中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上。
目前很多书籍还是基于JDK 7以前的版本,JDK已经发生了很大变化,Intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。但是,Intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上。
2.创建一个对象的实现细节
在Java 语言中,对象访问是如何进行的?对象访问在Java 语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会却涉及Java 栈、Java 堆、方法区这三个最重要内存区。
如下面的这句代码:
1 Object obj = new Object();
假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java 栈的本地变量表中,作为一个reference 类型数据出现。而“new Object()”这部分的语义将会反映到Java 堆中,形成一块存储了Object 类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不同,这块内存的长度是不固定的。另外,在Java 堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地
址信息,这些类型数据则存储在方法区中。
由于reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。
句柄访问方式:Java 堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据的具体地址信息,如下图所示。
直接指针访问方式:Java 堆对象信息中必须存放一个指针放置访问类型数据地址,栈中reference 中直接存储的就是对象地址,如下图所示
两种方式的区别:这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的数据指针,而reference 本身不需要被修改。使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。就文讨论的主要虚拟机Sun HotSpot 而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。
原文:https://www.cnblogs.com/sunweiye/p/10852540.html