【JAVA】JVM

1 JVM内存模型

在这里插入图片描述
概括地说来,JVM初始运行的时候都会分配好 Method Area(方法区) 和Heap(堆) ,而JVM 每遇到一个线程,就为其分配一个 Program Counter Register(程序计数器) , VM Stack(虚拟机栈)和Native Method Stack (本地方法栈), 当线程终止时,三者(虚拟机栈,本地方法栈和程序计数器)所占用的内存空间也会被释放掉。系统垃圾回收的场所只发生在线程共享的区域(实际上对大部分虚拟机来说知发生在Heap上)。

⑴程序计数器

唯一一个没有规定任何OutOfMemoryError区域。用来指示当前线程执行的字节码到了第几行(因为线程会因为没有获取CPU时间片而间断)

⑵虚拟机栈(就是我们常说的栈)

生命周期和线程相同,方法调用到执行完成就对应一个栈帧在虚拟机栈中入栈到出栈的过程。

⑶本地方法栈

与虚拟机栈相似,虚拟机栈为虚拟机执行java方法服务,本地方法栈为虚拟机使用的Native方法服务。

⑷堆

堆是垃圾收集管理的主要区域

⑸方法区

用于存储已被虚拟机加载的类信息,常量,静态变量,但JDK1.8它们移到了堆上
在这里插入图片描述

2 不同数据类型存放位置

⑴堆

主要用于存放对象和基本数据类型成员变量

⑵栈

基本数据类型的局部变量(int、short、long、byte、float、double、boolean、char等)以及对象的引用变量

⑶方法区

用于存储已被虚拟机加载的类信息,常量,静态变量,但JDK1.8它们移到了堆上

3 JVM垃圾回收

⑴可达性算法

采用可达性算法来确定对象需要被垃圾回收。
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(或者说从GC Roots到这个对象不可达),则证明此对象是不可用的。
注意:不可到达的对象也并非是非死不可的。对象死亡必须标记两次,如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过(也就是说对象的finalize()方法只能被调用一次),虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只需要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

⑵垃圾回收算法
①标记清除

第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。
优点:

  • 简单,不会增加编译器或赋值函数的负担
    缺点:
  • 耗时
  • 产生大量不连续内存碎片,导致新进大对象无法找到连续内存而触发新的垃圾回收
②标记复制

内存按容量划分为大小相等的两块每次只使用其中的一块。当这一块的内存使用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点:

  • 简单高效
  • 没有内存碎片
    缺点:
  • 内存缩小为原来一半
③标记整理

第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。但是标记整理算法一看效率就不高。

④分代收集

当前商业虚拟机使用的主流时算法:

  • 新生代:
    采用改进的标记复制算法。
    改进的标记复制算法:
    将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。新创建的对象在Eden区,当回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间,最后清理掉Eden和刚才用过的Survivor空间,清理完成后,刚刚被清理的Eden和另一块在回收时放入存活对象的Survivor空间作为使用内存,刚被清理的Survivor作为保留空间,以便后面用来回收之用。
    改进的收集算法的缺点:那块空的Survivor空间能否放得下Eden和使用的Survivor空间中还存活的对象,如果Survivor空间不够存放上一次新生代收集下来的存活对象,此时就需要向老年代“借”内存,那些剩余未放下的对象就通过分配担保机制进入老年代。
    老年代:
  • 对象存活率高,采用标记清除或标记整理算法。

4.垃圾收集器

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1
我们先看看什么叫吞吐量:吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

⑴Serial收集器

单线程,进行垃圾回收时,必须暂停其它所有工作线程。这个基本不用,所以也不多讲。

⑵Parallel(并行)收集器

可以使用多个线程扫描并压缩堆。进行垃圾回收时也会停止工作线程。吞吐量大,停顿时间较短。

  • Parallel New:用于年轻代,复制算法,多线程回收
  • Parallel Scavenge:用于年轻代,复制算法,多线程回收,关注吞吐量,它不能和CMS一起使用,和Parallel New相比,它可以设置最大gc停顿时间(-XX:MaxGCPauseMills)以及gc时间占比(-XX:GCTimeRatio)。
  • Parallel Old:用于年老代,标记-整理算法,多线程回收
⑶CMS收集器

标记-清除算法,用于年老代,并发收集,低停顿,但是会产生大量碎片,吞吐量稍低,适用于对系统响应时间要求较高的系统中,如页面请求/web服务器。

⑷G1收集器

出生于JDK1.7,对大于4G的堆有更好的支持
基础算法:标记-整理算法,因而不会产生内存碎片

⑸ZGC

ZGC是java11的一项新技术,它可以控制GC时间在10秒以内

5.JVM参数

  • -Xmx:最大堆大小
  • -Xms:初始堆大小
  • -Xmn:年轻代大小
  • -XXSurvivorRatio:年轻代中Eden区与Survivor区的大小比值
  • -Xss128k:设置每个线程的栈大小,默认JDK1.4中是256K,JDK1.5+中是1M,减小这个值能生成更多的线程。但是操作系统对一 个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误.

-Xmx10240m -Xms10240m -Xmn5120m -XXSurvivorRatio=3
年轻代5120m, Eden:Survivor=3,Survivor区大小=1024m(Survivor区有两个,即将年轻代分为5份,每个Survivor区占一份),总大小为2048m。

6.逃逸分析

逃逸分析并不是直接的优化手段,而是一个代码分析,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸。

  • 1、方法逃逸:当一个对象在方法中定义之后,作为参数传递到其它方法中;
  • 2、线程逃逸:如类变量或实例变量,可能被其它线程访问到;
    如果不存在逃逸行为,则可以对该对象进行如下优化:栈上分配、标量替换和同步消除。
-XX:+DoEscapeAnalysis开启逃逸分析(jdk1.8默认开启,其它版本未测试)
-XX:-DoEscapeAnalysis 关闭逃逸分析
⑴栈上分配

栈上分配就是把方法中的变量和对象分配到栈上,方法执行完后自动销毁,而不需要垃圾回收的介入,从而提高系统性能。

⑵标量替换

Java虚拟机中的原始数据类型(int,long等数值类型以及reference类型等)都不能再进一步分解,它们就可以称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量,Java中最典型的聚合量是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。拆散后的变量便可以被单独分析与优化,可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。标量替换基于分析逃逸基础之上,开启标量替换必须开启逃逸分析。

-XX:+EliminateAllocations开启标量替换(jdk1.8默认开启,其它版本未测试)
-XX:-EliminateAllocations 关闭标量替换
⑶同步消除

线程同步本身比较耗,如果确定一个对象不会逃逸出线程,无法被其它线程访问到,那该对象的读写就不会存在竞争,对这个变量的同步措施就可以消除掉。单线程中是没有锁竞争。锁消除基于分析逃逸基础之上,开启锁消除必须开启逃逸分析

-XX:+EliminateLocks开启锁消除(jdk1.8默认开启,其它版本未测试)
-XX:-EliminateLocks 关闭锁消除

7.堆外内存

堆外内存就是存在于JVM管控之外的一块内存区域,因此它是不受JVM的管控。
堆外内存了解即可,可以通过设置-XX:MaxDirectMemorySize=10M控制堆外内存的大小。
优点:

  • 减少了垃圾回收的工作
  • 加快了复制的速度。因为堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作。
    缺点:
  • 堆外内存难以控制,如果内存泄漏,那么很难排查
  • 堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合。

8.内存泄漏和内存溢出

1.内存泄漏

对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。比如A对象引用B对象,A对象的生命周期(t1-t4)比B对象的生命周期(t2-t3)长的多。当B对象没有被应用程序使用之后,A对象仍然在引用着B对象。这样,垃圾回收器就没办法将B对象从内存中移除。

2.内存溢出

是指程序在申请内存时,没有足够的内存空间供其使用。

⑴栈溢出

方法调用、方法内的局部变量都是在栈空间申请的,如果这一块空间不够用了就会产生StackOverflowError

⑵堆溢出

对象需要的内存大于了我们给虚拟机配置的内存,导致OutOfMemoryError。
不断的创建线程,导致OutOfMemoryError

猜你喜欢

转载自blog.csdn.net/cheidou123/article/details/95054669