蚌埠铁路建设监理公司网站windows优化大师功能

终于到了最经典的部分了,开搞开搞~

首先,按照惯例,我们先慢慢引出JVM内存布局,那怎么引出呢?当当当~

内存
我们(好吧,是我是我,我承认)常常忽略这个看似不起眼,实则非常非常重要的东西——内存。内存是硬盘和CPU之间的桥梁,承载着操作系统和应用程序的试试运行。
JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高校稳定运行。这里有个小东西需要注意一下,不同的JVM对于内存的规划方式和管理机制不同(小差异),我们现在说的JVM内存布局是指结合JVM虚拟机规范下,最经典的内存布局。
少废话,直接上图:

那么图也看了,滚吧~

咳咳,不好意思,接下来我们来详解一下JVM中的五大内存区域。

堆(线程共享)
堆是线程共享的,是OOM(out of memory)故障最主要的发源地(这里为什么说是最主要呢,因为OOM还可以发生在虚拟机栈、方法区等位置),它存储着几乎所有的实例对象。那这里为什么又说是几乎所有呢?难道还有不存储在堆中的对象吗?

是的,在Java中,典型的对象不在堆上分配的情况有两种:TLAB(Thread Local Allocation Buffer)和栈上分配(严格来说TLAB也是属于堆,只是在TLAB比较特殊),我这里简单说一下栈上分配。

栈上分配:JVM在Server模式下的逃逸分析(可能不稳定)可以分析出某个对象是否永远只在某个方法、线程的范围内,并没有“逃逸”出这个范围,就可以在栈上分配内存给对象存储。
堆所占的空间是所有内存区域中最大的,但是如果没有节制的创建对象,也会造成内存溢出的情况。所以堆的内存空间既可以是固定的大小,也可以动态的调整。
可以通过-Xms256M -Xmx1024M来设置最大容量和最小容量,但是在生产环境中,如果堆的空间大小会不断的收缩或者扩张的话,会造成不必要的系统压力,所以通常JVM的-Xms和-Xmx会设置成同一个值,避免在GC后调整堆的大小带来的压力。
接下来是比较重要的知识点了,高能预警…

堆分为两大块:新生代和老年代。对象产生之初在新生代,步入暮年是进入老年代,但是老年代也接纳在新生代无法容纳的超大对象。
新生代=Eden区+Survivor1区(s0)+Survivor2区(s1)
内存大小比例:Eden:s0:s1 = 8:1:1
接下来我们就通俗的说一下对象的创建和回收过程(重要):
绝大多数对象在Eden区出生,当Eden区满了,接收不下创建新对象时,会触发YGC(Young Garbage Collection)。垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象直接回收,依然存活的对象会被移送到Survivor区。
这个时候Survivor区因为有两个区域s0和s1,具体送到哪个区呢?如果两个区都是空的,那就移送到任意s0或者s1中,如果只有s0中存在对象,那么就会把Eden区刚刚转移过来的对象和s0中存活的对象一起移送到s1中,同理,如果只有s1中存在对象,那么就会把Eden区刚刚转移过来的对象和s1中存活的对象一起移送到s0中。也就是说,s0和s1同一时刻下,只有一个区域中存在对象。当Survivor区容量上限小于YGC后从Eden转移过来的对象时,就会把YGC淘汰过来的对象直接送到老年代。
那这样会出现一个问题?某些对象岂不是一直可以在s0和s1之间换来换去吗?

其实JVM也考虑到了这一点,每一个对象都有一个计数器,每次YGC都会+1,可以通过设置-XX:MaxTenuringThreshold参数能配置计数器的值到达某个阈(yu)值的时候,对象直接移交到老年代。如果该参数设置为1,那么会直接从Eden区移交到老年代,默认值为15。

这里贴一个对象分配与简要GC流程图:

这里的堆内存出现OOM的概率是所有内存耗尽异常中最高的,所以给JVM设置运行参数 -XX:+hHeapDumpOnOutOfMermoryError,让JVM在遇到OOM异常时能输出堆内信息,特别是很久之后(好几个月)才出现的OOM异常尤其重要。

元空间(线程共享)
元空间的前身perm区(永久代)在JDK8版本以后就被淘汰了,因为永久代在启动固定大小,很难调优,并且FGC时会移动类元信息。在某些场景下,如果动态加载类过多的话,就容易产生永久代的OOM。当然并不是这一点原因,还有诸如永久代在垃圾回收过程中还存在诸多问题这些原因,JDK8使用元空间替换永久代。
区别于永久代,元空间在本地内存中分配。在JDK8中,永久代中的字符串常量移至堆内存中,其他内容包括类元信息、字段、静态属性、方法、常量等,都移动到了元空间中。
怎么样?就这两段话,只需要记住元空间里面存的什么就可以了,芜湖~起飞!

JVM Stack虚拟机栈(线程私有)
栈呢,是一个先进后出的数据结构。相对于普通基于寄存器的运行环境来说,JVM是基于栈结构的运行环境。栈的结构移植性更好,可控性更强。
JVM中的虚拟机栈是描述Java方法执行的内存区域。栈中的元素用于支持虚拟机进行方法调用,方法调用的过程对应的就是栈帧从入栈到出栈的过程,也就是可以理解为一个Java方法对应虚拟机栈中的一个栈帧。
在活动线程中,只有栈顶的栈顶是有效的,称为当前帧,当前帧对应的Java方法也就是正在执行的方法称之为当前方法,栈帧是方法运行的基本结构。在执行引擎执行时,所有的指令都只能针对当前帧进行操作。

这里列举两种Java虚拟机栈可能出现的异常:
1.线程请求的栈深度大于虚拟机所允许的栈深度,将跑出StackOverflowError异常。
2.虚拟机栈空间可以动态扩展,当动态扩展无法申请到足够的空间时,将抛出OOM异常。
虚拟机栈通过压栈和出栈的方式,对每个方法对应的活动栈帧进行运算处理,方法正常结束的话,就跳到下一个栈帧上,那如果发生了异常呢?
答案是如果在执行过程中发生异常,会进行异常回溯,返回地址通过异常处理表决定,那这个异常处理表又是什么东西呢?

当当当当~来了!
- 每个方法都附带一个异常表
- 异常表中每一个条目, 就是一个异常处理器
这里顺便提一下JVM处理异常的流程:
- 编译而成的字节码中,每个方法都附带一个异常表。
- 异常表中每一个条目代表一个异常处理器
- 触发异常时,JVM会遍历异常表,比较触发异常的字节码的索引值是否在异常处理器的from指针到to指针的范围内。
- 范围匹配后,会去比较异常类型和异常处理器中的type是否相同。
- 类型匹配后,会跳转到target指针所指向的字节码(catch代码块的开始位置)
那如果在方法的异常表中没有匹配到异常处理器,会怎么样?
1、会弹出当前方法对应的Java栈帧
2、在调用者上重复异常匹配的流程。
3、最坏情况下,JVM需要编译当前线程Java栈上所有方法的异常表
好了,我们接下来继续说栈帧,栈帧在整个JVM体系中地位颇高,包括局部变量表、操作栈、动态连接、方法返回地址等。

局部变量表
局部变量表是存放方法参数和局部变量的区域,是一片连续的存储空间。
需要注意的是,局部变量表所需要的内存空间在编译器完成时分配完成,在进入一个方法时,这个方法对应的栈帧在栈中需要分配多大的局部变量空间完全是确定的,在方法运行期间不会改变局部变量表的大小。
操作栈
操作栈是一个初始状态为空的桶式结构栈,在方法执行过程中,会有各种指令往栈中写入和提取信息。
JVM的执行引擎是基于栈的执行引擎,这里说的栈就是指操作数栈。字节码指令集的定义都是基于操作栈类型的,操作栈与局部变量表的交互是极其频繁的。
动态连接
每个栈帧中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态连接。
方法返回地址
方法执行时有两种退出情况:
第一,正常退出,即正常执行到任何方法的返回字节码指令,如RETURN、IRETURN、ARETURN等;第二,异常退出,无论何种退出情况,都将会返回至当前方法被调用的位置。方法弹出的过程相当于弹出当前帧,退出会有三种方式:
- 返回值压入上层调用栈帧。
- 异常信息抛给能够处理的栈帧。
- PC计数器指向方法调用后的下一条指令
本地方法栈(线程私有)
在JVM内存布局中,Java虚拟机栈“主内”,本地方法栈“主外”。有点像两口子的感觉哦…当然这个主外是针对JVM来说的,也就是说在JVM这个家里,虚拟机栈主内,本地方法栈主外。

那么为什么这么说呢?因为本地方法栈为Native方法提供服务。线程开始调用本地方法(非Java代码编写的方法)时,会进入一个不再受JVM约束的世界。本地方法可以调用JNI(Java Native Interface)来访问虚拟机运行时的数据区,甚至可以调用寄存器,具有和JVM相同的能力和权限。
当大量的本地方法出现时,势必会削弱JVM对系统的控制力,因为它出错的地方都比较黑盒,对于内存不足的情况,本地方法栈还是会抛出native heap OutOfMemory。
这里重点说一下JNI类本地方法,最主要的还是System.currentTimeMillis(),JNI使Java深度使用操作系统的特性功能,复用非Java代码。
但是在项目过程中,如果大量使用其他语言来实现JNI,就会丧失夸平台特性,威胁到程序运行的稳定性。所以假如需要与本地代码交互,就可以用中间标准框架进行解耦,这样即使本地方法崩溃也不至于影响到JVM的稳定。当然如果是要求极高的执行效率的、偏底层这样的跨进程操作时,可以考虑设计为JNI调用方式。

程序计数器(线程私有)
程序计数器(Program Counter Register, PC),意译为程序计数寄存器,其中寄存器(Register)的命名来源于CPU的寄存器,寄存器存储指令相关的现场信息。每个线程在创建后都会有属于自己的程序计数器和栈帧,程序计数器用来存放指令的偏移量和行号指示器等,线程执行或者恢复都要依赖程序计数器。
程序计数器在各个线程之间互不影响,此区域也不会发生内存溢出的异常。
最后通过一张图我们来看一下Java的内存结构:

等等…ThreadLocal?

咳咳…下期再说咯?
快乐的时光总是短暂的~
哈哈哈!下期再见!

我是际遇,IT后浪中普通的一份子。