JVM-运行时内存篇

程序计数器是一个比较小的内存区域其主要用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。 每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。

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

使用PC寄存器存储字节码指令地址有什么用呢?(为什么使用PC寄存器记录当前线程的执行地址呢?)
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。


PC寄存器为什么会被设定为线程私有
我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做
任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正
在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。


虚拟机栈 FIFO(first in last out)

如何设置栈内存的大小?-Xss size(即:-XX:Threadstacksize)
一般默认为512k-1024k,取决于操作系统。
栈的大小直接决定了函数调用的最大可达深度。

栈管运行,堆管存储

栈的单位:栈桢

栈桢的内部结构:局部变量表、操作数栈、动态链接、方法出口

方法和栈桢之间存在怎样的关系?
在这个线程上正在执行的每个方法都各自对应一个栈帧(StackFrame)
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

局部变量表(local variables table)
局部变量表也被称之为局部变量数组或本地变量表
1、定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型(8种)、对象引用(reference),以及returnAddress类型。
2、局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的
maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
3、方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少
4、局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁

操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间

1、我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
2、每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称之为表达式栈(Expression Stack)。
3、操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
4、每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack的值。
5、栈中的任何一个元素都是可以任意的Java数据类型。
        32bit的类型占用一个栈单位深度
        64bit的类型占用两个栈单位深度
6、操作数栈,在方法执行过程中,根据字节码指令,并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作,往栈中写入数据或提取数据来完成一次数据访问。
    某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。比如:执行复制、交换、求和等操作
7、如果被调用的方法带有返回值的话,其返回值将会被压入当前栈顿的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。       

动态链接指向运行时常量池的方法引用
1、每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令
2、在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

方法返回地址

1、存放调用该方法的pc寄存器的值。
2、一个方法的结束,有两种方式:
        正常执行完成;
        出现未处理的异常,非正常退出。
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的
pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址
。而通过异常退出的,返回
地址是要通过异常表来确定,栈帧中一般不会保存这部分信息


栈溢出的情况?

栈溢出:StackoverflowError
举个简单的例子:在main方法中调用main方法,就会不断压栈执行,直到栈溢出;
栈的大小可以是固定大小的,也可以是动态变化(动态扩展)的。
如果是固定的,可以通过-Xss设置栈的大小;
如果是动态变化的,当栈大小到达了整个内存空间不足了,就是抛出outofMemory异常
(java.lang.OutofMemoryError)

调整栈大小,就能保证出现溢出吗?                                                                                             
不能。因为调整栈大小,只会减少出现溢出的可能,栈大小不是可以无限扩大的,所有不能保证不出现溢出。

分配的栈内存越多越好吗?                                                                                                           
不是,因为增加栈大小,会造成每个线程的栈都变的很大,使得一定的栈空间下,能创建的线程数量会变小。

垃圾回收是否会涉及到虚拟机栈?
不会;垃圾回收只会涉及到方法区和堆中,方法区和堆也会存在溢出的可能;
程序计数器,只记录运行下一行的地址,不存在溢出和垃圾回收;
虚拟机栈和本地方法栈,都是只涉及压栈和出栈,可能存在栈溢出,不存在垃圾回收。

栈和堆的区别?
1、大小。栈大小为512k-1024k,堆的默认最大值为内存的1/4
2、执行效率。栈执行效率快,堆执行效率慢
3、存放内容不一样。栈存放基本数据类型和对象的引用,堆存放数组和对象实例
4、栈没有垃圾回收,堆是垃圾回收

可能抛出的异常?
StackOverFlowError 和 OutofMemoryError

Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的
1、如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候确定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出StackOverflowError异常。
2、如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将会抛出OutofMemoryError异常。

 


本地方法栈

简单地讲,一个Native Method就是一个Java调用非Java代码的接口。一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。这个特征并非Java所特有,很多其它的编程语言都有这机制,比如在C++中,你可以用extern“c"告知C++编译器去调用一个C的函数。


核心概述
1、一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域
2、Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
3、堆内存的大小是可以调节的。
4、《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
5、堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。
6、在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除

对象都分配在堆上?
《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated)数组和对象可能永远不会存储在栈上,因为栈桢中保存引用,这个引用指向对象或者数组在堆中的位置。
“几乎”所有的对象实例都在这里分配内存。例外:栈上分配(JVM性能调优)

堆的内部结构                                                                                                                                       

年轻代与老年代
1、存储在JVM中的Java对象可以被划分为两类
      一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
      另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。
2、Java堆区进一步细分的话,可以划分为年轻代(YoungGen)和老年代(oldGen)
3、其中年轻代又可以划分为Eden空间、Survivora空间和Survivor1空间(也叫做from区、to区)。

4、几乎所有的Java对象都是在Eden区被new出来的。
5、绝大部分的Java对象的销毁都在新生代进行了。

如何设置堆内存大小?
1、Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项“-Xms”和”-Xmx”来进行设置。
        -Xms用于表示堆区的起始内存,等价手-XX:InitialHeapsize
        -Xmx用于表示堆区的最大内存,等价于-XX:MaxHeapsize
2、一旦堆区中的内存大小超过“-Xmx”所指定的最大内存时,将会抛出OutOfMemoryError异常。
3、通常会将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。
        heap默认最大值计算方式:如果物理内存少于192M,那么heap最大值为物理内存的一半。如果物理内存大于等于1G,那么heap的最大值为物理内存的1/4
        heap默认最小值计算方式:最少不得少于8M,如果物理内存大于等于1G,heap默认最小值为物理内存的1/64,即1024/64=16M。最小堆内存在jvm启动的时候就会被初始化。
4、堆空间大小在实际开发中一般设置为2GB

如何设置新生代与老年代比例?

1、-XX:NewRatio配置新生代与老年代在堆结构的占比
        默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
        可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
2、可以使用-Xmn设置新生代最大内存大小这个参数一般使用默认值就可以了。

如何设置Eden、幸存者区比例?
在Hotspot中,Eden空间和另外两个Survivor空间默认所占的比例是8:1:1
可以通过选项-XX:SurvivorRatio调整这个空间比例。比如-XX:SurvivorRatio=8

对象内存分配策略?
为新对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。
针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to区
关于垃圾回收;
频繁在新生区收集
很少在养老区收集
几乎不在永久区/元空间收集

对象分配过程
1、new的对象先放伊甸园区。此区有大小限制。
2、当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(MinorGC/YGC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
3、然后将伊甸园中的剩余对象移动到s0区。
4、如果再次触发垃圾回收,Eden区和s0区的存活对象会放到s1区。
5、如果再次经历垃圾回收,Eden区和s1区的存活对象会放到s0区。如此反复。
6、啥时候能去养老区呢?可以设置次数。默认是15次
        可以设置参数:-XX:MaxTenuringThreshold=15 设置对象普升老年代的年龄阀值
7、在养老区,相对悠闲。当养老区内存不足时,再次触发GC:Major Gc,进行养老区的内存清理。
8、若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常
java.lang.OutOfMemoryError:Java heap space

内存分配策略(或对象提升(promotion)规则):
如果对象在Eden出生并经过第一次MinorGc后仍然存活,并且能被Survivor容纳的话,将被移动到survivor空间中,并将对象年龄设为1。对象在survivor区中每经过一次MinorGc,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代中。

内存分配原则
针对不同年龄段的对象分配原则如下所示:
1、优先分配到Eden
2、大对象直接分配到老年代。尽量避免程序中出现过多的大对象
3、长期存活的对象分配到老年代
4、动态对象年龄判断。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
5、空间分配担保 -XX:HandlePromotionFailure

空间分配担保策略 -XX:HandlePromotionFailure
在发生MinorGC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,
1、如果大于,则此次MinorGC是安全的。
2、如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败。
3、如果HandlepromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次MinorGc,但这次MinorGC依然是有风险的;如果小于,则改为进行一次FullGC。


解释MinorGc、MajorGc、FulIGC
JVM在进行GC时,并非每次都对上面三个内存(新生代、老年代、方法区)区域一起回收的,大部分时候回收的都是指新生代。
针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型
1、一种是部分收集(Partial GC)
2、一种是整堆(新生代、老年代、方法区)收集(Full GC)
部分收集:不是完整收集整个]ava堆的垃圾收集。其中又分为
 ⑴新生代收集(Minor GCYoung GC):只是新生代(Eden\s0,s1)的垃圾收集
 ⑵老年代收集(Major Gc/ Old Gc):只是老年代的垃圾收集。目前,只有CMS GC会有单独收集老年代的行为。注意,很多时候MajorsC会和Full GC混滑使用,需要具体分辨是老年代回收还是整堆回收。
 ⑶混合收集(MixedGc):收集整个新生代以及部分老年代的垃圾收集。目前,只有G1 GC会有这种行为
整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。

Minor GC触发机制
1、当年轻代空间不足时,就会触发Minor Gc。这里的年轻代满指的是Eden区满,Survivor满不会触发GC。(每次Minor Gc会清理年轻代的内存。)
2、因为Java对象大多都具备朝生夕的特性,所以Minor Gc非常频繁,一般回收速度也比较快。
3、Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。

Major GC触发机制
1、指发生在老年代的GC,对象从老年代消失时,我们说发生了Major GC或Full GC。
    出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。
    在老年代空间不足时,会先尝试触发Minor GC。如果之后空间还不足,则触发Major GC
2、Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。
3、如果Major GC后,内存还不足,就报OOM了。


Full GC触发机制:
触发Full GC执行的情况有如下五种:
(1)老年代空间不足
(2)方法区空间不足
(3)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(4)由Eden区、s0(From区) 区向s1(to区)复制时,对象大小大于To区可用内存,则把该对象转到老年代,且老年代的可用内存小于该对象大小
(5)调用system.gc()时,系统建议执行Full GC,但是不必然执行
说明:Full GC是开发或调优中尽量要避免的。这样暂时时间会短一些。


OOM如何解决
1、要解决OOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
2、如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置
3、如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗

为什么需要把Java堆分代?不分代就不能正常工作了吗?
其实不分代完全可以,分代的唯一理由就是优化GC性能。如果没有分代,那所有的对象都在一块。GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

为什么有TLAB(Thread Local Allocation Buffer)?什么是快速分配策略?
1、堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
2、由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
3、为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
所以,多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略


方法区

堆、栈、方法区的关系

在jdk1.7及之前,习惯上把方法区称为永久代。jdk1.8开始,使用元空间代替了永久代。
元空间使用本地内存。
1、方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。
2、方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是连续的
3、方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
4、方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutofMemoryError:PermGen space 或者
java.lang.OutOfMemoryError: Metaspace。(加载大量的第三方的jar包;Tomcat部署的工程过多(30-59个);大量动态的生成反射类)
5、关闭JVM就会释放这个区域的内存。
6、将jdk1.6方法区中静态变量和StringTable移动到jdk1.7的堆中

如何设置方法区的内存大小?
方法区的大小不必是固定的,JVM可以根据应用的需要动态调整。
jdk7及以前:
        通过-XX:PermSize来设置永久代初始分配空间。默认值是20.75M
        -XX:MaxPermSize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
        当JVM加载的类信息容量超过了这个值,会报异常OutofMemoryError:PermGenspace
jdk8及以后:
元数据区大小可以使用参数-XX:MetaspaceSize和-Xx:MaxMetaspaceSize指定,替代上述原有的两个参数。
默认值依赖于平台。windows下,-XX:MGtaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即没有限制。
与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace

为什么要用元空间代替永久代?
1、为永久代设置空间大小是很难确定的。在某些场景下,如果动态加载类过多,容易产生Perm的OOM。比如某个实际web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类经常出现java.lang.OuOfMemoryError: PermGen space
而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
2)对永久代进行调优是很困难的

JVM的永久代中会发生垃圾回收吗?
会发生,但是这个区域的垃圾回收效果很差,尤其是类型的卸载,条件相对苛刻。但是这部分区域的回收有时又是必要的。方法区的垃圾回收主要回收两部分内容:常量池中废弃的常量和不再使用的类型。

猜你喜欢

转载自blog.csdn.net/qq_39940205/article/details/121479578
今日推荐