ITPub博客

首页 > 应用开发 > Java > 掌握JVM一篇就够

掌握JVM一篇就够

Java 作者:huangdazhu 时间:2018-11-15 15:48:01 0 删除 编辑

掌握JVM一篇就够

  • 程序的本质-堆和栈

  • JVM运行机制

  • 类的生命

  • JVM内存模型

  • 垃圾回收

  • JVM常见参数

追本溯源——堆和栈

堆通常是一个可以被看做一棵树的数组对象,栈是一种只能在一端进行插入和删除操作的先进后出线性表,JVM的本质是堆和栈


第一,从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。


第二,堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。


第三,栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。


第四,面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。不得不承认,面向对象的设计,确实很美。


JVM运行机制


此图看出jvm内存结构


JVM内存结构主要包括两个子系统和两个组件。两个子系统分别是Classloader子系统和Executionengine(执行引擎)子系统;两个组件分别是Runtimedataarea(运行时数据区域)组件和Nativeinterface(本地接口)组件。


Classloader子系统的作用:  根据给定的全限定名类名(如java.lang.Object)来装载class文件的内容到Runtimedataarea中的methodarea(方法区域)。Java程序员可以extendsjava.lang.ClassLoader类来写自己的Classloader。


Executionengine子系统的作用:  执行classes中的指令。任何JVMspecification实现(JDK)的核心都是Executionengine,不同的JDK例如Sun的JDK和IBM的JDK好坏主要就取决于他们各自实现的Executionengine的好坏。


Nativeinterface组件:  与nativelibraries交互,是其它编程语言交互的接口。当调用native方法的时候,就进入了一个全新的并且不再受虚拟机限制的世界,所以也很容易出现JVM无法控制的nativeheapOutOfMemory。


RuntimeDataArea组件:  这就是我们常说的JVM的内存了


JVM的生命周期

一、首先分析两个概念

   JVM实例和JVM执行引擎实例

  (1)JVM实例对应了一个独立运行的java程序,它是进程级别。

  (2)JVM执行引擎实例则对应了属于用户运行程序的线程,它是线程级别的。


二、JVM的生命周期

  (1)JVM实例的诞生:当启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。

  (2)JVM实例的运行 main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程。

  (3)JVM实例的消亡:当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。


类的生命


一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况


双亲委派模型  是一种组织类加载器之间关系的一种规范,他的工作原理是:如果一个类加载器收到了类加载的请求,它不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,这样层层递进,最终所有的加载请求都被传到最顶层的启动类加载器中,只有当父类加载器无法完成这个加载请求(它的搜索范围内没有找到所需的类)时,才会交给子类加载器去尝试加载.


认识JVM内存模型


对于JVM内存,程序员只需要关注

  • 对象去哪儿——堆内存(包括新生代和老年代,新生代又分为Enden区和两个Survivor区),堆内存用于存放对象,合理分区主要是为了提高垃圾回收效率,新创建的对象会在Enden区,经历Minor GC后会到Survivor区,经历一般15次GC还存活的话会进入老年代。

  • 函数如何调用——栈内存用于运行线程,它们包含了方法里的临时数据、堆里其它对象引用的特定数据。

  • 类去哪儿——方法区用来存储类型信息(运行时常量和静态变量)和方法代码和构造函数代码,通常也叫永久代,JDK8用元空间代替永久代


JVM资源一般分为两种CPU和内存,CPU代表着线程栈,内存代表着堆,而往往生产环境JVM问题一般都是这两种资源不足导致的,当线程栈使用不当的时候,通常会CPU爆满,当堆内存使用不当的时候,通常会出现内存溢出。


内存管理清洁工——垃圾回收

Java程序语言中的一个最大优点是自动垃圾回收,Java垃圾回收会找出没用的对象,把它从内存中移除并释放出内存给以后创建的对象使用。下面图片生动对比手动垃圾回收和自动垃圾回收的区别。垃圾回收重点关注碎片和性能。




初识垃圾回收算法


按回收策略

(1).引用计数算法:

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不再被使用的,垃圾收集器将回收该对象使用的内存。引用计数算法实现简单,效率很高,微软的COM技术、ActionScript、Python等都使用了引用计数算法进行内存管理,但是引用计数算法对于对象之间相互循环引用问题难以解决,因此java并没有使用引用计数算法。


(2).根搜索算法:

通过一系列的名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则该对象不可达,该对象是不可使用的,垃圾收集器将回收其所占的内存。


在Java语言里,可作为GC Roots对象的包括如下几种:

a.System Class,像rt.jar里面的java.util.*

b.Thread,开始状态的线程

c.虚拟机栈(栈桢中的本地变量表)中的引用的对象

d.方法区中的类静态属性引用的对象

e.方法区中的常量引用的对象

f.本地方法栈中JNI的引用的对象


(3).标记-清除算法:


最基础的垃圾收集算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。


标记-清除算法的缺点有两个:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。


(4).复制算法:

将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。


复制算法的缺点显而易见,可使用的内存降为原来一半。


(5).标记-清除-整理算法:


标记-整理算法在标记-清除算法基础上做了改进,标记阶段是相同的标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。


标记-整理算法相比标记-清除算法的优点是内存被整理以后不会产生大量不连续内存碎片问题。

复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记-整理算法效率会大大提高。


按分区回收

(1).分代算法:

根据对象的存活周期的不同将内存划分为几块。一般把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须使用“标记-整理”算法进行回收。


按系统线程

串行/并行回收方式:在GC过程中,单线程/多线程收集器采用Stop-the-World机制,做事专一吞吐量高;

并发回收:工作线程和垃圾回收线程并发执行,一心二用,吞吐量较低,不过保证在GC的时候,其它用户线程可以工作;


垃圾收集器


JVM常见参数

最后汇总一下JVM常见配置


类加载设置

-XX:+TraceClassLoading:类加载日志

-XX:+TraceClassUnloading:类卸载日志


堆设置

-Xms:初始堆大小

-Xmx:最大堆大小

-XX:NewSize=n:设置年轻代大小

-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4

-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5

-XX:MaxPermSize=n:设置持久代大小


收集器设置

-XX:+UseSerialGC:设置串行收集器

-XX:+UseParallelGC:设置并行收集器

-XX:+UseParalledlOldGC:设置并行年老代收集

-XX:+UseConcMarkSweepGC:设置并发收集器


垃圾回收统计信息

-XX:+PrintGC

-XX:+PrintGCDetails

-XX:+PrintGCTimeStamps

-Xloggc:filename


并行收集器设置

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。

-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间

-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)


并发收集器设置

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况

-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。


原创博文地址:https://blog.csdn.net/yeyincai/article/details/77885354


来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/28869493/viewspace-2220190/,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2013-05-08

  • 博文量
    184
  • 访问量
    403820