[心得体会]jvm

1. jvm基本架构图

1583465392481

橙色: 线程共享的, gc主要的场所

灰色: 线程不共享

1583817450720

2. 类加载器

启动类加载器(c++加载器)

扩展类加载器(java应用程序加载器)

应用加载器(加载classpath当前目录下的类)

自定义类加载器

3. java沙箱安全机制

限制程序运行的环境, 将java的代码限定在沙箱中, 防止java系统的类被用户恶意篡改

例如用户自定义一个java.lang的包, 再添加一个叫String的类, 这个时候这个类是无法被加载执行的

4. 双亲委派机制

类的加载会被jvm无限制的推给接近根加载器的加载器进行加载, 然后才是根加载器以后的加载器进行加载, 如果无法加载则再往下走

5. 本地方法栈、本地方法接口和本地方法库

java需要调用本地方法(navtive)方法的时候需要去找本地方法接口, 而有些本地方法需要使用到window或者linux系统中的库, 所以还要使用上本地方法库

6. java栈

就是我们平时所说的栈,每个方法被执行时,都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。 每个方法从被调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中从出栈到入栈的过程。 「属于线程私有的内存区域

局部变量+实例对象的引用+方法的调用

(1) 栈帧

栈帧主要是存放函数调用时的函数的参数, 返回值等信息, 每个独立的栈帧都包括

  • 函数的返回地址和参数
  • 临时变量: 包括函数的非静态局部变量以及编译器自动生成的其他临时变量
  • 函数调用的上下文 栈是从高地址向低地址延伸,一个函数的栈帧用ebp 和 esp 这两个寄存器来划定范围.ebp 指向当前的栈帧的底部,esp 始终指向栈帧的顶部; ebp 寄存器又被称为帧指针(Frame Pointer); esp 寄存器又被称为栈指针(Stack Pointer); 1583816408983

在函数调用的过程中,有函数的调用者(caller)和被调用的函数(callee). 调用者需要知道被调用者函数返回值; 被调用者需要知道传入的参数和返回的地址;

(2) 函数调用

函数调用分为以下几步:

  • 参数入栈: 将参数按照调用约定(C 是从右向左)依次压入系统栈中;
  • 返回地址入栈: 将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行;
  • 代码跳转: 处理器将代码区跳转到被调用函数的入口处;
  • 栈帧调整: 1.将调用者的ebp压栈处理,保存指向栈底的ebp的地址(方便函数返回之后的现场恢复),此时esp指向新的栈顶位置; push ebp 2.将当前栈帧切换到新栈帧(将esp值装入ebp,更新栈帧底部), 这时ebp指向栈顶,而此时栈顶就是old ebp mov ebp, esp 3.给新栈帧分配空间 sub esp, XXX

(3) 函数返回

函数返回分为以下几步:

  • 保存被调用函数的返回值到 eax 寄存器中 mov eax, xxx
  • 恢复 esp 同时回收局部变量空间 mov ebp, esp
  • 将上一个栈帧底部位置恢复到 ebp pop ebp
  • 弹出当前栈顶元素,从栈中取到返回地址,并跳转到该位置 ret

到这里栈帧以及函数的调用与返回已经结束了,这里涉及一些汇编的知道,这里还没有记录不同平台的调用约定和一些特殊的寄存器.

7. 程序计数器

(1) 是什么?

程序计数器是一块较小的内存区域,可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。「属于线程私有的内存区域

可以看做是程序下次执行的汇编地址

8. Java堆

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此区域的唯一目的就是 存放对象实例。 Java堆是垃圾收集器管理的主要区域,很多时候被称为“GC堆(Garbage Collected Heap)”。 如果在堆中没有内存完成实例分配,并且无法继续扩展时,会抛出OutOfMemoryError异常。

类加载器读取了类文件的信息后, 需要把类 方法 常、变量放入到堆内存中, 保存所有应用类型的真实信息, 以方便执行器执行, 堆内存分为3个部分:

1583818043861

但是这是在jdk1.7之前的部分

1583818106504

比如现在new出来了100个对象, 会被存放在Eden区, 等到下次gc线程(这里的gc是普通的gc Minor GC)执行的时候, 这些new出来的对象将会有很大一部分被销毁, 幸存下来的5个对象(假设为5个对象幸存)将被存放在S0区(幸存0区, 正确的说法是被放入到了to区), 直到下次gc再次执行时, 如果5个对象没被销毁则会进入S1区, 此时有个叫法叫S0时from区, S1是TO区(TO区通常就是为空的区域), gc再次执行, from和to区再次交换, from变to, to变from交换完毕再次复制那5个对象, 往返重复15次(默认15次好像), 这个时候如果这5个对象还没被销毁, 则这5个对象全部进入养老区, 进入养老区之后使用的gc将被更换为Full GC去销毁, 当养老区空间不足而Full GC 又无法提供更加多的空间, 程序将会报错, OutOfMemoryError: java heap space异常, 此时可以调高jvm的 -Xms 基础空间大小和 -Xmx最大空间大小来完成调节

上面这个过程是理论上的情况, 当Eden幸存的对象过多的时候, 丢给S0或者S1(To区), To区根本不够放, 部分幸存者直接丢到了养老区

1583818885722

Eden的空间大小默认是8M, S0, S1大小默认是1, 比例大小 8:1:1

9. 永久区(永久代)

线程共享的存储jdk自身所携带的Class Interface的元数据, 它存储的是运行环境必须的类信息, 被加载进此区域的数据在将不会被垃圾回收器回收, 关闭jvm才会释放此区域所占用的内存

hotspot虚拟机的永久代一直是经常存在重大bug的区域, 很多虚拟机早已经去掉了这个区域所以在jdk1.8以后永久代被彻底放弃, 引入了新的区域叫Meta Space元空间

1583819912757

(1) 方法区

方法区和堆一样, 是线程共享区域, 它用于处理虚拟机加载的: 类信息+普通常量+静态常量+编译器编译后的代码等等, 虽然jvm规范将方法区描述为堆的一个逻辑部分, 但它却还有一个别名叫做Non-Heap(非堆), 目的就是要和堆分开

很多资料都说永久代就是方法区, 其实不然, 永久代其实是方法区的一个实现

10. 常量池

常量池是方法区的一个部分, class文件除了有类的版本 字段 方法 接口等描述信息外, 还有一项信息就是常量池, 这部分内容将在类加载后进入方法区的运行时常量池中存放

11. GC

(1) 什么是GC?

jvm的垃圾回收机制, 频繁回收young区, 较少回收old区, 基本不动perm区

(2)垃圾收集算法

1) 引用计数法

1583836640420

2) 复制算法

年轻代中使用的是Minor GC,这种GC算法采用的是复制算法(Copying)

原理
Minor GC会把Eden中的所有活的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Old generation中,也即一旦收集后,Eden是就变成空的了。 当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,通过-XX:MaxTenuringThreshold 来设定参数),这些对象就会成为老年代。
-XX:MaxTenuringThreshold — 设置对象在新生代中存活的次数

整个详细的解释过程:

年轻代中的GC,主要是复制算法(Copying)

HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1,一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(90%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片

1583837250324

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

1583837388782

因为Eden区对象一般存活率较低,一般的,使用两块10%的内存作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的from活动区间与另外80%中存活的eden对象转移到10%的to空闲区间,接下来,将之前90%的内存全部释放,以此类推

gc_copying

缺点:
复制算法它的缺点也是相当明显的。   1、它浪费了一半的内存,这太要命了。   2、如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。 所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。

3) 标记清除(Mark-Sweep)

老年代一般是由标记清除或者是标记清除与标记整理的混合实现

1583839934815

当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。 标记:从引用根节点开始标记所有被引用的对象。标记的过程其实就是遍历所有的GC Roots,然后将所有GC Roots可达的对象 标记为存活的对象。 清除:遍历整个堆,把未标记的对象清除。 缺点:此算法需要暂停整个应用,会产生内存碎片

用通俗的话解释一下标记/清除算法,就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将依旧存活的对象标记一遍,最终再将堆中所有没被标记的对象全部清除掉,接下来便让程序恢复运行。

mark_sweep

回收时,对需要存活的对象进行标记 回收不是绿色的对象

缺点:

1、首先,它的缺点就是效率比较低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲 2、其次,主要的缺点则是这种方式清理出来的空闲内存是不连续的,这点不难理解,我们的死亡对象都是随即的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。

1583840081205

4) 标记压缩(Mark-Compact)

老年代一般是由标记清除或者是标记清除与标记整理的混合实现

1583840162689

在整理压缩阶段,不再对标记的对像做回收,而是通过所有存活对像都向一端移动,然后直接清除边界以外的内存。 可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。

  标记/整理算法不仅可以弥补标记/清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价

缺点:

标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。

5) 标记清除压缩(Mark-Sweep-Compact)

1583840227404

mark_compact

6) 总结

内存效率:复制算法>标记清除算法>标记整理算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。 内存整齐度:复制算法=标记整理算法>标记清除算法。 内存利用率:标记整理算法=标记清除算法>复制算法。

可以看出,效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存,而为了尽量兼顾上面所提到的三个指标,标记/整理算法相对来说更平滑一些,但效率上依然不尽如人意,它比复制算法多了一个标记的阶段,又比标记/清除多了一个整理内存的过程

不存在最好的算法,只有最合适的算法。==========>分代收集算法。

年轻代(Young Gen)

年轻代特点是区域相对老年代较小,对像存活率低。

这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。

老年代(Tenure Gen)

老年代的特点是区域较大,对像存活率高。

这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。

Mark阶段的开销与存活对像的数量成正比,这点上说来,对于老年代,标记清除或者标记整理有一些不符,但可以通过多核/线程利用,对并发、并行的形式提标记效率。

Sweep阶段的开销与所管理区域的大小形正相关,但Sweep“就地处决”的特点,回收的过程没有对像的移动。使其相对其它有对像移动步骤的回收算法,仍然是效率最好的。但是需要解决内存碎片问题。

Compact阶段的开销与存活对像的数据成开比,如上一条所描述,对于大量对像的移动是很大开销的,做为老年代的第一选择并不合适。

基于上面的考虑,老年代一般是由标记清除或者是标记清除与标记整理的混合实现。以hotspot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对于对像的回收效率很高,而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器做为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。

JVM内存模型以及分区,需要详细到每个区放什么

堆里面的分区:Eden,survival from to,老年代,各自的特点。

GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方

Minor GC与Full GC分别在什么时候发生






原文地址:https://www.cnblogs.com/bangiao/p/12458142.html