Java高级进阶 1 深入JVM之JVM内存模型分析

    深入理解JVM内存模型和掌握处理JVM内存问题已经是java程序员必不可少基础技能之一。

JVM内存模型  

 java内存区域主要分为线程私有区域(程序技术器、java虚拟机栈、本地方法栈),线程共享区域(方法区、实例堆(java堆))和直接内存。

    线程私有区域生命周期与线程相同,依赖用户线程的创建/销毁。

    线程共享区域随jvm的启动/关闭而创建。

    直接内存不属于JVM运行时数据区的一部分,但是也会频繁使用。NIO提供了基于Channel和Buffer的IO方式,可以使用Native函数库直接操作堆外内存,然后使用DirectByteBuffer对象作为这块内存的引用进行操作。

    程序计数器(线程私有)

   较小一块内存区域,当前线程所执行字节码的行号指示器,每个线程都有一个独立的计数器。正在执行java方法,计数器记录的是jvm当前执行指令的地址,如果是执行Native,则计数器为空。该区域是jvm唯一没有任何OutOfMemeryError情况的区域。

    虚拟机栈(线程私有)

    描述java方法执行的内存模型,生命周期与线程相同。每个方式在执行同时会创建一个栈帧,用来存储局部变量表、操作数栈、动态链接、方法出口等。每个方法从调用开始到执行完成都对应一个栈帧的入栈和出栈。

    栈帧是用来存储数据和部分结果的数据结构,同时也用来处理动态链接、方法返回值和异常分派等。栈帧随着方法调用而创建,方法结束而销毁。

    本地方法栈(线程私有)

    和虚拟机栈类似,区别是虚拟机栈是为java方法服务,而本地方法栈是为Native方法服务。

    堆(线程共享)

    虚拟机所管理的内存中最大的一块,唯一目的就是存放对象实例,几乎所有对象实例都是在堆分配内存。也是垃圾回收的主要区域。现代JVM采用分带收集法,因此java堆从Gc的角度可以分为(新生代(Egen区、From Survivior 和To Survivor)和老年代)。

    方法区(线程共享)

    是我们常说的永久代,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据。

    运行时常量池

    运行时常量池也属于方法区一部分,Class文件除了类版本、字段、方法、接口等信息外,还有一项就是运行时常量池,用于存放编译期生成的字面量和符号引用。

   内存中对象分配、布局和访问

    对象创建

    1 类加载检查

    检查是否在常量池中定位到类的符号引用,检查类是否已被加载、解析和初始化过。如果没有加载需要先执行类加载过程。

    2 对象内存分配

    类加载检查通过后,虚拟机将为对象分配内存,类加载完后便可以确认对象所需要内存大小。

   3 内存空间初始化为零值

   4 对象初始化

    对象内存布局

    对象头

对象头包含两部分信息,一是用于存储对象自身运行时数据(哈希码、GC分代年龄、锁标志、线程持有锁、偏向线程ID、偏向时间戳等);另一部分是对象类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确认对象是那个类的实例。

    实例数据

    存储对象有效信息,即代码中所定义的各种类型字段内容

    对齐填充

    对齐填充不是必要的,没特别含义,仅起到占位的作用。

   对象访问

    java程序通过虚拟机栈上的reference(对象引用)数据来操作堆上的对象实例,对象访问方法取决于虚拟机的实现,目前主流对象访问方法有两种:通过句柄访问对象和通过直接指针访问对象。

句柄访问对象:

 直接指针访问对象:

句柄方式访问的好处就是referencec存储的是稳定的句柄地址,对象被移动只会改变句柄中的实例数据指针,reference本身不改变。直接指针方式获取的好处就是速度更快,节省了一次指针定位的开销。

    JVM内存异常分析     

    常见内存分析工具

  • jvisualvm.exe

    JDK自带监控工具,可以对运行中的java应用程序进行全方面的监控分析,配合相应插件可以组成功能强大实用的性能分析工具

  • jconsole.exe

    JDK自带监控工具,功能和visualvm类似,支持插件较少

  • jca

    是类工具,启动命令java -jar jca433.jar,专业的线程分析工具,兼容sun/oracle JDK 线程堆dump

  • MAT

    Memory Analyzer Tool ,基于eclipse的内存溢出分析工具,专业的内存泄漏分析工具。使用方法

  • jprofile

    商业的java分析工具,功能强大,在内存泄漏分析和线程死锁分析方面非常专业

JVM内存参数配置

    可以通过参数-Xmx(堆最大容量,默认物理内存1/4)、-Xms(堆初始容量 ,默认物理内存1/64)来指定堆的大小,如:-Xms128m -Xmx1003m。默认空余堆内存小于40%,JVM会增大堆直到-Xmx最大限制,空余堆内存大于70%,虚拟机会缩小堆直到-Xms初始值限制,可以通过设置-Xmx和-Xms值相等避免堆自动调整大小。

    -Xmn 通常为-Xmx 的1/3或1/4

    -XX:NewRatio =2 新生代和老年代比例  ,-XX:NewRatio =2表示新生代占堆空间1/3,老年代占堆空间(2/3)

    -XX:NewSize 设置新生代最小空间大小

     -XX:MaxNewSize 设置新生代最大空间大小

    -XX:SurvivorRatio=8 新生代Eden区比例,默认为8

    -XX:PrintGCDetails 打印Gc信息

    -XX:+HeapDumpOnOutOfMemoryError 设置虚拟机出现内存溢出时Dump出当前内存堆转储快照

    -XX:PermSize 设置永久代最小空间

    -XX:MaxPermSize 设置永久代最大空间

    -Xss参数设置栈内存容量(每个线程的栈大小)

    -XX:+UseParallelGC 选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。

    -XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。

    -XX:MaxDirectMemorySize 设置直接内存大小,默认物理内存1/4

    JVM内存限制(最大)

    操作系统分配给每个进程的内存是有限的,譬如32位windows限制为2GB,即2GB - Xmx - MaxPermSize剩下的空间被虚拟机栈和本地方法栈瓜分(程序计数器器可以忽略)。

     堆内存分配

    根据Gc的角度可以分为(新生代(Eden区、From Survivior 和To Survivor)和老年代),其中java堆空间=新生代(1/3) + 老年代(2/3),新生代空间=Eden区(8/10) + From Survivor(1/10)+ To Survivor(1/10)。

   

    Java堆溢出

    当堆内存不足时,会抛出OutOfMemoryError,异常堆栈信息“java.lang.OutOfMemoryError:Java heap space”

    基本解决思路:1 通过内存映像分析工具(如Eclipse Memory Analyzer Tool)对堆转储快照Dump进行分析,2 定位内存OOM异常是内存泄漏还是内存溢出。3 如果是内存泄漏,可以进一步通过工具查看泄漏对象GC Roots的引用链,找到泄漏对象通过怎样的路径与GC Roots相关联导致垃圾收集器无法自动回收,根据对象类型和引用链定位内存泄漏的代码。4 如果不是内存泄漏,内存对象必须存在,那就需要考虑增加虚拟机堆参数(-Xmx和-Xms),从物理内存对比看是否可以调大,从程序代码上检查是否存在某些对象生命周期过长,尝试调整程序设计减少程序运行期的内存消耗。

    oom溢出堆栈信息

    

[Full GC (Allocation Failure) [PSYoungGen: 7912K->7905K(9216K)] [ParOldGen: 7967K->7967K(10240K)] 15880K->15872K(19456K), [Metaspace: 2731K->2731K(1056768K)], 0.1014711 secs] [Times: user=0.75 sys=0.01, real=0.10 secs] 
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid11364.hprof ...
Heap dump file created [27994219 bytes in 0.119 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Unknown Source)
	at java.util.Arrays.copyOf(Unknown Source)
	at java.util.ArrayList.grow(Unknown Source)

     虚拟机栈和本地方法栈溢出

    java栈溢出在虚拟机规范中定义了两类:StackOverflowError 线程请求的栈深度大于虚拟机允许的最大深度和OutOfMemoryError 扩展栈时无法申请打足够内存。

    StackOverflowError 线程请求的栈深度大于虚拟机允许的最大深度,该问题常见于递归场景中,当递归深度过深或局部变量过多时,可能出现该问题。通常使用默认的虚拟机栈参数配置,栈的深度足够满足正常的方法调用(包括递归)。

    OutOfMemoryError 通常是创建过多线程导致的内存溢出,主要优化手段包括:优化程序设计避免过多创建线程,在不能减少线程数或更换64位操作系统情况下,只能通过-Xss参数降低每个线程栈内存容量来换取更多的线程。

    方法区和运行时常量池溢出

    方法区用于存放Class的相关信息,如类名、访问修饰、常量池、字段描述等,该区域溢出主要出现在动态生成Class的场景,比如:大量JSP或动态JSP文件的应用、基于OSGi的应用、大量使用反射和动态代理、CGLib等Bytecode框架等。

    方法区溢出堆栈信息:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Unknown Source)
	at java.util.Arrays.copyOf(Unknown Source)
	at java.util.ArrayList.grow(Unknown Source)

    本机直接内存溢出

    本机直接内存不属于java运行时数据库,该区域内存分配经常被忽略。该区域溢出主要出现在使用NIO的场景。直接内存溢出不会在Heap Dump文件中看见明显异常,如果出现溢出且dump文件很小,可以考虑从直接内存溢出放心分析。

 

    

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值