为什么要有JVM?
JVM就是Java运行虚拟机,那么虚拟机又分为系统虚拟机和程序虚拟机,而JVM是属于程序虚拟机,所以不要看到是虚拟机就误认为JVM是系统虚拟机。
JVM是帮助Java程序开发者在开发过程中无需考虑无用的资源需要进行回收,避免内存溢出等问题且实现在不同平台上运行Java程序。
如: 开餐馆,你每天要把店铺的垃圾拉到垃圾厂去,如果你不拉或忘记拉,越积越多垃圾会堆满你的店铺,甚至还会堆到别的店铺去,不止你自己的店铺无法营业,别人的店铺也无法营业,随着时间的累积,整条街的店铺都给垃圾堆满,都无法营业。
在看到这种情况房东就有意见了,房东说:”你们每个月都给我多交一些钱,我解决垃圾这个问题。(JVM运行也要资源)”
房东会在街道上摆放上大的垃圾桶,定时的检查,如果垃圾桶满了,先拉到一个集中的地方,等这个区域的垃圾慢慢的差不多满了,就把这些垃圾拉到垃圾厂进行处理。
当这个时候我觉得房租太贵了,我搬去别的地方,那么又是一个新的房东,这个房东对垃圾处理这个问题就不一样了,他可能要求你必须要买我的袋子装着的,我才会去处理这个垃圾。
相关的管理者看到这个情况,马上说:“接下来,垃圾的这个问题,由我们安排的人来统一处理,以后不管你搬去哪里,只要你到我们的官网上填一份表格就行了。(运行环境)”
如果没有JVM,可以脑补一下。
JVM是什么?
在弄清楚JVM是什么之前,先弄清楚JDK、JRE是什么?
JDK就是开发的工具包,包括了JRE。
JRE是Java运行环境,包括了JVM和Java核心类库
JVM就是Java程序运行平台,拥有自己的指令集,抽象操作系统和CUP结构、内存结构,在运行时操作不同的内存区域。
Java程序编译后的文件是*.class文件,*.class文件是按照Java标准编译的文件,JVM是实现了Java制定的标准,因此JVM是可以运行Java程序的,而JVM是一个虚拟出来的机器,通过自定义的执行引擎、接口等实现方式与实际机器各种交互,使得Java程序在运行过程与实际机器无耦合,从而实现跨平台。
如: 以上的例子,相关管理者只管理各自的区域垃圾问题,各自的管理者都使用不同的颜色,导致每次只要有分店在别的地点开张,那么这个分店就要换垃圾袋颜色,否则管理者不承认这个垃圾是他管的。
生产垃圾袋的厂家觉得这样也不好,回收回来的袋子还要做分类,不利于他回收袋子,于是和各个店家商量都只用一种颜色的袋子,厂家也只生产这个颜色的袋子,不管管理者,厂家愿意这样做,店家也愿意用。
于是生产厂家就只生产一种颜色的袋子,一种袋子到处通用(Compile Once,
Run Everywhere.)。
作用
给Java程序提供一个独立的运行环境
特点
无需依赖于任何系统、平台之上。
优点
跨平台
可扩展性强
……
按照无需依赖任何系统,独立运行环境大家可以试下这个思路去分析,这里就不写那么多了。
注:每个人的学习方式不一样,以上只是提供一个思路而已。
缺点
JVM是一个程序虚拟机,但始终还是要运行在操作系统之上的,初始化的时候需要与操作系统建立各种交互,导致启动时间长,与操作系统交互导致资源的消耗……
相当于一个苹果放在一个盘子上,盘子放了一些水,而苹果在放进去的时候,盘子的水会高涨,如果超过盘子就会溢出,所以有一个盘子的要求,要么就对放入的苹果大小,质量进行控制,以达到要求,另外苹果的灵活性也无法自由的变化形状,所以放入占用了一定的位置导致能放的东西越来越少,盘子放入更多的东西的时候会越来越挤……
至于其他的缺点,大家可以试下这个思路去做分析。
注:每个人的学习方式不一样,以上只是提供一个思路而已。
主流JVM
名称研发者特点
HotspotLongview Technologies开发,然后被sun收购性能出色,复杂度有点高
JRockitBEA公司,被oracle收购任务控制能力出色,合并到Hotspot
J9IBM研发IBM内部使用,往往需要和IBM套件共同使用
HarmonyIBM和Intel研发的,捐给Apache作为孵化项目Apache退出了JCP之后,慢慢的就没有什么商用了
注:接下来讲的是Hotspot虚拟机,但虚拟机基本差不多,但JRockit是没有解释器的,这些区别自己去了解。
探索JVM内部
编译器
为什么要有编译器?
那么我们先来假设下没有编译器的情况吧
如果没有编译器,我们现在编写一个程序,这个程序是在windows上编写的,开发人员的本地测试也是在windows进行测试的,但环境部署上去的机器是Liunx时,这个时候两个操作系统的机制以及执行的字节码可能不一样,如( / )在Liunx和windows的表示都是不一样的,所以这种差别是使得开发人员很痛苦,难道每一次部署的系统环境不一样时或开发的系统环境不一样的时候就要写不同的代码吗?
而编译器的作为就是将开发人员写的代码编译成为一份是由JVM专门识别的一个字节码,直接由JVM进行运行,不在与操作系统有关。
是什么?
将开发人员写的*.java的源代码编译成字节码(*.class),这种字节码也可以叫做JVM的机器语言
执行过程
符号表:就是由符号地址和符号的信息所组成的表格,符号表其实就是记录编译的时候读取的信息。
词法分析:源代码的字符流,转换为标记的集合(字段标记,方法的标记等),并检查词法是否是正确的。
语法分析:是将词法分析后的这个标记的集合转换为一个树状结构的表现形式,并检查语法等是否是正确的。
注解处理:就是处理语法分析之后的这个树状结构的内容,注解处理时是可以对内容进行增删改查的,如果对这个语法分析后的树状结构数据进行更改了,那么编译器将回到解析和填充符号表的过程中重新处理。比如:标识这个值是a和b的变量相加得来的,大家去看看*.class文件的内容就知道了
语义分析:对语法分析之后的这个树状结构进行读取,并且对其上下文的联系是否合理进行上下文的分析,类型是否匹配、方法是否有返回值、将判断泛型等编译成简单的语法结构等……
字节码生成:将各个步骤的所产生的信息及存储在符号表内的信息进行转换为字节码,写出为*.class文件。
如:现在商家想要得到垃圾袋,要去管理者那申请,先填写申请单,管理者要制作这些申请单,管理者会先记下大概要填写的几个模块(词法分析),在各个模块中将要填写的内容写上去,在形成一个树状化的展示形式申请单(基本信息– 名字)(语法分析),对一些要填写的地方进行注解(注解处理),这个时候申请单就做好了,给到各个商家,商家填写完成后,要检查商家填写的内容是否有错误(语义分析),最后没有问题了,那么根据申请单信息进行审核,审核通过了则给袋子给商家。
*.class文件内容:
结构信息:文件版本号、各个部分数量、大小等信息。
元数据:常量的信息、继承的类、实现的接口、声明的信息、常量池等信息。
方法信息:语句和表达式对应信息,字节码、异常处理表、求值栈和局部变量的大小,求值栈的类型记录等信息
类装载系统
为什么要有类装载机制?
类加载系统-图一
类加载系统-图二
大家看下类加载系统-图一和类加载系统-图二,作者建立了一个类,这个类是和Java自身提供的java.lang.String是一样的包名和类名。
可以想象一下如果这个类成功执行了,那么接下来如果有别的类在引用的时候,应该先引用作者写的这个,还是引用Java自身的java.lang.String类?
如:以上的例子大家都知道,垃圾袋的颜色已经统一成一模一样了,有一天A商家在门口放了两袋垃圾袋,一袋是装着打碎的瓷盘碎片,一袋是装着厨房的垃圾,都绑着。
马上就要下班了,员工准备拿垃圾袋去扔,由于垃圾袋绑着,这位员工赶着去约会,直接往垃圾袋随时一抓,好了,结果大家猜到了,于是在第二天员工聪明了向商家要求,由于垃圾袋颜色一样,要求垃圾袋贴上小纸条,标明哪个垃圾袋是装什么的,这样做的之后,提高了安全性,而且又能够保证在做垃圾分类的时候可以保证各个垃圾袋放的东西是按照垃圾分类的要求放的。
类装载系统是由数个加载器组成的,负责将class文件信息加载进内存,存放在数据区-方法区内。
加载:通过完全限定名查找到这个类的字节码文件,将其静态的存储结构转化虚拟机的方法区运行时数据结构,并生成一个代表这个类的对象,这个就是为什么可以进行反射操作。
类加载过程:
为什么要这样加载?
当类在加载的时候,如类加载系统-图一,定义一个java.lang.String是一样的包名和类名,而类加载的时候是通过限定名去查找这个类字节码文件的,那样就出现了相同的内容,那么则出现了冲突,破坏了Java内部的完整性以及一致性。
在类加载系统-图二中我们可以看到,类加载器是有父类的,所以在↑查找的时候,是查找类是否已经在启动等过程中,已启动的加载器加载了,如果查找到的所有加载器都没有加载,那么则向下查找,哪个加载器是可以加载到这个类的,而这个也叫做双亲委派机制,而Tomcat、Jboss都依照Java规范有着实现了加载器。
双亲委派机制:可以理解为不止是坑爹的,且还是坑到爷一代的,在接到一个类加载的请求的时候,会先问他爹加载了没有,他爹会问他爷加载了没有,他爷也没有加载,那么就会给回去,最后就只好自己加载了,也就是只有父类无法完成的任务才自己完成。
验证:文件格式验证、元数据验证、字节码验证、符号引用验证,目的在于确保字节码文件内的信息符合虚拟机的要求并不会破坏到虚拟机的内容(虚拟机的一致性,完整性)。
准备:为类变量(静态变量)分配对应的内存,并设置这些类变量的值,这些内存是分配方法区的内存,但设置的这些类变量的值,具体要看是否有final修饰符,如果没有那么则无论值是多少都是为0,如果修饰符有final,那么设置的这个值则就是类变量的值。
解析:将符号引用转化为直接引用,符号引用就是通过对应的符号找到目标对象,符号可以是字面量,符号引用和虚拟机内存的布局是无关的,因符号引用的对象可以不加载到内存里,直接引用就是存在于内存中的,是有指针可以指向到的。
虚拟机没有规定解析的时间,只需要在anew arry、check cast、get field、instance of、invoke interface、invoke special、invoke static、invoke virtual、multi anew array、new、put field和put static这13个用于操作符号引用指令执行之前,对符号引用进行解析。
所以虚拟机会判断是在类被加载器加载的时候对符号引用进行解析还是等符号引用在要被使用前去解析,可以通过看上面的13个指令去得到答案。
解析的东西主要是类、接口、字段、类方法、接口方法这五类进行解析。
无非就是不管你这个类是接口还是实现类,还是什么,只要你是个类,那么就解析你里面的所有内容。
初始化:类初始化的时候就是触发到了new、getstatic、putstatic或invokestatic这4条指令的时候,也就是通常在开发过程中new对象的时候,读取或设置一个的静态字段,以及调用静态方法,被final修饰与已被编译器把结果放入常量池的静态字段除外。
初始化一个类的时候其父类还没初始化,也会触发其父类初始化。
但JVM最先初始化的是,main()方法这个类。
如:现在卫生局要检查卫生了,卫生局通过信息文档的省、市、区、详细地址这个信息知道了接下来要检查卫生的店在哪里,检查的结果是要写在对应的检查结果文档上的,所以要将这个信息文档上的信息(地址、营业执照等)转化为检查结果文档的格式,并生成一个这个要检查的店的专门一个检查结果的基本文档,接下来要先在系统上记录什么时候去检查,验证这些信息有没有输入错误,接下来要根据这家店的情况,准备下检查的固定事项(final),以及一些可能临时的事项(类变量),检查人员要准备出发了,要先把检查结果文档下载下来,打印成文件出来便于做记录,接下来检查人员到了店面进行检查,检查完了之后将记录下来的内容结果上传到对应的系统上。
解释器
为什么要有?
我们都知道JVM是为了实现跨平台,写一次到处跑的实现理念,但不同的机器可能因为生产厂家或操作系统等原因有着不一样的标准,那么其机器底层执行的指令等可能各有区别。
是什么?
将要执行的字节码转换为机器码,而这个解释是一句一句的解释,这个也说明了Java是解释性语言。
即时编译器(JIT)
为什么要有?
刚刚我们说到了解释器,其实JIT和解释器做的事情是一样的,但如果每次都要进行一句句的解释,那么效率太低。
是什么?
JIT和解释器做的事情是一样的,都是为了将要执行字节码转换为机器码,而不一样的是,JIT类似在编程的过程中将经常使用的数据放到缓存中,所以JIT会把经常使用的字节码,如:循环等高频率使用方法,它是以方法为单位一次性将整个方法的字节码编译为机器码。
而对于一个方法是否是经常使用,会通过探测热点的方式。
既然是探测热点的方式,这里提一个最基本的思路,用一个计数器,但达到相应的阈值的时候就判定是热点代码,但是维护比较麻烦,接下来我们会提到内存哪一块是线程独有,哪一块是线程共享,这里就会存在问题,技术有优点也有缺点。
执行引擎
为什么要有?
当编译器转换为机器码了之后总要有东西去告知底层操作系统或某个操作者,接下来要做什么。
是什么?
执行引擎主要还是告知底层操作要做的事情,只是一个概念上的词,上面说到的编译器也可以理解为是有一个编译执行引擎,所以这个只是概念上的东西。
本地接口
为什么要有?
这其中是有历史原因吧,因为Java在问世的时候,C语言的程序是主流的,那么多程序是使用C语言,那么Java必然不可避免的要与C语言的程序进行交互,且Java是无法对操作系统底层进行访问和操作的,但是可以通过本地接口调用其他语言的实现实现对底层进行访问的操作。
是什么?
就是为了融合不同的变成语言的程序为Java语言的程序所用,所以在在内存中开辟了一块专门的处理标记是native的代码,。
目前这种方法的使用越来越少了,除非是直接和硬件交互的,因为现在基本通过Socket等通信方式实现程序直接的交互。
垃圾回收系统
为什么要有?
程序的运行的过程中,有一些是只运行一次或数次之后就不再运行了,而随着运行的时间增长,在系统中堆积的越来越多,最终超过系统的极限程序就停止了运行了。
站在用户的使用角度来看,用一下就不能用,或进行一些数据运行的时候就忽然不能用了,作者相信这个是没有用户是可以容忍的。
是什么?
将在一段时间不再使用,或在系统内部不再活跃的时候,则在系统中释放掉这部分的信息。
运行时数据区
指令区:是线程独有的
虚拟机栈:也可以叫栈内存,是在线程创建的时候创建,也就说明了,一个线程是有一个独有的栈,虚拟机栈的生命周期是随着线程的结束而释放内存,对于栈而言不存在垃圾回收的问题,只要线程结束,那么生命周期和线程是一致的,是栈会存储基本类型变量、实例方法、引用类型变量,都是在栈内存中分配,就是线程执行独有的方法的时候,会将方法区的对应的对象,类信息copy需要的部分信息到栈内存中,执行每一个方法的时候可以理解为是一个栈帧,具体看个人怎么去理解JVM栈内存,栈是遵循一个LIFO的一个原则,而栈一般是由三个部分组成,局部变量表,栈数据区,操作数栈。
局部变量表:存储报错的行数和方法使用到的局部变量等
操作数栈:保存计算过程中的结果,且作为计算过程中的变量临时存储空间。
栈数据区:除了局部变量和操作数栈,栈还需要一些数据来支持常量池的解析,这里的栈数据区就是保存常量池的指针、方法返回地址等,另外发送异常和处理异常代码等,所以栈数据区还有一个异常处理表。
栈执行过程:当一个线程在执行某个方法的时候,就是在栈内运行的,而栈是遵循LIFO的原则,那么就可以解释当A方法调用B方法的时候,只有B方法执行完了,那么A方法才会继续向下执行,那么在调用的时候是先调用A方法,那么A方法是先进栈,而B是后进栈的,B方法执行完了,弹出栈,继续A方法,A方法执行完了,那么则出栈。
本地方法栈:用于本地方法调用,允许java调用本地方法,具体可以看本地接口上述,本地接口如同一个大的存储每一个对象,而本地方法栈就是存储这个对象要执行的方法信息。
程序计数器/PC寄存器:指向方法区中的方法字节码(用于存储指向下一条指令的地址,也就是要指向的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,如果执行的方法是本地方法,寄存器值为undefined,如果不是那么寄存器会存放当前环境指针、程序计数器、操作栈指针、计算的变量指针等信息。
数据区:是所有线程共享的
方法区:保存类的元结构信息、运行时常量池、静态变量、常量、字节码、在类/实例/接口初始化用到的特殊方法等,方法区是可以调节大小的,且方法区是可以会进行垃圾回收的,所以可以理解方法区是一块逻辑区域。
堆:存储Java对象和数组等,但这个对象在堆中的首地址会在栈存储,堆内存的大小是可以调节的。
堆可以说是Java一块比较特殊的区域,因所有的Java对象实例都存储在这个地方,那么Java实例多了,则需要回收,那么也不可能每一次使用完这个实例之后就把这个实例在内存马上销毁,如果过多几秒时候又使用到了呢?
所以针对这种情况,Java堆的设计就有点特殊了。
堆 = 新生代 + 老年代 + 永久代(特殊)
新生代:主要存储一个新的对象,通过不同的垃圾回收策略进行计算什么时候进入老年代,什么时候进行回收,具体看垃圾回收机制。
TLAB(Thread-local allocation buffer)区:是一个线程独有的区域,
是为了加速内存分配而存在的,也就是线程独享的缓冲区,避免多线程问题,没有锁的问题,也就没有了锁对资源的开销,提高对象分配使用,但这个区域是只存储小对象的,无法进入TLAB区的对象会直接进入堆,而TLAB区对对象是否进入的条件是按照对象的大小是否小于整个空间的64/1,这是一个默认的比例值,这个比例值是可以调整的。
老年代:存储在新生代达到一定阈值(默认15),则进入老年代。
永久代:这是一个非常特殊的区域,这个是属于Hotspot这个虚拟机比较独有的,不同的虚拟机实现不一样,但如J9、JRockit是没有永久代的概念,永久代只是一个概念上的意义,永久代也会发生垃圾回收,但条件比较苛刻,稍后会在JDK6、7、8的区别中讲到,而永久代在JDK8时就给移除了,改为元空间。
直接内存/堆外内存:不属于JVM运行时数据区的,应用于NIO中,直接内存是跳过了Java堆,来提升内存的访问速度,其实就是使用通道和缓冲区的方式进行IO交互的方式,在操作的过程中如果没有设置这一块的堆空间大小,会引起OOM,可以通过-XX:MaxDirectMemonrySize进行设置,如果不配置默认为最大的堆空间大小。
注:直接内存也会触发GC的。
思考:Java为什么要这样设计JVM?为什么还要区分线程共享数据,和线程独有数据?
作者的思考思路,集中式有集中式的好,分布式有分布式的好,具体看其语言定位与发展。
Java运行过程
垃圾回收机制
为什么要进行垃圾回收?垃圾是什么?已经在上面讲了。
如何判定对象是可回收的?
如同这段代码,在方法内执行完了之后,这个test1对象在内存中则是为null了,因方法结束了,也没有其他引用了,在进行对象的引用查找时,则查找不到任何的引用,所以为null,那么则判定这个对象是不可达的,可以进行回收。
垃圾回收级别:作者将其分为三个级别,初级回收(minor GC),二级回收(major GC),完全回收(Full GC)。
初级回收(minor GC):当有新的对象要进入新生代的Eden区时, Eden区的空间不足以存放这个对象,则发生初级回收,而活跃的对象会先存放到新生代的sv区域,并记录年龄+1,当达到阈值(默认15)时就进入老年代。
二级回收(major GC):新生代对象达到阈值或新生代的eden区无法装入大对象时也会进入老年代,但老年代的空间不足以存放这个对象,则会二级回收。
完全回收(Full GC):就是当老年代的空间不足以存放新对象时或永久代的内存不足以存放内容时等。
那么完全回收和二级回收的区别在哪里?
JVM是有自动调节功能的,会根据程序在运行中进行调节的,所以何时触发完全回收,那么具体要看JVM的策略,但如果进行了完全回收之后还是出现空间不足以存放,那么则会出现OOM。
算法
主要说几个主流的垃圾回收算法的思想。
引用计数算法:计数器计算引用的次数,达到阈值就进入老年代,次数为0,则进行回收,对资源消耗严重,每次引用都要进行计算,但精确。
标记清除算法:分为标记和清除阶段,对标记的对象进行清除,清除后导致内存空间不连续,因而产生空间碎片。
对象何时标记清除?
就是一个树状结构,根节点向下查找是否可以查找到这个对象,查找不到的对象,则标记清除。
复制算法:将内存区域分为两块,假设现在在使用A区域,这时要进行垃圾回收,把A区域正在使用的对象复制到B区域去,清除A区域所有对象,反复的如此进行,完成垃圾收集。
复制算法:将内存区域分为两块,假设现在在使用A区域,这时要进行垃圾回收,把A区域正在使用的对象复制到B区域去,清除A区域所有对象,反复的如此进行,完成垃圾收集,主要用于新生代。
标记压缩算法:将存活的对象进行压缩,放到一个区域后,在进行垃圾回收,就是结合了标记清除算法和复制算法,主要用于老年代。
为什么复制算法和标记压缩算法主要的应用地方不一致?
新生代GC频繁,老年代对象大多数都是稳定的状态,对象多、耗时长。
分代算法:按照对应的策略将内存分为N块区域,根据策略的规定将不同的对象放入不同的区域,控制回收的空间,而不是每次都针对整个空间进行回收,减少GC停顿时间。
如:有个城市是这样规划的女孩子做针线活比较厉害,则把女孩子放到针线区,男孩子力气比较大则放到搬运区,小孩子喜欢玩,放到游乐区,游乐区的人慢慢多了,那么就只对游乐区进行人行疏导,而不需要整个城市都需要进行疏导,对游乐区进行疏导的时候也不会影响到别的区的运行。
分区算法:将内存分为N块独立空间,每次只控制回收多少空间,而不是每次都针对整个空间进行回收,减少GC停顿时间。
分代算法和分区算法区别:分代就是根据对象的特点进行划分,分区就是不管你是什么对象,控制每次回收多少个空间。
注:GC停顿就是把在进行垃圾回收的时候,会挂起正在运行的线程,使得其不在产生新的垃圾,回收完了之后才重新运行这些线程。
回收器
注:使用参数和设置线程数这些,读者请自己去找文档。
串行收集器:单线程进行垃圾回收,适用于并行能力不强的计算机(CPU),可以在新生代和老年代中使用,根据作用于不同的堆空间分为新生代串行回收器和老年代串行回收器。
Serial回收器:采用复制算法,在进行垃圾回收的时候其他线程会给挂起,直到垃圾回收完成(俗称:STW,全世界停止),开启后年轻代和老年代都采用这个回收器。
并行收集器:在串行的基础改为多线程并行进行垃圾回收,适用于并行能力强的计算机。
ParNew回收器:适用于新生代的垃圾回收器,只是进行简单的串行多线程化,回收策略和算法和串行是一样的。
ParallelGC回收器:适用于新生代,采用了复制算法的收集器,在进行垃圾回收的时候会进入STW,直到垃圾回收完成,ParallelGC是非常关注系统吞吐量。
ParallelOldGC回收器:适用于老年代,ParallelGC回收器一样,但采用标记压缩算法实现
CMS回收器:应用于老年代的多线程回收器,采用标记清除算法,主要关注系统停顿时间,是目前主流的回收器,CMS的整个回收过程分为,初始标记、并发标记、重新标记、并发清除四个步骤,在初始标记和重新标记时会进入STW,在并发标记和并发清除的过程中是不会进入STW的,而是应用程序可以不停的工作,但CMS在回收的过程中要保证内存有足够资源,CMS回收时机是达到阈值后,触发回收,老年代默认阈值是68%。
如果在CMS回收过程中,内存不足,那么则触发老年代的串行回收器,且CMS无法处理浮动垃圾(第一次告诉GC不使用,标记吧,留待第二次GC回收,第二次GC回收时告诉GC,我现在又要用了,但GC还是回收了。)
注:可以通过参数设置CMS回收多少次进行碎片整理和压缩。
G1回收器:采用了分区算法,独特的回收策略的多线程回收器,区分新生代和老年代依然有Eden、Sv0、Sv1区,不要求整个Eden区或新生代,老年代空间是连续的,G1的出现主要为了替代CMS,CMS采用标记清除导致出现空间碎片,对CPU资源的要求等,G1回收器是可以应用到新生代和老年代,但还是无法解决浮动垃圾等问题。
JVM与多线程
注:这里不谈论过多的多线程的内容,未来作者会单独对多线程进行撰写。
JVM与多线程-图一
JVM与多线程-图二
多线程为什么需要锁?
在上面的时候就解释了,JVM的数据区内的方法区和堆是线程共享的,在JVM与多线程-图一说明了方法区与堆是共享的,JVM与多线程-图二则说明了线程的栈是独有的,方法是在栈中运行的,当两个线程互相抢占CPU资源,会导致执行顺序不可控,促使执行结果是不可控的。
多线程锁:大体上线程并发常见的锁有,自旋锁、偏向锁、轻量锁、重量锁。
自旋锁:当前线程不会进入阻塞等待锁的状态,而是会通过循环的方式尝试获取到锁。
偏向锁:某个线程一直在执行某一段代码的时候,获取到锁一次,之后就默认是自动获取到锁了,是一种提高性能的方式。
轻量锁:当前线程还是处于偏向锁的状态,当有别的线程在访问时则会升级为轻量锁,其他线程可以通过自旋锁进行获取。
重量锁:A线程在处于轻量级锁时,B线程通过自旋的方式去尝试获取到锁,当达到自旋的阈值时还没有获取到锁,B线程则会进入阻塞状态,A线程的锁就变为重量锁。
JVM调试参数
Java8:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
以下展示的是Java7常用的:
指令描述
-XX:-AllowUserSignalHandlers如果应用程序安装了信号处理程序,请不要抱怨。(只适用于Solaris和Linux)。
-XX:AltStackSize=16384备用信号栈大小(以Kbytes表示)。(仅与Solaris相关,从5.0删除)。
-XX:-DisableExplicitGC在默认情况下,调用System.gc()是启用的(-XX:- disableitgc)。使用-XX:+ disableitgc来禁用对System.gc()的调用。注意,JVM仍然在必要时执行垃圾收集。
-XX:+FailOverToOldVerifier当新的类型检查失败时,故障转移到旧的验证器。(介绍6)。
-XX:+HandlePromotionFailure最年轻的一代收集不需要保证所有的活物体都能得到充分的推广。(在1.4.2更新中引入)[5.0和更早:false。]
-XX:+MaxFDLimit将文件描述符的数量增加到最大值。(Solaris。)
-XX:PreBlockSpin=10自旋计数变量使用-XX:+ usesping。在输入操作系统线程同步代码之前,控制最大的自旋迭代。(1.4.2中介绍)。
-XX:-RelaxAccessControlCheck在验证器中放松访问控制检查。(介绍6)。
-XX:+ScavengeBeforeFullGC在完整的GC之前进行年轻一代GC。(介绍1.4.1)。
-XX:+UseAltSigs使用替代信号代替SIGUSR1和SIGUSR2,用于VM内部信号。(在1.3.1更新中引入,1.4.1。与Solaris。)
-XX:+UseBoundThreads将用户级线程绑定到内核线程。(与Solaris。)
-XX:-UseConcMarkSweepGC为老一代人使用并发的标记-清除集合。1.4.1(介绍)
-XX:+UseGCOverheadLimit使用一个策略,在抛出OutOfMemory错误之前,限制在GC中使用的VM时间的比例。(介绍6)。
-XX:+UseLWPSynchronization使用基于lwp的而不是基于线程的同步。(介绍1.4.0。与Solaris。)
-XX:-UseParallelGC使用并行垃圾收集来清除垃圾。1.4.1(介绍)
-XX:-UseParallelOldGC为完整的集合使用并行垃圾收集。启用此选项将自动设置-XX:+UseParallelGC。(在5.0更新中引入)
-XX:-UseSerialGC使用串行垃圾收集。(5.0中引入的)。
-XX:-UseSpinning在进入操作系统线程同步代码之前,允许在Java监视器上进行简单的旋转。(只适用于1.4.2和5.0)[1.4.2,多处理器Windows平台:true]
-XX:+UseTLAB使用线程本地对象分配(在1.4.0中引入,在此之前被称为UseTLE)[1.4.2和更早的,x86或与-客户端:false]
-XX:+UseSplitVerifier使用具有StackMapTable属性的新类型检查器。(5.0中引入的。)(5.0:假)
-XX:+UseThreadPriorities使用本机线程优先级。
-XX:+UseVMInterruptibleIO在OS_INTRPT中,线程中断之前或与EINTR之间的I/O操作结果。(介绍了6。与Solaris。)
G1垃圾回收器参数
指令描述
-XX:+UseG1GC使用垃圾优先(G1)收集器。
-XX:MaxGCPauseMillis=n设置最大GC暂停时间的目标。这是一个软目标,JVM将尽最大努力实现它。
-XX:InitiatingHeapOccupancyPercent=n启动一个并发GC循环的(整个)堆占用率。它是由GCs使用的,它基于整个堆的占用而触发一个并发的GC循环,而不仅仅是一代(例如G1)。0的值表示“持续GC循环”。默认值是45。
-XX:NewRatio=n新旧一代的比例。默认值是2。
-XX:SurvivorRatio=n伊甸园/幸存者空间大小的比率。默认值是8。
-XX:MaxTenuringThreshold=n保持阈值的最大值。默认值是15。
-XX:ParallelGCThreads=n设置在垃圾收集器的并行阶段中使用的线程数。默认值随JVM运行的平台而异。
-XX:ConcGCThreads=n并发垃圾收集器将使用的线程数。默认值随JVM运行的平台而异。
-XX:G1ReservePercent=n设置保留为假上限的堆数量,以减少升级失败的可能性。默认值是10。
-XX:G1HeapRegionSize=n在G1中,Java堆被细分为一致大小的区域。这设置了每个子分区的大小。该参数的默认值是根据堆大小确定的。最小值为1Mb,最大值为32Mb。
性能参数
指令描述
-XX:+AggressiveOpts打开在即将发布的版本中默认为默认的点性能编译器优化。(在5.0更新中引入)
-XX:CompileThreshold=10000编译前的方法调用/分支数量[-客户端:1,500]
-XX:LargePageSizeInBytes=4m设置用于Java堆的大页面大小。(引入1.4.0更新1)[amd64: 2m]
-XX:MaxHeapFreeRatio=70在GC之后最大百分比的堆释放,以避免收缩。
-XX:MaxNewSize=size新生成的最大大小(以字节为单位)。自1.4以来,MaxNewSize被计算为NewRatio的函数。(1.3.1 Sparc:32 m;1.3.1 x86:2.5。]
-XX:MaxPermSize=64m永久世代的规模。[5.0和更新:64位虚拟机的比例增加了30%;1.4 amd64:96;1.3.1客户:32 m。)
-XX:MinHeapFreeRatio=40在GC后,堆的最小百分比以避免扩展。
-XX:NewRatio=2新旧一代的比例。[Sparc客户:8;x86 - server:8;x86客户:12。]-客户端:4 (1.3)8
(1.3.1+),x86: 12]
-XX:NewSize=2m新生成的默认大小(以字节为单位)[5.0和更新:64位虚拟机的比例增加了30%;x86:1米;x86, 5.0及以上:640k]
-XX:ReservedCodeCacheSize=32m保留代码缓存大小(以字节为单位)——最大的代码缓存大小。[Solaris 64位,amd64和-server x86: 2048m;在1.5.0_06和更早的版本中,Solaris 64位和amd64: 1024m。
-XX:SurvivorRatio=8eden/幸存者空间尺寸的比例[Solaris amd64: 6;Sparc在1.3.1:25;其他Solaris平台在5.0和更早:32]
-XX:TargetSurvivorRatio=50清除后使用的幸存者空间的期望百分比。
-XX:ThreadStackSize=512线程堆栈大小(以Kbytes表示)。(0表示使用默认栈大小)[Sparc: 512;Solaris x86: 320(在5.0和更早之前是256);Sparc 64位:1024;Linux amd64: 1024(5.0或更早时为0);所有其他0。)
-XX:+UseBiasedLocking使偏向锁。有关更多细节,请参见此调优示例。(在5.0更新中引入)[5.0:false]
-XX:+UseFastAccessorMethods使用得到<原始>字段的优化版本。
-XX:-UseISM使用的共享内存。不接受非solaris平台。)有关细节,请参见亲密共享内存。
-XX:+UseLargePages使用大页面内存。(在5.0更新中引入)有关详细信息,请参见Java对大内存页的支持。
-XX:+UseMPSS使用多个页面大小来支持堆的w/4mb页面。不要用“主义”来代替“主义”的需要。(在1.4.0版本中引入,与Solaris 9和更新版本相关)[1.4.1和更早:false]
-XX:+UseStringCache启用通常分配的字符串的缓存。
-XX:AllocatePrefetchLines=1使用JIT编译代码中生成的预取指令,在最后一个对象分配之后加载的缓存行数。如果最后一个分配的对象是一个实例,如果它是一个数组,默认值是1。
-XX:AllocatePrefetchStyle=1为预取指令生成的代码样式。
0 -无预取指令产生*d*,
1 -每次分配后执行预取指令,
2 -在执行预取指令时,使用TLAB分配水印指针到gate。
-XX:+UseCompressedStrings对可以表示为纯ASCII的字符串使用一个字节[]。(引入Java 6更新21性能版本)
-XX:+OptimizeStringConcat尽可能优化字符串连接操作。(Java 6更新20)
日志参数
指令描述
-XX:-CITime打印时间花在JIT编译器上。(介绍1.4.0)。
-XX:ErrorFile=./hs_err_pid<pid>.log如果发生错误,将错误数据保存到该文件。(介绍6)。
-XX:-ExtendedDTraceProbes启用performance-impacting
dtrace探测。(介绍了6。与Solaris。)
-XX:HeapDumpPath=./java_pid<pid>.hprof用于堆转储的目录或文件名路径。可控的。(1.4.2更新12,5.0更新7)
-XX:-HeapDumpOnOutOfMemoryError当java.lang时将堆转储到文件中。抛出OutOfMemoryError。可控的。(1.4.2更新12,5.0更新7)
-XX:OnError="<cmd
args>;<cmd args>"
在致命错误上运行用户定义的命令。(在1.4.2更新中介绍)
-XX:OnOutOfMemoryError=";
"
当第一次抛出OutOfMemoryError时,运行用户定义的命令。(介绍1.4.2更新12,6)
-XX:-PrintClassHistogram在Ctrl-Break上打印类实例的直方图。可控的。(1.4.2中介绍)。jmap
-histocommand提供了等价的功能。
-XX:-PrintConcurrentLocks打印java.util。在Ctrl-Break线程转储中并发锁。可控的。(介绍6)。jstack -lcommand提供了等价的功能
-XX:-PrintCommandLineFlags在命令行上出现的打印标志。(5.0中引入的)。
-XX:-PrintCompilation在编译方法时打印消息。
-XX:-PrintGC在垃圾收集中打印消息。可控的。
-XX:-PrintGCDetails在垃圾收集中打印更多的细节。可控的。(介绍1.4.0)。
-XX:-PrintGCTimeStamps在垃圾收集中打印时间戳。管理(介绍1.4.0)。
-XX:-PrintTenuringDistribution打印任期年龄信息。
-XX:-PrintAdaptiveSizePolicy允许打印关于自适应生成规模的信息。
-XX:-TraceClassLoading跟踪加载的类。
-XX:-TraceClassLoadingPreorder跟踪所有已加载的类(未加载)。(1.4.2中介绍)。
-XX:-TraceClassResolution跟踪常量池的决议。(1.4.2中介绍)。
-XX:-TraceClassUnloading跟踪卸货的类。
-XX:-TraceLoaderConstraints加载器约束的跟踪记录。(介绍6)。
-XX:+PerfDataSaveToFile在退出时保存jvmstat二进制数据。
-XX:ParallelGCThreads=n在年轻和旧的并行垃圾收集器中设置垃圾收集线程的数量。默认值随JVM运行的平台而异。
-XX:+UseCompressedOops允许使用压缩指针(对象引用表示为32位的偏移量,而不是64位指针)以优化64位性能,Java堆大小小于32gb。
-XX:+AlwaysPreTouch在JVM初始化期间预触摸Java堆。因此,堆的每一页都是在初始化过程中,而不是在应用程序执行期间递增的。
-XX:AllocatePrefetchDistance=n设置对象分配的预取距离。在这个距离(以字节为单位),在最后一个分配对象的地址之外,以新对象的值写入内存。每个Java线程都有自己的分配点。默认值随JVM运行的平台而异。
-XX:InlineSmallCode=n仅当生成的本机代码大小小于这个时,内联一个以前编译的方法。默认值随JVM运行的平台而异。
-XX:MaxInlineSize=35一个方法的最大字节码大小。
-XX:FreqInlineSize=n最大字节码大小的经常执行的方法被内联。默认值随JVM运行的平台而异。
-XX:LoopUnrollLimit=n使用服务器编译器中间表示节点的展开循环体的计数小于该值。服务器编译器使用的限制是这个值的函数,而不是实际值。默认值随JVM运行的平台而异。
-XX:InitialTenuringThreshold=7设置在并行的年轻收集器中用于自适应GC分级的初始阈值。招贴阈值是指一个物体在被提升到旧的或终身的一代之前,在年轻的集合中存活的次数。
-XX:MaxTenuringThreshold=n设置在自适应GC分级中使用的最大阈值。当前最大的值是15。并行收集器的默认值为15,CMS的默认值为4。
-Xloggc:<filename>日志GC详细输出到指定的文件。详细输出由正常的详细GC标志控制。
-XX:-UseGCLogFileRotation启用GC日志旋转,需要-Xloggc。
-XX:NumberOfGClogFiles=1设置旋转日志时要使用的文件数量,必须是>= 1。旋转的日志文件将使用以下命名方案,<filename>。0,<文件名>。1,…,<文件名> .n-1。
-XX:GCLogFileSize=8K日志文件的大小将会被旋转,必须是>= 8K。
JDK6、7、8的JVM区别
1.6
1.7
1.8
可以看到1.6到1.7可以说变化并不大,但到了1.8时,大家可以发现非常大了,出现了元空间的区域,并这个区域是在本地内存中的,且这个区域是存储类的元数据信息的,类的常量、方法等。
看起来元空间似乎和之前的方法区/永久代没有什么区别,元空间是使用本地内存的,受制于本地内存大小,在没有通过(MaxMetaspaceSize)VM参数设置时,会根据程序的运行时间动态调控大小。
那么也就不会再出现OOM的情况了,但元空间只是为了解决OOM的问题吗?
为什么要有元空间?
永久代主要是用于存储类的信息,但很难确定类的大小,所以在指定的时候就有点困难,容易造成OOM,另外一个原因就是Hotspot和JRockit的合并,JRockit是没有永久代的。
为什么Hotspot要和JRockit合并?
必然合并肯定是要实现互补的,JRockit的任务控制、垃圾收集算法、监控等能力都是比较优秀的,而Hotspot在性能优势也就使得其比较复杂,所以结合双方等各个优点进行合并,形成更强大的JVM。
另外Hotspot和JRockit都是Oracle旗下的。
元空间带来的影响?
有部分的数据移到堆,所以在1.8的时候会发现堆的空间会增加的比以往快,由于是使用本地内存,如果吞吐量大的时候,会带来大量的交换区交换。
元空间是否有垃圾回收?
当然也会有垃圾回收,不可能说应用程序不用这个类了,这个类失效了,还要一直保留着这些信息,这个是绝对不合理的。
元空间垃圾回收触发时机?
上面我们提到,元空间是存储类的元数据信息的,类加载器加载类的信息到元空间中,当这个类不在有引用时,这个类的信息就会给回收了。
调试JVM
为什么这里写的是调试,而不是优化。
免得误人子弟,优化这个问题,是要根据不同的应用程序进行优化,而调试也是一个比较大的话题吧,具体的调试的参数还是要根据应用程序而定。
这里分享下作者的思路:程序的定位(吞吐量等),程序的运行,位置定位。
1. 系统内存泄漏时会先定位是程序的哪里的代码导致的内存泄漏,如果是NIO导致的内存泄漏,则可能是堆外内存泄漏,如果递归循环则有可能导致的是栈,先定位到位置,之后在进行参数的调试,一步一步的确认位置。
2. 程序在运行的过程中,不断的越来越慢,而应用程序的吞吐量是比较大(这个时候我们还是要先定位到位置,如果是产生大量的对象,而这些对象的使用次数也不多,当有相当一部分在很多时候达到了进入老年代的条件,从而进入了老年代,但进入老年代后呈现就不在使用这个对象,我们都知道老年代的对象比较稳定,回收的不多,那么处理的时间长,所以对老年代的回收时间会比较久),那么可以通过调整进入老年代的条件,尽量使得对象在新生代时就给回收了,并减少GC次数。
注:JDK7版本后(包含),部分JVM参数已经是拥有自动化调整的能力,如TLAB区域,除非是对系统等各个方面熟悉,否则建议不要乱调参数。
可以写一个递归方法造成内存泄漏在程序启动时配置导出dump文件参数,可以使用Eclipse的MAT插件查看dump文件,或jvisualvm等工具查看。
为什么要学JVM?
学习了JVM后,我们来看个问题,为什么学JVM?
以下只是作者的个人观点:
工作:JVM是作为一名Java程序员所必备了解的过程,但随着工作的年限的增长,可能接触到的项目越来越多,而项目本身业务的复杂性可能会出现一次性加载的东西太多,导致内存出现泄漏,而我们当我们没有去了解JVM的时候,会认为是硬件问题,或许上百度查一下知道是内存泄漏,要把堆调大,但如果是堆外内存泄漏呢?那么当应用程序吞吐量大的时候,是否可以通过调整进入老年代的条件而利用好内存空间呢?
面试:现在很多公司在面试的时候都会问关于JVM的内容。