JVM参数说明
参数 | 说明 | 默认值 |
---|---|---|
-Xss | 调整虚拟机栈内存 | 各系统默认值不同 |
-Xms,-Xmx | 堆初始大小和堆最大容量,绝大多数情况下把这两个数值设置成一样 | 物理内存的1/64,1/4 |
-XX:NewRatio | 老年代跟新生代的容量比例 | 2 |
-XX:SurvivorRatio | 新生代中Eden占据总内存(视为10份)的份量 | 8 |
-XX:+PrintGCDetails | 打印垃圾回收细节 | |
-XX:TLABWasteTargetPercent | 设置TLAB空间占Eden区大小百分比 | 1 |
-Xmn | 设置新生代的大小 | |
-XX:+PrintFlagsInitial | 查看所有的参数的默认初始值 | |
-XX:+PrintFlagsFinal | 查看所有的参数的最终值 | |
-XX:MaxTenuringThreshold | MinorGC中晋升老年区最大次数 | 15 |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yUAl0Jlr-1617204285360)(/Users/dinl/Library/Application Support/typora-user-images/image-20210311104734993.png)]
- PC寄存器: pc寄存器用来存储指向下一条指令(要执行的代码)的地址,由执行引擎来读取下一条代码.每一个线程一份
- 虚拟机栈:由多个栈帧构成,栈帧中包括: 本地变量表(Local Variables),操作数栈(Operand Stack),动态链接(Dynamic Links),返回地址(Return Address),每个线程一份
- 方法区: HotSpot独有的区域,存放常量,类信息,域信息,方法信息等,线程共用,生命周期与JVM进程相同
- 本地方法栈: 调用本地方法,每个线程一份
- 堆区: 存放Java对象,被多个线程共享,是RuntimeArea最大的区域,线程共用,生命周期与JVM进程相同
3.1 PC寄存器
-
它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都要依赖程序计数器完成,
-
字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
-
它是唯一一个在Java虚拟机规范中没有规定任何OutOfMemory的区域,也没有GC
常见面试题:
PC寄存器为什么要记录当前线程的执行地址?
因为PC寄存器是线程私有的,CPU在各个线程的切换中,线程内部的PC寄存器可以告诉CPU从哪继续执行;
为什么PC寄存器是线程私有的?
因为需要保证切换期间,每个线程可以完整执行,不被其他线程影响.
3.2 虚拟机栈
3.2.1 栈
- 来历?
为什么不继续使用PC寄存器,而是用栈帧来保存调用顺序呢?
因为不同硬件CPU的寄存器架构不同,考虑到JVM跨平台的设计理念,不能设计为基于寄存器的;
- 跟堆的分工
栈是运行时的单位,堆是存储的单位
栈被用来解决程序的运行问题,即程序执行顺序,怎样去处理数据?(5%内存空间)
堆被用来解决程序的存储问题,数据怎么存储,存储在那里?(95%的内存空间)
-
介绍
-
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈.
-
每个线程在创建的时候都会创建一个虚拟机栈,内部保存着一个个的栈帧(Stack Frame),对应着一次次的Java方法的调用;
-
生命周期与线程相同
-
-
作用
主管Java程序的运行,它保存方法的局部变量,部分结果,并参与方法的调用和返回.
-
栈的特点(优点)
-
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
-
JVM直接对Java栈的操作只有两个:
- 每个方法执行,伴随着进栈
- 执行结束后的出栈
-
栈不存在GC
-
存在OOM问题
-
常见面试题:
开发中遇到的异常有哪些?
OutOfMemoryError: JVM允许动态扩展,但是在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程的时没有足够的内存去创建新的虚拟机栈;
StackOverflowError:线程请求的栈容量大于JVM设定(-Xss)的最大容量的时候抛出此异常(递归代码常见错误);
3.2.2 栈帧(Stack Frame)
栈中的数据以栈帧的形式存在,一个线程正在执行的方法对应着栈中的一个栈帧,栈帧是一个内存块(数据集),保存着执行过程中的各种信息;
-
局部变量表(Local Variables)
-
局部变量表是一个数字数组,主要用来存放方法参数和定义在方法体内的局部变量;
-
局部变量表建立在线程内部栈的栈帧上,是线程私有数据,不存在线程数据安全问题;
-
局部变量表大小是编译初期确定下来的,保存为maximum local variables,运行期间不改变局部变量表大小;
-
最基本的存储单元是slot(变量槽)
局部变量表中的变量是垃圾回收的根节点,只要被局部变量表中直接或间接引用的对象都不会被回收.
-
-
操作数栈(Operand Stack) (表达式栈)
-
操作数栈是数组实现的,限制只能操作尾部元素,但是有数组的索引,按照顺序存放;
-
操作数栈就是根据操作指令,压栈(bipush,istore_1),取栈顶元素(iload_1)
-
虽然操作数栈具有数组索引,但是只能通过栈顶操作元素,无法通过索引访问操作数栈
-
跟局部变量表一样,操作数栈也是创建初期就确定长度.
-
栈顶缓存技术,指的是将栈顶元素缓存在物理CPU的寄存器中,降低读写次数
-
-
动态链接(Dynamic Linking) (指向运行时常量池的方法引用)
- 比如invokevirtual #7 调用常量池中第7个符号引用对应的方法.
-
方法返回地址(Return Address) (或方法正常或异常退出时的定义)
- 存放调用该方法的pc寄存器的值
-
一些附加信息
不一定有,比如程序调试相关信息.
常见面试题
举例常见栈溢出情况?
- 递归调用有问题, -Xss调整大小
调整栈大小就能保住不出现溢出吗?
- 不一定, 如果程序正常,可能调大了能用
- 如果程序本身有问题,后续还会出现问题.
分配的栈内存越大越好吗?
- 栈内存分配过大,允许创建的线程数量可能就会变少
栈内有垃圾回收机制吗?
- 没有
方法内定义的局部变量是否安全?
- 仅供方法内部的原始内存变量是安全的
- 如果返回给其他线程调用的引用变量,有可能不安全
3.3 本地方法接口(JNI)
定义: Native Method就是一个Java调用非Java代码的接口.设计初衷是融合C/C++程序.
为什么要使用Native Method?
-
与Java外环境交互: 使用C/C++;
-
与操作系统交互: JVM不是操作系统,在操作系统内的语言没法直接与操作系统交互.
-
Sun’s Java: Sun的解释器是C实现的,所以很难避免的去调用C
本地方法栈
用于管理Java对本地方法的调用.HotSpot将本地方法栈跟虚拟机栈合二为一了.
3.4 堆(heap)
一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域.
所有的对象实例以及数组都应该在运行时分配在堆上.
堆按照垃圾回收机制,又可以做如下区分,新生区Eden+Survivor,养老区Old/Tenure,永久区Perm(JDK8之后叫做元空间Meta).
堆空间大小设置:
“-Xms” 初始化的堆内存,等价于 -XX:InitialHeapSize
“-Xmx” 堆区的最大内存.等价于: -XX:MaxHeapSize
开发中一般将初始内存跟最大内存设计成相同值,可以避免扩容缩容过程的性能损耗.
OOM举例
存放数据大于最大空间容量.
新生代老年代相关参数设置
年轻代:Eden(占年轻代总空间的80%),Survivor0,Survivor1(有时候也叫from区跟to区), 老年代.
默认-Xx:NewRatio=2,表示老年代内存空间是新生代的两倍. 一般情况下不会修改该比例.除非已知程序中有大量生命周期长的对象.
内存分配策略
- 优先分配到Eden区
- 大对象直接分配到老年代(需要比较长连续空间的对象),避免出现过多大对象.
- 长期存活的对象分配到老年代
- 动态年龄对象判断(相同年龄的对象数总和大于Survivor区空间的一半,年龄大于或等于该年龄的对象提前进入老年代)
- 空间分配担保(老年代有空间时,把Survivor区存不下的数据放到老年区)
为每个线程分配Thread Local Allocation Buffer
为了避免多个线程操作同一个地址,JVM为每个线程独立分配私有缓冲区.(在Eden区划分).默认情况下,TLAB空间内存非常小,占整个Eden空间的1%,通过--XX:TLABWasteTargetPercent
设置TLAB空间所占用Eden空间的百分比大小.TLAB是线程分配对象内存的首选.一旦对TLAB空间分配内存失败,JVM便尝试通过使用加锁机制,确保数据操作的原子性,从而在Eden空间中分配内存.
堆是分配对象存储的唯一选择吗?
new对象的时候默认是堆,但是随着JIT发展与逃逸分析技术
逐渐成熟,栈上分配和标量替换技术导致了一些微妙的变化,如果一个对象并没有逃逸出方法的话,就有可能被优化成栈上分配技术
.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MY7lT4cE-1617204285361)(/Users/dinl/Pictures/mockup/JVM.png)]
3.5 方法区
一块独立于Java堆的内存空间
- 方法区与Java堆一样,是各个线程共享的内存区域
- 方法区在JVM启动的时候被创建,物理内存跟堆一样可以是不连续的
- 方法区的大小跟堆一样,既可以固定大小又可以动态扩展
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多类,导致方法区溢出,同样会抛出OutOfMemoryError:PermGen Space/MetaSpace.
在JDK7及以前,习惯把方法区称为永久代,JDK8开始,叫做元空间.
元空间跟永久代的本质区别: 元空间是本地内存(非虚拟机内存),永久代是虚拟机内存.
存放的数据:
- 类型信息: 类class,接口interface,枚举enum,注解annotation,类继承关系实现接口,类修饰符等
- 域信息:类中的属性
- 方法信息: 方法名,方法参数,方法修饰符,方法返回类型,异常表
运行时常量池vs常量池
常量池可以看成一张表,编译时准备好,虚拟机通过符号引用, 可以获取到要执行的类,方法等信息
运行时常量池,在加载类以后,对应的常量池表.具备动态性.此时不再是符号地址了,而是真实的内存地址.
3.6 垃圾回收
1) 7种垃圾回收算法
① 可达性分析算法(标记阶段)
原理: 可达性分析算法是以根对象集合(GCRoots)为起始点,按照自上至下的方式搜索被根对象集合所连接的目标对象是否可达。
可以作为GC Roots的包括: 虚拟机栈,本地方法栈,方法区,字符串常量池等对堆空间进行引用的地方。
② 标记清除算法(年轻代清除阶段)
原理: 当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除 标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收
缺点:
- 标记清除算法的效率不高
- 在进行GC的时候需要STW,用户体验差
- 这种方式整理出来的内存空间不连续,需要维护一张可用内存空间列表
③ 复制算法(年轻代清除阶段)
为了弥补标记清除算法的缺点,有了复制算法。
原理:将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点:解决了内存碎片的问题
缺点: 需要占用更多的内存空间
④ 标记整理算法(老年代清除阶段)
背景:复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
原理:第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象。第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。
优点:
消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。消除了复制算法当中,内存减半的高额代价。
缺点:从效率上来说,标记-整理算法要低于复制算法。移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址 移动过程中,需要全程暂停用户应用程序。即:STW。
⑤ 分代收集算法(主流JVM的做法,上述算法的混合)
**背景:**不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
年轻代:复制算法 老年代:由标记-清除或者是标记-清除与标记-整理的混合实现。
⑥ 增量收集算法
原理:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。
**缺点:**使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
⑦ 分区算法(G1 收集器)
原理:分区算法将整个堆空间划分成连续的不同小区间。每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。