夜光序言:
“宰相肚里能撑船”,心有多大,天地就有多大。凡事,看淡一些,看开一些,看远一些,就会发现“潮平两岸阔,风正一帆悬”并非妄念。学会原谅那些无心之失,学会坦然面对事业、生活中的不如意。做一个大气、大度、大方的人,一笑而过,也是一种智慧。
正文:
三、java虚拟机(JVM)
1.java虚拟机的原理:
java虚拟机就是一台虚拟机器,是一款软件,用来执行一系列虚拟计算机指令,虚拟机分为系统虚拟机和程序虚拟机。
2.java虚拟机的基本结构:
1.类加载子系统
负责文件系统或者网络中加载class信息,加载的信息存放在一块称为方法区的内存空间
2.方法区
存放类信息,常量信息,常量池信息,包括字符串字面量和数字常量等。
3.java堆
在虚拟机启动的时候建立java堆,它是java程序主要存放的内存工作区域,几乎所有的对象实例都存放在java堆中,堆空间是所有线程共享的。
4.直接内存
java的NIO库允许java程序使用直接内存,从而提高性能,通常直接内存速度会优于java堆,读写频繁的场合推荐使用。
5.java栈
每个虚拟机线程都有一个私有的栈,一个线程的java栈在线程中创建的时候被创建,java栈中保存着局部变量、方法参数,同时方法的调用、返回值等。
6.本地方法栈
本地方法栈和java栈相似,最大不同为本地方法栈用于本地方法调用,java虚拟机允许java直接调用本地方法(通常使用C编写)。
7.垃圾回收系统
垃圾收集系统是java的核心,也是必不可少的,java有一套自己进行垃圾清理的机制,开发人员无需手工清理
8.PC寄存器
是每个线程私有的空间,java虚拟机会为每个线程创建PC寄存器,在任何时刻一个java线程总是在执行一个方法,这个方法称为当前方法,如果当前方法不是本地方法,PC寄存器就会执行当前正在被执行的指令
如果是本地方法,则寄存器值为undefined,寄存器存放如当前环境指针、程序计数器、操作栈指针、计算的变量指针等信息。
9.虚拟引擎
虚拟机最核心的组件是执行引擎,负责执行虚拟机的字节码,一般进行先编译成机器码后执行。
10.堆、栈、方法区概念和联系
堆:堆解决的是数据存储问题,即数据怎么放,放在哪
栈:栈解决程序运行问题,即程序如何执行,或者如何处理数据
方法区:方法区则辅助堆栈的快永久区,解决堆栈信息的产生,是先决条件
11.java堆
java堆是和java应用程序关系最密切的内存空间,几乎所有对象都存放在其中,并且java堆完全是自动化管理的,通过垃圾回收机制,垃圾对象会自动清理,不需要显示的释放。
根据垃圾回收机制不同,java堆有可能拥有不同的结构,最为常见的是将整个java堆分为新生代和老年代,其中新生代存放新生的对象,老年代则存放老年对象。
新生代分为Eden区、S0区、S1区,S0和S1也被称为from和to区域,两块大小相等并且可以互换角色的空间。
绝大多数的情况下对象首先分配在Eden区,在一次新生回收后,如果对象还存活,则进入S0或者S1区,之后每经过一次新生代回收,如果对象存活则它的年龄就加1,当对象达到一定的年龄后,则进入老年代。
12.java栈
java栈是一块线程私有的空间,一个栈由三部分组成:局部变量、表、操作数栈和帧数据区。
局部变量表:用于报错函数的参数及局部变量。
操作数栈:主要保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
帧数据区:除了局部变量表和操作数栈以外,栈还需要一些数据来支持常量池的分析,这里帧数据区保存着访问常量池的指针,方便程序访问常量池,另外当函数返回或者出现异常时,虚拟机必须有一个异常表,方便发送异常的时候找到异常的代码,因此异常处理表也是帧数据区的一部分。
13.java方法区
java方法区和堆一样,方法区是一块所有线程共享的内存区域,它保存系统的类信息,比如:类的字段、方法、常量池等。方法区的大小决定了系统可以保存多少类,如果系统定义太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误,方法区可以理解为永久区(perm)。
3.垃圾收集算法
1.引用计数法:这是个古老而经典的垃圾收集算法,其核心是在对象被其他所引用时计数器加1,而当引用失败则减1,这种方式有非常严重的问题,1.无法处理循环引用的情况,2.每次进行加减操作比较浪费系统性能。
2.标记清楚法:分为标记和清除两个阶段进行处理内存中的对象,这种方式也有很大的弊端,就是空间碎片问题,垃圾回收后的空间不是连续的,不连续的内存空间的工作效率要低于连续的内存空间。
3.复制算法:其核心思想是将内存空间分为两块,每次只使用其中一块,,在垃圾回收时将正在使用的内存中存留的对象复制到未被使用的内存块中去,之后清除之前正在使用的内存块中所有的对象,反复去交换两个内存的角色,完成垃圾收集(java中新生代的from和to空间就是使用这个算法)。
4.标记压缩法:标记压缩在标记清除法基础上做了优化,把存活的对象压缩到内存一端,而后进行垃圾清理(java中的老年代使用的就是这个算法)。
5.分代算法:根据对象的特点把内存分成N块,然后根据每个内存的特点使用不同的算法。
对于新生代和老年代来说,新生代回收频率很高,但是每次回收耗时都很短,而老年代回收频率较低,但是耗时较长,所以应该尽量减少老年代的GC。
6.分区算法:其主要是将整个内存分为N多个小的独立空间,每个小的空间都可以独立使用,这样细粒度的控制一次回收多少个小空间和哪些个小空间,而不是对整个空间进行GC,从而提高性能,并减少GC的停顿时间。
4.虚拟机参数
在虚拟机运行的过程中,如果可以跟踪系统的运行状态,那么对于问题的故障排查会一定的帮助,为此,虚拟机提供了一些跟踪系统状态的参数,使用给定的参数执行java虚拟机,就可以在系统运行时打印相关日志,用于分析实际问题,我们进行虚拟机参数配置,其实主要围绕着堆、栈、方法区进行配置。
5.堆分配参数
-xx:+PrintGC 使用这个参数,虚拟机启动后,只要遇到GC就会打印日志
-xx:+UserSerialGC 配置串行回收器
-xx:+PrintGCDetails 可以查看详细信息,包括各区的情况
-xms:设置java程序启动时初始堆大小
-xmx:设置java程序能获得的最大堆的大小
-xmx20m-xms5m-xx:+PrintCommandLineFlags:可以将隐藏式或者显示传给虚拟机的参数输出。
总结:在实际工作中,可以直接将初始化的堆大小与最大堆大小设置相等,这样的好处是可以减少程序运行时的垃圾回收次数,从而提高性能。
6.新生代配置
-xmn:可以设置新生代大小,设置一个比较大的新生代会减少老年代的大小,这个参数对系统性能以及GC行为有很大的影响,新生代大小一般会设置整个堆空间的1/3到1/4左右。
-xx:SurvivorRatatio:用来设置新生代中Eden空间和from/to空间的比例。
总结:不同的堆分布情况,对系统执行会产生一定的影响,在实际工作中,应该根据系统的特点做出合理的配置,基本策略:尽可能将对象预留在新生代,减少老年代的GC次数。
除了可以设置新生代的绝对大小(-xmn),还可以使用(-xx:NewRatio)设置新生代和老年代的比例,比如:-xx:NewRatio=新生代/老年代。
7.堆溢出处理
在java程序的运行过程中,如果堆空间不足,则会抛出内存溢出的错误(Out of Menory),OOM ,一旦这类问题发生在生产环境,可能引起严重的业务中断,java虚拟机提供了-xxHeapDumpOnOutOfMemoryError,使用该参数可以在内存溢出时导出整个堆信息,
与之配合使用的还有参数:--xx:HeapDumPath:可以设置导出堆的存放路径。
内存分析工具:Memory Analyzer
8.栈配置
java虚拟机提供了参数-xss来指定线程的最大栈空间,整个参数也直接决定了函数可调用的最大深度
9.方法区
方法区和java堆一样,方法区是一块所有线程共享的内存区域,用于保存系统的类信息,方法区(永久区)可以保存多少信息可以对其进行配置,在默认情况下,-xx:MaxPermSize为64MB,如果系统运行时生产大量的类,就需要设置一个相对合适的方法区,以免出现永久区内存溢出的问题。-xx:PermSize=64 -xx:MaxPermSize=64
10.直接内存配置
直接内存也是java程序中重要的组成部分,特别广泛用在NIO中,直接内存跳过了java堆,使java程序可以直接访问原生堆空间,因此在一定程度上加快了内存空间的访问速度。
相关配置参数:-xx:MaxDirectMemorySize,如果不设置默认值为最大堆空间,即-xmx。直接内存使用达到上限时,就会触发垃圾回收,如果不能有效的释放空间,也会引起系统的OOM。
11. client和server虚拟机工作模式
目前java虚拟机支持client和server两种运行模式,使用参数-client可以指定使用-client模式,使用-server即使用server模式,可以直接在命令行查看当前计算机系统自动选择的运行模式,java -version即可
二者之间的区别:client模式相对server启动较快,如果不追求系统的长时间使用性能仅仅是测试,可以使用client模式,而server模式则启动较慢,原因是会对其进行复杂的系统性能信息收集和使用更复杂的算法对程序进行优化,生产环境都使用server模式,长期运行其性能要远快于client模式。
12.垃圾回收时的停顿现象
垃圾回收器的任务是识别和回收垃圾对象进行内存清理,为了让垃圾回收器可以高效的执行,大部分情况下,会要系统进入一个停顿的状态,停顿的目的是终止所有的应用线程,只有这样系统才不会有新的垃圾产生,同时停顿保证了系统状态在某一个瞬间的一致性,也利于更好的标记垃圾对象,因此在垃圾回收时,都会产生应用程序的停顿。
13. 对象如何进入老年代
一般而言对象首次创建会被放置在新生代的Eden区,如果没有GC的介入,则对象不会离开Eden区,那么Eden区的对象如何进入老年代,一般来讲只要对象的年龄达到一定的大小,就会自动离开年轻时代进入老年代,对象年龄是由对象经历数次GC决定的,在新生代每次GC之后如果对象没有被回收则年龄加1,虚拟机提供了一个参数来控制新生代对象的最大年龄,当超过这个年龄范围就会晋升老年代。
-xx:MaxTenuring Threshold : 默认情况下为15。
总结:根据设置MaxTenuring Threshold参数可以指定新生代对象经过多少次回收后进入老年代,大对象(新生代Eden区无法装入时,也会进入老年代),JVM里有个参数可以设置对象的大小超过在指定的大小之后,直接晋升老年代。
-xx:PretenureSize Threshold
使用PretenureSizeThreshold可以进行指定进入老年代的对象大小,但是要注意TLAB区域优先分配空间。