JVM结构和细节

JVM结构细节

jvm理论上应该属于java基础,不过从目前行业的情况来看,反而成了挺高端的东西。可能因为做java的,绝大部分是在做业务系统,大部分时间都花在了业务梳理和前端的事情上了,而且平时普遍看书少。不管怎么说,既然入了java这行,jvm的详细的结构和运行机制还是必须懂要求懂的。自己之前的一家公司做的是互联网通信,由于量慢慢起来出现性能问题,中途折腾了点但总算解决了。时间长了怕忘记,这里重新整理一下关于jvm的东西。

先上图预习下,重要的部分使用了粗体。

 

下面对每一部分做详解。

类加载器:

也就是常说的class loader,完整的类加载过程有加载->链接(验证+准备+解析)->初始化->使用->卸载。根据java的动态特性,一个类只有用到的时候才会被加载。所以即使你lib里存放了一大堆jar包,只要不用是不会影响性能的。加载时候的jar文件寻址过程如下。

1.Jre目录,一般是rt.jar

2.Classpath目录

3.启动参数-Djava.ext.dirs指定的扩展目录

4.程序当前目录

类加载后是被存放在方法区中的,一会再对这个区详解。

执行引擎:

简单说就是执行程序的部分。

本地方法接口:

简单说就是native方法。

垃圾收集器:

负责GC的模块,具体操作的是运行时数据区,这里不作为重点,下面介绍运行时数据区域时候再详谈。

运行时数据区域:

所谓运行时数据区域,不难理解就是java运行时动态的存放数据的区域。作为以动态性著称的java,理解这块区域是非常重要的。

堆(Heap):

堆是被所有线程共享的数据区域,仅仅存储对象的实例。所有被实例化的对象都是存储在堆中,不存储任何基本数据类型。堆的默认初始化大小是物理内存1/64,默认最大物理内存1/4。堆里面划分为年轻代和老年代,默认年轻代大小为堆的1/3。年轻代包含1个伊旬区和2个幸存区,默认伊旬园大小为年轻代的1/9,2个幸存区是大小完全一致的对称的区域。伊旬区其实就是初始区,所有对象首次被创建的时候都是存放在伊旬区里,英文为Eden,老外用这两个字定义初始区,可见其含义之深。另外,以上说的默认大小都是常规linux64位+JDK1.5以上且-server模式的情况,仅给一个基本参考。具体环境差异可能很大,请查阅官方文档,实际应用中不要使用默认值。

年轻代的GC方式称为复制GC,从其作用范围而言,称为MINOR GC,具体过程如下:

1.对象在伊旬区中创建,当伊旬区满后,将仍然被使用的对象转移到幸存区1;

2.重复以上步骤,当幸存区1满以后,发生一次MINOR GC。扫描幸存区1中仍 被使用的引用的对象,全部转移到幸存区2,同时清空幸存区1;

3.这时候,伊旬区满且仍然被使用的对象,将被转移到幸存区2;

4.当幸存区2满以后,重复步骤2,将数据转移的幸存区1;

5.当数据在幸存区1和2中往复转移多次仍然被引用,将被转移到老年代中;

以上多次提到的“仍然被引用”,可以查看一些根搜集算法的网络文章。最后提到的“多 次使用”具体是多少次,这个不确定,一般10次以内。复制GC的效率非常高,代价 也很明显,两个幸存区代表双倍的内存空间。年轻代发生的MINOR GC,一般来说发 生的间隔不小于10s,执行的时耗不大于100ms,否则应该对程序或者jvm做优化。

老年代的GC方式为标记清除GC,从其作为范围而言,称为MAJOR GC。当然,这些称呼都是非正式的,有些直接呼之为FULL GC,其实都没有错(个人感觉为了对应JVM工具的一些输出称呼,叫FULL GC更好些)。另一方面,方法区也会触发FULL GC,所以即使都是FULL GC,清理的区域和范围也可以认为是不一样的。这里老年代产生的FULL GC,可以理解为清理老年代,具体过程如下:

1.对整个老年代做扫描,找到已经不再被引用的对象;

2.将不再引用的对象,打上标记;

3.对老年代做一次压缩,将打了标记的对象转移到连续的内存区域(有点类似磁盘碎片整理);

4.删除打了标记的对象,腾出新的连续的内存空间;

FULL GC的系统开销比较大,尽量减少频率和时耗,一般需要控制在周期60s以上和时耗1s以内。曾经FULL GC呼之为stop the word(停止一切),可见其对程序运行的影响有多大。当然现在多核CPU时代,这一点已经改善,但还是尽量少吧。

方法区(持久区):

方法区也是被所有线程共享的。如前面所说的,类被加载的时候,类的描述信息(成员、关系,方法等等)全部是存放在方法区里的。除此之外方法区还存储static类型的变量。如果是基本数据类型,直接存储值,如果是对象类型,存储引用,对象实例还是放在堆里。这些统称为值类型吧(基本类型+引用)。从功能上看,完全可以理解该区为何叫方法区或者持久区。为了对应程序的一些输出,我们称为持久区好些。持久区默认初始大小物理内存1/64,默认最大值物理内存1/4。

大家都遇到过的内存溢出有:

java.lang.OutOfMemoryError: PermGen space

java.lang.OutOfMemoryError: Java heap space

java.lang.OutOfMemoryError: unable to create new native thread

第一种就是持久区满了且无法GC出空闲空间导致,一般是你的jar文件太多导致;第二种很明显是上面介绍过的堆的老年代满了;第三种未曾接触过,暂且不评论。

持久区是否会GC?会GC,也叫FULL GC,老年代触发的GC称呼一样,作用域不一样。

JAVA线程栈(stack):

JAVA线程栈是单个JAVA线程独享的,每个JAVA线程都有自己的线程栈。

线程栈里的上下文和操作指令区就不说了,要注意的是该区存放基础变量类型和对象的 引用,也就是值类型。这部分跟持久区有点类似,只不过持久区存放的是static类型的。 线程栈在我们常用的Linux64位中默认大小是1m,死循环,递归太深,变量太多都可 能导致溢出。

本地方法栈:

Java栈对应的native方法栈,每个线程一个。

程序计数器:

也叫PC寄存器,每个线程一个。

JVM优化入门:

程序性能瓶颈排查和优化是很大的学问,涉及到软硬件。这里说的优化仅是对jvm的部分优化和建议,有空再写篇详细的排查和优化文章。

一般来说,常用的jvm参数有以下:

-server

服务器模式,必用

-Xms1g

JVM内存初始大小为1g(年轻+老年+持久)

-Xmx1g

JVM内存最大大小为1g

-Xmn512m

年轻代大小为512m

-Xss512k

Java每个线程栈大小512k

-XX:ThreadStackSiz=512

跟Xss一样,单位k,不要重复设置,否则可能冲突

-XX:NewRatio=4

年轻代与老年代比值为4,N=1,O=4

-XX:SurvivorRatio=4

幸存区总和与伊旬区比值为4,S=1,E=4,每个S应该为1/6

-XX:PermSize=64m

持久区初始大小为64m

-XX:MaxPermSize=64m

持久区最大大小为64m

-XX:MaxTenuringThreshold=7

对象经过7次MINOR GC后进入老年代,注意可设最大值为15

-XX:+UseParallelGC

并行收集器,推荐

-Xloggc:gc.log

GC日志输出到文件,学习调优必备

完整的JVM参数设置,可以查看官方文档:

http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

使用jinfo xxx命令可以查看jvm运行基本参数,其中xxx是进程PID。

为减小扩容操作,建议Xms和Xmx设置为一样的值。

设置Xss越小可跑线程越多,推荐控制在1000以内,特殊情况不要超过2000。例如tomcat生产环境一般最大线程数都是设置为1000。不是线程越多约好,线程切换损耗资源。如果线程数已经设置很大,但是硬件资源还剩很多,可以跑多个JVM实例。

GC情况检测常用命令是

jstat -gc 6856 1000

jstat -gcutil 6856 1000

6856是进程PID,1000是刷新毫秒数,-gc查看值,-gcutil查看比例,范例输出如下:

  

因为我是监控的是我未运行任何程序的eclipse进程,所以数据没啥变化。每个字段首字母含有请根据文章介绍自行判断,尾字母C代表current(当前),U代表Usable(可用),T代表Total(总共)。

 

阅读更多
换一批

没有更多推荐了,返回首页