声明:本文比较枯燥,适合对JVM有一定了解以及对JVM感兴趣的人阅读。
一、前言
实在不知道取什么名字好,取大了怕写不来,取小了怕没得写,于是随便叫了个名字。从去年开始,陆陆续续看了许多关于Java虚拟机方面的资料和书,目前感觉对JVM算是有一些了解,加上今天听了一天毕玄的讲课,顺便写篇博客,算是对自己这一年学的东西做一个总结吧。大致的可能会涉及到Java内存模型,垃圾回收,最后顺便简单提提OOM。
二、Java内存模型
Java内存大致分5个区域,PC寄存器,Java栈,堆,方法区和本地方法栈。
PC寄存器顾名思义就是指向下一条要执行的指令,相信大家都明白,不做多解释。
每一个线程对应一个Java栈,Java栈又由栈帧组成,一个方法调用对应一个栈帧,栈帧以栈的方式在Java栈中存放。栈帧里面又分局部数据区:存放方法里面的局部变量,以数组的形式存在;操作数栈,当局部变量作计算等时数值会在里面计算,计算结果在操作数栈顶存放,以栈的形式存在;还有帧数据区,保存一些数据来支持常量池解析,正常方法返回,异常等工作。在Java程序启动时可以加 -Xss 指定栈的大小,目前Sun JDK1.6 32位机默认是512k,64位机默认是1m。栈所占用的内存在哪里可以点这里查看讨论。
可以说Java里面所有的new操作都是在堆里面分配内存的。Sun JDK的堆又可分为新生代,旧生代和永久代。一般来说对象都会在新生代里面分配,当然如果要分配的对象大小超过新生代的大小等特殊情况下会直接在旧生代里分配,新生代分Eden,S0,S1三个区域,当在新生代里面的经过几次Minor GC后仍然存在或者Survivor空间(即S0,S1中的一个)不足垃圾收集器会将其移到旧生代。旧生代用来存放存活周期比较长的对象,当其空间不足时会触发Major GC(又称Full GC),GC后空间仍不足的话就会抛出OOM异常。参数 -Xms,-Xmx 分别设置堆启动内存大小和能占用的最大大小, -Xmn 设置新生代的大小,-XX:SurvivorRatio用来设置新生代里面Eden大小的Survivor大小的比例。
方法区又称永久代用来存放类的一些信息,比如类有哪些方法,类实现的接口、继承的类==,还有常量,静态变量等信息, -XX:PermSize,-XX:MaxPermSize 分别设置永久代的默认大小和最大值。
本地方法栈我很少用。
三、垃圾回收
垃圾回收主要是回收程序里面用不到,但在内存里面还存在的对象。
1、垃圾回收按种类分有串行收集、并行收集和并发收集。
串行收集就是暂停所有的应用,启用一个线程回收堆中的内存,在 client 模式(JDK在32机,64位机内存小于2G,内核小于2个时默认为 client 模式,也可通过编译参数 -client 或 -server 来指定)下默认为串行,参数 -XX:+UseSerialGC 可强制指定使用串行收集。这种收集方式在应用上没看出有什么优点,缺点很明显,浪费硬件资源。在Eden空间不足的时候会触发YGC(Young GC, Minor GC),会把Eden和From(S0或S1)区域中没有引用到的对象清除,将存活的对象复制到To区域,To区域放不了的,或者存活次数超过参数TenuringThreshold值的对象移到旧生代。当旧生代空间不足、永久代空间不足等情况下会触发FGC(Full GC, Major GC)。可通过 jstat 或者GC日志查看程序垃圾回收情况,串行收集是其他收集方式的理论基础,其他方式收集过程以及触发机制大致跟串行类似。
Server模式下并行收集默认为YGC:PS,FGC:Parallel MSC,也可以通过参数 -XX: +UseParallelGC 或 -XX:+UseOldParallelGC强制指定。JDK启动多线程并行的进行垃圾回收,因此优点在于回收比较高效,缺点表现在堆内存增大后,造成应用的暂停时间变长。
用参数-XX:+Use ConcMarkSweepGC可指定使用并发收集(CMS),并发收集启用多线程来收集垃圾,并且采用和并行收集不同的算法使得对旧生代回收时对应用造成的暂停时间特别短,适合对延迟要求比较高的应用场景,缺点也是很明显,会造成内存碎片、旧生代分配效率低、整个回收过程耗时过长、和应用程序争用CPU资源,还有一个致命的缺点,在CMS收集失败后,会使用串行MSC来回收。
至于什么情况下使用什么回收方式,一般的经验是 -XX:UseOldParallelGC 就够用了,可以持续监控运行状态,如果出现GC造成应用暂停时间过长再切换成CMS方式。
最后介绍一种未来可能会一统江湖的垃圾收集器:Garbage First(G1)。在JDK1.6 update14和JDK7里面都包含这种垃圾收集器,它的想法是让内存使用更简单,不用像现在一样有太多的参数去设置内存不同区域的大小,甚至它不是按代区别内存,而且在内存越来越大的情况下,改善目前垃圾回收时间过长的情况。按照构想,以后你要设置的内存参数只有四个:-Xms, -Xmm, -XX:MaxGCPauseMillis, -XX:GCPauseIntervalMillis,后两个参数的含义是在多少毫秒内GC引起的应用暂停占多少毫秒。构想是很伟大的,但目前G1的表现确是相当的惨不忍睹,不过相信G1能够成功。
前面提到的垃圾回收会引起应用的暂停,可能会有人对在系统运行的时候如何精准的暂停应用会感兴趣。Sun JDK用了一个比较巧妙的办法解决这个问题,它在编译的时候,引用切换的地方(比如A a = new B())生成一个safepoint,运行的时候safepoint会检查内存是否可读,如果不可读则抛出异常,应用挂起,JVM需要做的就是将内存设置为只读就可以了。
前面还有一个关键点没有提到,就是悲观策略问题(要注意悲观策略在官方文档上面是没有任何涉及的)。垃圾收集器会在每次新生代竞升到旧生代时记下本次有多大的对象竞升,然后记下这个平均值,在串行回收的YGC前,并行回收的YGC前和后都会检查旧生代剩余的大小是否大于这个平均值,如果答案是否的话会触发Full GC。
2、按算法分主要有标记-清除法、标记-整理法和复制法,还有理论上的一些算法:比如火车算法,计数法等。
标记-清除分两部分,先从根对象出发,遍历堆上的对象并标记,第二阶段清除堆里面未被标记的对象。这种方法简单高效,当然缺点也很明显,比如分配顺序为A--B--C--D,现在B和C不再存活被清除了,现在有请求分配E,但是D后面的内存不够E使用,A和D中间的内存也不够E使用,遇到这样的情况JVM则给E分配内存失败,实际上有可能A和D中间+D后面的内存是大于E的请求大小的,也就是会引起内存碎片的问题。
标记-整理很好的解决了上面的问题,在标记后把所有存活对象都向一端移动,然后直接清理掉端边界以外的内存,这样则将不存在内存碎片的问题,由于需要移动内存块,则增加了回收时所耗费的时间。
复制法将内存区域分为相同大小的两块:From,To(JVM新生代内存结构借鉴了这点)。从根对象开始在From块里面遍历并标记,然后将存活的块移动到To块并清除From块,也避免了内存碎片等问题,但缺点是比较浪费内存,有一半的内存经常处于闲置状态。
最后解释下上面提到的根对象,Sun JDK认为以下对象为根对象:当前运行线程的栈上引用的对象;常量及静态变量;本地方法handles;JVM handles。
四、OOM(OutOfMemoryError)
OOM主要有如下几种情况:
GC overhead limit exceeded
Java head Space
Unable to create new native thread
PermGen space
Out of swap space
如何解决OOM?可以用jmap等工具,Linux下输入 jmap -histo pid 可以查看运行的Java程序中占用内存大小的类型排名情况,运气好的话在排名前几个类型里面可以找到内存溢出的原因。比较标准的排查流程可以在程序启动参数中加上 -XX:+HeapDumpOnOutOfMemoryError 可以在出现OOM时把内存情况输出到日志文件,然后用 Eclipse mat 分析内存使用情况,根据内存使用情况猜测问题可能出现在哪里,然后编写btrace脚本运行找到这些问题地方的代码,可以参见这里,这个流程基本可以解决绝大部分OOM的问题,当然这个流程需要丰富的经验来准确猜测问题所在。
我们在写代码的时候注意以下几点也可以避开绝大多数产生OOM的可能(这里参考了毕玄的课件):
慎用ThreadLocal;
限制Collection/StringBuilder等的大小;
限制提交请求的大小,尤其是批量处理;
限制数据库返回数据的大小;
避免死循环。
五、总结
很多人会思考为什么要了解JVM,有些人认为了解JVM主要是为了性能调优,比如调整JDK启动参数分配不同的堆大小、使用不同的垃圾回收算法==。但是实际上或者说很多情况下参数的调整不会起到立竿见影的效果,一般都差别不大,有时候系统反而会变得更慢……我认为学习JVM是为了让我们更明白Java代码在执行的时候到底做了些什么,我们为什么要这样写,不要为了调优而调优,编写高质量的代码仍然是王道。
分享到:
相关推荐
深入 Java 虚拟机.pdf Java 虚拟机(Java Virtual Machine,JVM)是 Java 语言的 runtime 环境,是 Java 程序执行的核心组件。它提供了一个平台无关的环境,允许 Java 程序在不同的操作系统和硬件平台上运行。 一...
《Java核心技术系列:Java虚拟机规范(Java SE 8版)》由Oracle官方发布,Java虚拟机技术创建人撰写,国内资深Java技术专家翻译。书中基于全新Java SE 8,完整且准确地阐述Java虚拟机规范,是深度了解Java虚拟机和...
本书是继《深入理解Java虚拟机》之后的又一经典著作,它一方面遵循《Java虚拟机规范》,一方面又独辟蹊径,不仅能让Java虚拟机的学习变得更加简单和有趣,而且能让你对Java虚拟机的原理认识更深入和更深刻!...
学习Java虚拟机对于深入理解Java程序的执行机制至关重要。这里我们将深入探讨Java虚拟机的几个关键知识点。 1. 类加载机制:Java程序的执行始于类加载。JVM有三个主要的类加载器——bootstrap classloader、...
Java虚拟机规范 Java SE 8版-带目录-pdf,本书完整而准确地阐释了Java虚拟机各方面的细节,围绕Java虚拟机整体架构、编译器、class文件格式、加载、链接与初始化、指令集等核心主题对Java虚拟机进行全面而深入的分析...
以下是对"深入理解Java虚拟机学习资料"的详细解析: 一、JVM概述 Java虚拟机是Java平台的核心组成部分,它负责加载、验证、执行Java字节码,并管理内存。JVM的设计目标是实现“一次编写,到处运行”。通过JVM,Java...
第1章 :简单地介绍了Java虚拟机的历史并吹捧了←_← 一下Java的平台无关性(一次编译,到处运行); 第2章:概览Java虚拟机整体架构; 第3章:介绍如何将Java语言编写的程序转换为虚拟机指令集; 第4章:定义...
Java虚拟机(JVM)是Java编程语言的核心组成部分,它是一种抽象的计算设备,能够运行Java字节码。Java虚拟机规范(Java SE 7版)是...通过学习这份规范,开发者能够更好地驾驭Java平台,解决实际开发中遇到的复杂问题。
Java虚拟机(JVM)是Java程序运行的基础,它负责执行Java字节码,提供了一个与平台无关的执行环境。JVM规范定义了JVM的结构、指令集和运行时数据区,以及如何执行指令和处理异常。自1999年以来,JVM规范经历了多次...
java虚拟机规范,高清PDF版本,含有目录结构:第一章:引言; 第二章:java虚拟结构(运行时区域内存:寄存器,java虚拟机栈,java堆,方法去,运行时常量池,本地方法栈); 第三章:为java虚拟机编译; 第四章:...
Java虚拟机(JVM)是实现Java技术的关键组件,它为Java程序提供了一个运行环境。...JVM规范的深入学习对于Java开发者来说至关重要,它不仅帮助开发者编写更好的Java程序,还能够深入理解Java技术的内部工作原理。
中文版的《Java虚拟机规范》填补了国内关于JVM领域知识的空白,使得中国广大对JVM感兴趣的程序员能够克服语言障碍,深入学习和掌握这一关键技术。译者们在翻译过程中,注重保持作品的准确性与可读性,尽可能采用通俗...
Java虚拟机(Java Virtual Machine,简称JVM)是Java编程语言的核心组成部分,它是一个用于执行Java字节码的软件或硬件设备。Java程序在编译时并不直接转化为机器语言,而是转化为中间代码,即字节码。JVM的作用就是...
本书是继《深入理解Java虚拟机》之后的又一经典著作,它一方面遵循《Java虚拟机规范》,一方面又独辟蹊径,不仅能让Java虚拟机的学习变得更加简单和有趣,而且能让你对Java虚拟机的原理认识更深入和更深刻!...
随着越来越多的第三方语言(Groovy、Scala、JRuby等)在Java虚拟机上运行,Java...《实战Java虚拟机——JVM故障诊断与性能优化》将通过200余示例详细介绍Java虚拟机中的各种参数配置、故障排查、性能监控以及性能优化。
Java虚拟机(JVM)是Java编程语言的核心组成部分,它为Java程序提供了跨平台的运行环境。Java程序在编写完成后,会被编译成字节码(.class文件),这些字节码可以在任何装有JVM的系统上运行,实现了“一次编写,到处...
《深入Java虚拟机(原书第2版)》,原书名《Inside the Java Virtual Machine,Second Edition》,作者:【美】Bill Venners,翻译:曹晓钢、蒋靖,出版社:机械工业出版社,ISBN:7111128052,出版日期:2003 年 9 ...
通过深入学习《Java虚拟机(第二版)》,开发者不仅可以理解Java程序的运行机制,还能掌握性能优化、问题排查等高级技巧,提升自己的编程水平。这本书通常会详细讲解上述知识点,并提供丰富的示例和实践指导,帮助...