前言:
Java是一种技术,由四方面组成:Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API),本文章对JVM虚拟机(JVM)做一个概括性的总结,以便需要之时可以查阅。
(一)JVM的组成
* Class Loader SubSystem (类加载器)
* Runtime Data Areas (运行时数据区)
* Execution Engine (执行引擎)
(二)简述各部分的作用
Class Loader SubSystem
装载.class字节码到Java 虚拟机中,读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。通过此实例的 newInstance()方法就可以创建出该类的一个对象。
类装载器的组成:
(1)引导类加载器(bootstrap class loader):用来加载 Java 的核心库。
(2)扩展类加载器(extensions class loader):用来加载 Java 的扩展库。
(3)系统类加载器(system class loader):根据Java应用的类路径(CLASSPATH)来加载Java 类。一般来说,Java应用的类都是由它来完成加载的。
Runtime Data Areas
运行时数据区指JVM在运行期间,其对计算机内存空间的划分和分配。
运行时数据区组成有:
(1)Method Area:方法区(主要有运行时常量池、类型信息、字段信息、方法信息、类变量,指向class实例的引用。存储已被虚拟机加载的类信息、常量、静态变量,即编译器变异后的代码等数据)
(2)heap:堆,用来保存对象的实例
(3)Stack Area:虚拟机栈(为每个运行的Java方法创建一个栈帧,来存储局部变量表、操作栈、动态链接、方法出口、返回值等)
(4)PC:指令计数器
(5)Native Method Stack:本地方法栈(调用本地方法时创建,和VM stack相似)
Execution Engine
执行字节码,或者执行本地方法。
(三)Jvm生命周期
产生:启动一个Java程序,jvm实例就产生了。
运行:main()是起点。
注意:Java虚拟机内部分守护线程和非守护线。守护线程是为其他线程服务的,必须等到其他线程全部撤离,它才会消亡。
消亡:所有非守护线程终止了,Jvm退出。System.exit().
(四)GC回收机制
1、基本垃圾回收算法
按照基本回收的算法分
引用计数、标记-清除、复制(两块区域,复制正在使用的对象到另一边)、标记-整理(集合前两种优点:第一阶段开始标记所有被引用的对象,第二阶段遍历整个堆,清除未标记对象并将存活对象“压缩”到堆的其中一块,顺序排放)
按照分区对待的方式分
增量收集、分代收集
按照系统线程分
串行收集器、并行收集器、(CMS并发标记清理收集器)
2、自动内存回收机制
回收对象有:
简单地说:不再使用的对象,即没有了引用的对象。
具体地说:
(1)、 超出作用域或者引用计数器为空的对象
(2)、 从gc root开始搜索,搜索不到的对象(注意相关的强引用、弱引用、软引用、幻影引用区别等等)
(3)、 经过一次标记-清理,仍然存活的对象
机制主要有:
(1)引用计数收集器
(2)跟踪收集器(使用卡片表计算法实现)
(3)卡片标记算法(将老年代按照某个字节大小划分成不同区域,这个区域称为卡片。Jvm使用卡表维护卡片的状态。使用对象的引用和释放对象的引用,相应修改卡表中卡片的状态。每次minorGC只需扫描卡表中标志位脏态的对象。)
3、创建对象申请内存的过程
Java创建对象的实例时,申请内存空间的过程大致会如下:
(1). JVM会试图为相关Java对象在新生代的Eden区中初始化一块内存区域。
(2). 当Eden区空间足够时,内存申请结束。否则执行下一步。
(3). JVM试图释放在Eden区中所有不活跃的对象(Young GC)。释放后若Eden空间仍然不足以放
入新对象,JVM则试图将部分Eden区中活跃对象放入Survivor区。
(4). Survivor区被用来作为Eden区及老年代的中间交换区域。当老年代空间足够时,Survivor区中
存活了一定次数的对象会被移到老年代。
(5). 当老年代空间不够时,JVM会在老年代进行完全的垃圾回收(Full GC)。
(6). Full GC后,若Survivor区及老年代仍然无法存放从Eden区复制过来的对象,则会导致JVM无法
在Eden区为新生成的对象申请内存,即出现“Out of Memory”。
示意图如下:默认情况下内存分配 Eden:Survivor 区为8:1
JVM中共划分了三个代:新生代(Young Generation)、老年代(Old Generation)和持久代(Permanent Generation)。
其中持久代主要存放的是java类的类信息,与垃圾收集要收集的java对象关系不大。新生代和老年代的划分是对垃圾收集影响比较大的。
新生代:
所有新生成的对象首先都是放在新生代的。新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。新生代分为三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活将被复制到另外一个Survivor区,当这个Survivor区也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“老年代(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在新生代中的存在时间,减少被放到老年代的可能。
老年代:
在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代中。因此,可以认为老年代中存放的都是一些生命周期较长的对象。
持久代:
用于存放静态文件,如java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久空间来存放这些运行过程中新增的类。持久代大小通过 -XX:MaxPermSize =
4、GC的分类
1、对新生代的对象的收集称为 minor GC
2、对老年代的对象的收集称为 Major GC
3、程序中主动调用System.gc()强制执行的GC为 Full GC
与之相对的回收算法如下:
新生代垃圾回收 – 标记-复制算法
老年代垃圾回收 – 标记-清除或者标记-整理
触发full GC的情况有四种:老年代空间不足、永久代空间满、CMS出现promotion failed和concurrent mode failure 、统计得到的minor GC级升到老年代的平均大小大于剩下的空间时
5、一道常见题目
GC在什么时候、对什么东西,做了什么事情?
时间:
程序员不能控制具体的时间。系统在不可预测的时间调用system.gc()函数。
Eden区满了触发minor GC。
当触发minor GC时,又有对象从survivor区升级到老年区,由于老年区剩余空间不足,触发major GC。
触发full GC的情况有四种:
1 老年代空间不足(升到老年代的对象大于老年代剩余的空间触发full GC)。
2 永久代空间不足时。
3 CMS出现promotion failed和concurrent mode failure 。
4 统计得到的minor GC级升到老年代的平均大小大于剩下的空间时,用NewRatio控制NewObject和OldObject的比例,用MaxTenuring控制进入OldObject的次数,使用OldObject存储空间延迟达到full gc,从而使得计时器引发gc时间延迟OOM的延迟,以延长对象生存期。
何物:
1、超出作用域或者引用计数器为空的对象。
2、从gc root开始搜索,搜索不到的对象(注意相关的强引用、弱引用、软引用、幻影引用区别等等)。
3、经过一次标记-清理,仍然存活的对象。
做了什么:
1、删除不使用的对象,回收内存空间。
2、运行默认的finalize(),想立刻调用,就用depose以释放资源如文件句柄,JVM用from survivor、to survivor对他进行标记清理,对象序列化之后也可以使它复活。
3、每次minor GC、major GC、full GC时,对象的复制、清理、各种回收机制的优缺点
minor GC时,Eden区为新生对象分配空间,如果空间不满会清除非存活对象,将还存活的对象移到survivor区,并整理。
Major GC时,老年代的垃圾回收算发取决于JVM采用的是什么垃圾回收器,扫描老年代对象,删除没有使用的对象。
full GC时,除了System.gc()之外,还有四种情况会触发full gc,需要对这四种情况进行更深入的理解。
6、内存溢出与内存泄漏
内存溢出Out of Memory(OOM)
要求分配的Java虚拟机的内存超过了系统所能提供的大小,内存不够产生溢出。
内存泄漏Memory leak
程序中动态分配了内存,但是在程序结束时,已经没有引用指向这个内存代表的对象,GC没有回收这块内存,这块空间处在一种游离的状态。重启电脑可以解决,但是仍有可能再次出现内存泄漏,这是软件设计缺陷引起的。
内存泄漏、溢出的异同?
同:
都会导致应用程序运行出现问题,性能下降或挂起。
异:
(1) 内存泄漏是导致内存溢出的原因之一;内存泄露积累起来将导致内存溢出。
(2) 内存泄漏可以通过完善代码来避免;内存溢出可以通过调整配置来减少发生频率,但无法彻底避免。
如何应对内存泄漏、溢出:
(1)尽早释放无用的对象引用,比如将引用设置为null
(2)程序进行字符串处理时,尽量避免使用String,而应使用StringBuffer。
(3)尽量少用静态变量。
(4)避免集中创建对象,比如创建大对象,或者循环创建很多对象。
(5)尽量运用对象池技术,以提高性能。
(6)不要经常调用某个方法区创建对象,可适当使用hashtable、vector创建一组对象容器,使用时去容器里面取出对象使用,而不是每次都去new。
(7)优化配置。