苍南做网站哪里找,德州哪里做网站,wordpress模板 汉化,查公司注册信息怎么查转载自 关于 JVM 内存的 N 个问题
JVM的内存区域是怎么划分的#xff1f;
JVM的内存划分中#xff0c;有部分区域是线程私有的#xff0c;有部分是属于整个JVM进程#xff1b;有些区域会抛出OOM异常#xff0c;有些则不会#xff0c;了解JVM的内存区域划分以及特征
JVM的内存划分中有部分区域是线程私有的有部分是属于整个JVM进程有些区域会抛出OOM异常有些则不会了解JVM的内存区域划分以及特征是定位线上内存问题的基础。那么JVM内存区域是怎么划分的呢
首先是程序计数器Program Counter Register)在JVM规范中每个线程都有自己的程序计数器。这是一块比较小的内存空间存储当前线程正在执行的Java方法的JVM指令地址即字节码的行号。如果正在执行Native方法则这个计数器为空。该内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM情况的内存区域。
第二Java虚拟机栈(Java Virtal Machine Stack)同样也是属于线程私有区域每个线程在创建的时候都会创建一个虚拟机栈生命周期与线程一致线程退出时线程的虚拟机栈也回收。虚拟机栈内部保持一个个的栈帧每次方法调用都会进行压栈JVM对栈帧的操作只有出栈和压栈两种方法调用结束时会进行出栈操作。
该区域存储着局部变量表编译时期可知的各种基本类型数据、对象引用、方法出口等信息。
第三本地方法栈Native Method Stack与虚拟机栈类似本地方法栈是在调用本地方法时使用的栈每个线程都有一个本地方法栈。
第四堆Heap,几乎所有创建的Java对象实例都是被直接分配到堆上的。堆被所有的线程所共享在堆上的区域会被垃圾回收器做进一步划分例如新生代、老年代的划分。Java虚拟机在启动的时候可以使用“Xmx”之类的参数指定堆区域的大小。
第五方法区Method Area)。方法区与堆一样也是所有的线程所共享存储被虚拟机加载的元Meta数据包括类信息、常量、静态变量、即时编译器编译后的代码等数据。这里需要注意的是运行时常量池也在方法区中。根据Java虚拟机规范的规定当方法区无法满足内存分配需求时将抛出OutOfMemoryError异常。由于早期HotSpot JVM的实现将CG分代收集拓展到了方法区因此很多人会将方法区称为永久代。Oracle JDK8中已永久代移除永久代同时增加了元数据区Metaspace。
第六运行时常量池Run-Time Constant Pool)这是方法区的一部分受到方法区内存的限制当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
在Class文件中除了有类的版本、方法、字段、接口等描述信息外还有一项信息是常量池。每个Class文件的头四个字节称为Magic Number它的作用是确定这是否是一个可以被虚拟机接受的文件接着的四个字节存储的是Class文件的版本号。紧挨着版本号之后的就是常量池入口了。常量池主要存放两大类常量 字面量Literal如文本字符串、final常量值 符号引用存放了与编译相关的一些常量因为Java不像C那样有连接的过程因此字段方法这些符号引用在运行期就需要进行转换以便得到真正的内存入口地址。
class文件中的常量池也称为静态常量池JVM虚拟机完成类装载操作后会把静态常量池加载到内存中存放在运行时常量池。
第七直接内存Direct Memory直接内存并不属于Java规范规定的属于Java虚拟机运行时数据区的一部分。Java的NIO可以使用Native方法直接在java堆外分配内存使用DirectByteBuffer对象作为这个堆外内存的引用。
下面这张图反映了运行中的Java进程内存占用情况 OOM可能发生在哪些区域上
根据javadoc的描述OOM是指JVM的内存不够用了同时垃圾收集器也无法提供更多的内存。从描述中可以看出在JVM抛出OutOfMemoryError之前垃圾收集器一般会出马先尝试回收内存。
从上面分析的Java数据区来看除了程序计数器不会发生OOM外哪些区域会发生OOM的情况呢
第一堆内存。堆内存不足是最常见的发送OOM的原因之一如果在堆中没有内存完成对象实例的分配并且堆无法再扩展时将抛出OutOfMemoryError异常。当前主流的JVM可以通过-Xmx和-Xms来控制堆内存的大小发生堆上OOM的可能是存在内存泄露也可能是堆大小分配不合理。
第二Java虚拟机栈和本地方法栈这两个区域的区别不过是虚拟机栈为虚拟机执行Java方法服务而本地方法栈则为虚拟机使用到的Native方法服务在内存分配异常上是相同的。在JVM规范中对Java虚拟机栈规定了两种异常1.如果线程请求的栈大于所分配的栈大小则抛出StackOverFlowError错误比如进行了一个不会停止的递归调用2. 如果虚拟机栈是可以动态拓展的拓展时无法申请到足够的内存则抛出OutOfMemoryError错误。
第三直接内存。直接内存虽然不是虚拟机运行时数据区的一部分但既然是内存就会受到物理内存的限制。在JDK1.4中引入的NIO使用Native函数库在堆外内存上直接分配内存但直接内存不足时也会导致OOM。
第四方法区。随着Metaspace元数据区的引入方法区的OOM错误信息也变成了“java.lang.OutOfMemoryError:Metaspace”。对于旧版本的Oracle JDK由于永久代的大小有限而JVM对永久代的垃圾回收并不积极如果往永久代不断写入数据例如String.Intern()的调用在永久代占用太多空间导致内存不足也会出现OOM的问题对应的错误信为“java.lang.OutOfMemoryError:PermGen space” 堆内存结构是怎么样的
可以借助一些工具来了解JVM的内存内容具体到特定的内存区域应该用什么工具去定位呢 图形化工具。图形化工具的优点是直观连接到Java进程后可以显示堆内存、堆外内存的使用情况类似的工具有JConsole,VisualVm等。 命令行工具。这类工具可以在运行时进行查询包括jstatjmap等可以对堆内存、方法区等进行查看。定位线上问题时也多会使用这些工具。jmap也可以生成堆转储文件Heap Dump文件如果是在linux上可以将堆转储文件拉到本地来使用Eclipse MAT进行分析也可以使用jhap进行分析。
关于内存的监控与诊断在后面会进行深入了解。现在来看下一个问题堆内的结构是怎么的呢
站在垃圾收集器的角度来看可以把内存分为新生代与老年代。内存的分配规则取决于当前使用的是哪种垃圾收集器的组合以及内存相关的参数配置。往大的方向说对象优先分配在新生代的Eden区域而大对象直接进入老年代。
第一, 新生代的Eden区域对象优先分配在该区域同时JVM可以为每个线程分配一个私有的缓存区域称为TLABThread Local Allocation Buffer避免多线程同时分配内存时需要使用加锁等机制而影响分配速度。TLAB在堆上分配位于Eden中。TLAB的结构如下
// ThreadLocalAllocBuffer: a descriptor for thread-local storage used by// the threads for allocation.// It is thread-private at any time, but maybe multiplexed over// time across multiple threads. The park()/unpark() pair is// used to make it avaiable for such multiplexing.class ThreadLocalAllocBuffer: public CHeapObjmtThread {friend class VMStructs;private:HeapWord* _start; // address of TLABHeapWord* _top; // address after last allocationHeapWord* _pf_top; // allocation prefetch watermarkHeapWord* _end; // allocation end (excluding alignment_reserve)size_t _desired_size; // desired size (including alignment_reserve)size_t _refill_waste_limit; // hold onto tlab if free() is larger than this
从本质上来说TLAB的管理是依靠三个指针start、end、top。start与end标记了Eden中被该TLAB管理的区域该区域不会被其他线程分配内存所使用top是分配指针开始时指向start的位置随着内存分配的进行慢慢向end靠近当撞上end时触发TLAB refill。因此内存中Eden的结构大体为 第二、新生代的Survivor区域。当Eden区域内存不足时会触发Minor GC也称为新生代GC在Minor GC存活下来的对象会被复制到Survivor区域中。我认为Survivor区的作用在于避免过早触发Full GC。如果没有SurvivorEden区每进行一次Minor GC都把对象直接送到老年代老年代很快便会内存不足引发Full GC。新生代中有两个Survivor区我认为两个Survivor的作用在于提高性能避免内存碎片的出现。在任何时候总有一个Survivor是empty的在发生Minor GC时会将Eden及另一个的Survivor的存活对象拷贝到该empty Survivor中从而避免内存碎片的产生。新生代的内存结构大体为 第三、老年代。老年代放置长生命周期的对象通常是从Survivor区域拷贝过来的对象不过当对象过大的时候无法在新生代中用连续内存的存放那么这个大对象就会被直接分配在老年代上。一般来说普通的对象都是分配在TLAB上较大的对象直接分配在Eden区上的其他内存区域而过大的对象直接分配在老年代上。 第四、永久代。如前面所说在早起的Hotspot JVM中有老年代的概念老年代用于存储Java类的元数据、常量池、Intern字符串等。在JDK8之后就将老年代移除而引入元数据区的概念。
第五、Vritual空间。前面说过可以使用Xms与Xmx来指定堆的最小与最大空间。如果Xms小于Xmx堆的大小不会直接扩展到上限而是留着一部分等待内存需求不断增长时再分配给新生代。Vritual空间便是这部分保留的内存区域。
那么综上所述可以画出Java堆内的内存结构大体为 通过一些参数可以来指定上述的堆内存区域的大小 -Xmx value 指定最大的堆大小 -Xms value 指定初始的最小堆大小 -XX:NewSize value 指定新生代的大小 -XXNewRatio value 老年代与新生代的大小比例。默认情况下这个比例是2也就是说老年代是新生代的2倍大。老年代过大的时候Full GC的时间会很长老年代过小则很容易触发Full GCFull GC频率过高这就是这个参数会造成的影响。 -XXSurvivorRation value . 设置Eden与Srivivor的大小比例如果该值为8代表一个Survivor是Eden的1/8是整个新生代的1/10。 常用的性能监控与问题定位工具有哪些
在系统的性能分析中CPU、内存与IO是主要的关注项。很多时候服务出现问题在这三者上会体现出现比如CPU飙升内存不足发生OOM等这时候需要使用对应的工具来对性能进行监控对问题进行定位。
对于CPU的监控首先可以使用top命令来进行查看下面是使用top查看负载的一个截图 load average 代表1分钟、5分钟、15分钟的系统平均负载从这三个数字可以判断系统负荷是大还是小。当CPU完全空闲的时候平均负荷为0当CPU工作量饱和的时候平均负荷为1。因此 load average 这三个数值越低代表系统负荷越小那么什么时候能看出系统负荷比较重呢这篇文章Understanding Linux CPU Load – when should you be worried里解释得非常通俗。如果电脑里只有一个CPU把CPU看成一条单行桥桥上只有一个车道所有的车都必须从这个桥上通过。那么
系统负荷为0代表桥上一辆车也没有 系统负荷0.5意味着桥上一半路段上有车 系统负荷1意味着桥上道路已经被车占满 系统负荷1.7代表着在桥上车子已经满了100%同时还有70%的车子在等待从桥上通过 从top命令的截图中可以看到这三个值机器的load average非常低。如果这三个值非常高比如超过了50%或60%就应当引起注意。从时间维度上来说如果发现CPU负荷慢慢升高也需要警惕。
其他的内存、CPU等性能监控工具的使用以一张脑图来展示 具体的使用方式可以参考从一次线上故障思考Java问题定位思路。