博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
JVM 内存模型
阅读量:4288 次
发布时间:2019-05-27

本文共 4035 字,大约阅读时间需要 13 分钟。

目录


1 JVM 内存模型

                                   

程序计数器(PC,Program Counter Register):在JVM规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。

Java 虚拟机栈:之前也叫做Java 栈,每个线程在创建的时候都会创建一个虚拟机栈,对应着Java 方法调用。

堆(Heap):存放Java 对象实例。堆被所有的线程共享,在虚拟机启动时,通过 “ Xmx” 之类的参数来指定堆大小。

方法区:所有线程共享的一块内存区域,用于存储元数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。

运行时常量池:常量池可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要在运行时决定的符号的引用。

 

2 OOM

谈到Java 内存模型,不可避免的要设计到 OutOfMemory (OOM)问题,那么在Java 里面存在哪种OOM 可能性,分别对应哪个内存区域的异常状况呢 ?

OOM通俗点说,就是JVM 内存不够用了。

public class OutOfMemoryError  extends VirtualMachineError

javadoc 中对  的解释是:Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector 。当Java虚拟机无法分配对象时抛出,因为它内存不足,垃圾回收器无法再提供内存。

这里还隐藏着另外一个意思:在抛出OutOfMemoryError 之前,会触发垃圾回收收集器,尽可能去清理出空间。

OOM 常见原因,简单总结下:

堆内存不足是常见的OOM 原因。可能是堆内存泄漏;也可能堆大小不合适,比如要处理的数据量比较大,但是没有指定JVM 堆大小;或者JVM 处理引用不及时,导致堆积起来,内存无法释放等。

Java 栈也会导致OOM。比如我们写一个方法不停的递归调用,而不退出,就会导致不断的进行压栈,这时候会抛出 StackOverFlowError 错误,如果JVM 试图去扩展栈空间的时候失败,则会抛出 OutOfMemoryError。

 

3 堆内部的结构

从对象年代视角来看:

                       

  • 新生代:在Java应用中,绝大部分对象生命周期都很短暂,这些生命周期比较短暂的对象叫做新生代对象。其内部又分为 Eden 区域和两个Survivor 区域。Eden 区域 是对象初始分配的区域;两个Survivor 区域,被用来防止从Minor GC 中保留下来的对象。
  • 老年代:放置长生命周期的对象,通常都是从 Survivor 区域拷贝过来的对象。也有特殊情况,如果对象比较大,完全无法在新生代找到足够长的连续空闲的空间,JVM 就会直接分配到老年代。
  • 永久代:JDK 8 之后就不存在永久代了。之前存放元数据、常量池等。
  • virtual: 表示暂时不可用的空间内存。。因为在JVM 内部,肯定不会将堆的大小扩展到其上限。

JVM 参数:

-Xmx value 最大堆体积 默认情况下是堆内存的1/4
-Xms value 初始的最小堆体积 默认情况是堆内存的1/64

-XX:NewRatio=value

老年代和新生代的比例 默认数值是2,也就是老年代是新生代的2倍大,换句话说新生代是堆大小的1/3
-XX:NewSize=value 指定新生代内存大小数值 不用比例的方式调整新生代,直接指定具体的大小
-XX:SurvivorRatio=value 指定新生代内部区域Eden、

Survivor 区域的大小比例

默认是8,那么Survivor区域就是Eden 区域的1/8,也就是新生代一共10份,Eden占8/10,Survivor 占 1/10。因为有两个Survivor区域。

YoungGen=Eden + 2*Survivor

 

     
     

 

如何查看堆内和堆外的使用情况呢? 

图形化工具: JConsole、VisualVM 等,对于Linux 系统可以 remote 连接。

命令行工具:Jstat  、Jmap、 Jmx

 

4 Java 常见的垃圾收集器

  • Serial GC: 收集工作是单线程的,并且在进行垃圾收集过程中,会进入“stop-the-world” 状态。
  • ParNew GC:新生代GC,它实际上是Serial GC 的多线程版本,最常见的场景是配合老年代的 CMC GC 工作
  • CMS GC:基于标记-清除(Mark-Sweep)算法,设计目标是尽量减少停顿时间,这一点对于Web等反应敏感的应用非常重要。缺点是存在内存碎片化的问题,所以很难避免在长时间运行情况下发生 full GC,导致恶劣的停顿。另外,既然是并发,CMS 会占用更多的CPU 资源,并和用户线程争抢资源。在JDK 9中被标记为废弃deprecated。
  • Parallel GC:吞吐量优先的GC,其特点是新生代和老生代 GC 都是并行进行的,在常见的服务器环境中更加高效。Parallel GC 可以设置吞吐量等目标。
  • G1GC:这是一种兼顾吞吐量和停顿时间的GC实现,是 JDK9 以后默认的GC 选项。其内存结构并不是简单的条带式划分,而是类似棋盘的一个个 region,可以有效的避免内存碎片,尤其是当Java 堆非常大的时候,G1的优势更加明显。
serial GC

-XX:+UseSerialGC

 
parNew GC 

-XX:+UseConcMarkSweepGC  

-XX:+UseParNewGC

 
parallel GC

-XX:+UseParallelGC

-XX:MaxGCPauseMillis=value
-XX:GCTimeRatio=N // GC 时间和用户时间比例 = 1 / (N+1)
G1 GC 
-XX:+DisableExplicitGC -XX:+UseG1GC
 

说明:client vm mode(win 32)一直是Seiral GC,servermode下,9以后改为了G1,以前是Parrallel Gc

4.1 垃圾收集的算法有哪些? 如何判断一个对象是否可以回收?

GC 主要回收堆中的实例对象,通过可达性分析一个对象的引用是否存在,如果不存在,则对象会被回收。回收有一下几种算法:

复制算法(Copying):新生代GC 都用到复制算法,将活着的对象(可能在eden,也可能在from-survivor)复制到to-survivor区域。代价是,既然要进行复制,就需要提前预留空间,有一定的浪费。对于G1这种region 块 GC,复制而不是移动,那么需要维护各个region 之间对象引用的关系,内存和时间的开销都比较大。

标记-清除(Mark-Sweep)算法:首先进行标记工作,标识出所有要回收的对象,然后进行清除。缺点是 标记、清除的过程效率比较低,也不可避免的会出现内存碎片化问题,这就导致其不适合特别大的堆。否则,一旦full GC,暂时时间会比较长。

标记-整理(Mark-Compact)算法:类似于标记-清除,但为了避免内存碎片化,它会在清除过程中将对象移动,以确保移动后的对象占用连续的内存空间。

 

4.2 垃圾收集器工作的基本流程

新生代对象垃圾回收:

1、大部分对象创建都是在Eden的,除了个别大对象外。当其空间占用达到一定阈值时,会触发Minor GC

2、仍然被引用的对象存货下来,被复制到JVM 选择的survivor区域,这个区域这个时候叫做:from-survivor,并且对象年龄+1

3、第一次Minor GC 之后,Eden空闲下来了,再次触发 Minor GC的时候,另一个空闲的survivor区域 则会成为 to-survivor,Eden的存活对象都copy到to-survivor中,from-survivor的存活对象也复制to-survivor中。其中所有对象的年龄+1

4、from-survivor清空,再下次Minor GC的时候,成为新的to-survivor,带有对象的to-survivor变成新的from-survivor。重复回到步骤3

                               

                                     

以上步骤会发生多次,直到对象年龄计数达到一个阈值,超过阈值的对象会被晋升为老年代对象。这个阈值可以通过MaxTenuringThreshold 来指定。

-XX:MaxTenuringThreshold=<N>

                                

然后对于老年代对象的GC 算法 取决于 不同的GC,算法主要有 标记-整理、标记-清除。

以G1 垃圾收集来说,新生代对象采用的是Copying 复制算法,老年代对象采用的 标记-整理算法。

老年代中的无用对象被清除后,GC 会将对象进行整理,以防止内存碎片化。通常我们把老年代的GC叫做 Major GC,将对整个堆进行的清理叫做 Full GC,包括young gen、old gen等。只要老年代的连续空间大于 新生代所有对象的总大小或者历次晋升的平均大小 时,会发生Minor GC,否则会进行Full GC。

                               

Minor GC  新生代对象的回收 copying 算法
Major GC 老年代对象的回收 标记-清除、标记-整理
Full GC  对整个堆进行回收,包括新生代、老年代

1、老年代的连续空间 小于 历次晋升的平均代销;

2、老年代的连续空间 小于新生代所有对象的总大小

 

GC 调优的话,主要还是主要看业务。从性能方面,通常关注三个方面: 内存占用、延时(latency)、吞吐量。大多数情况下会侧重某一个或者两个方面。

 

 

 

 

转载地址:http://unlgi.baihongyu.com/

你可能感兴趣的文章
移动端防止被抓包
查看>>
Android异步批量压缩图片
查看>>
仿主流APP功能实现
查看>>
Java读取文件夹大小的6种方法及代码
查看>>
Java多线程中的10个面试要点
查看>>
Java面试经典,小题目大学问
查看>>
《程序员》:携程移动端 UI 界面性能优化实践
查看>>
Android指纹识别深入浅出分析到实战
查看>>
你们要的多数据库功能终于来了
查看>>
Android中实现微信本地视频发布到朋友圈功能
查看>>
非替代品,MongoDB与MySQL对比分析
查看>>
Hadoop平台相关技术
查看>>
java学习11天-自定义异常&异常转换(实例应用)
查看>>
MySql、SqlServer、Oracle数据库行转列大全
查看>>
程序员常用的自助建站资源汇总!
查看>>
分布式与集群的区别是什么?
查看>>
MySql常用必备脚本大全
查看>>
Velocity初探小结--velocity使用语法详解
查看>>
设计模式学习 - Singleton Pattern
查看>>
学习Spring——依赖注入
查看>>