Java JVM 复习

一、JVM

1.1 运行时数据区组成

●程序计数器
当前线程所执行字节码的行号指示器。
●虚拟机栈(stack)
1、线程私有;
2、描述的是java方法执行的内存模型;
3、每个方法被执行时会同时创建一个栈帧用来存储局部变量表、操作栈、动态链接、方法出口等信息;
4、局部变量表所需的内存空间在编译期完成分配。
5、存储速度快,缺点是栈中数据大小和生存期限是固定的。
●本地方法栈
与虚拟机栈类似,虚拟机栈为虚拟机执行java方法(字节码)服务,本地方法栈是为虚拟机使用到的native方法服务。
●java 堆
1、线程间共享;
2、唯一目的就是存放对象实例;
3、堆是垃圾回收器管理的主要区域;
4、Java堆中可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。
5、年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
6、在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
7、Eden和Survivor的大小比例是8:1。
●方法区(non heap 非堆)
1、线程共享;
2、用于存储被虚拟机加载的类信息、常量、静态变量等。
3、如果把JVM内存分为堆和非堆的话,堆就是Java代码可及的内存,留给开发人员使用;非堆是JVM留给自己使用的。
Java7 之前,类的静态变量(简称类变量,比如你写的 staff)存放在 永久代(PermGen)—— 在 Hotspot JVM 上,PermGen 就是方法区;Java7 之后,将类变量的存储转移到了 堆。
●运行时常量
1、常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。
2、常量池,实际上分为两种形态:静态常量池和运行时常量池。
3、所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。
4、而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。
运行时常量池(Runtime Constant Pool)是方法区的一部分。
●直接内存
1、直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
2、JDK1.4新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

1.2 操作系统中的堆和栈

堆栈是两种数据结构。堆栈都是一种数据项按序排列的数据结构,只能在一端(称为栈顶(top))对数据项进行插入和删除。在单片机应用中,堆栈是个特殊的存储区,主要功能是暂时存放数据和地址,通常用来保护断点和现场。

要点:
堆,队列优先,先进先出(FIFO—first in first out)。
栈,先进后出(FILO—First-In/Last-Out)。

1.2.1 堆栈空间分配区别:

 1、栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈; 
 2、堆(操作系统):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。

1.2.2 堆栈缓存方式区别:

 1、栈使用的是一级缓存,他们通常都是被调用时处于存储空间中,调用完毕立即释放; 
 2、堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

1.2.3 堆栈数据结构区别:

 堆(数据结构):堆可以被看成是一棵树,如:堆排序; 
 栈(数据结构):一种先进后出的数据结构。

1.3 Java中的堆和栈

在JVM中,内存分为两个部分,Stack(栈)和Heap(堆)。
JVM是基于堆栈的虚拟机.JVM为每个新创建的线程都分配一个堆栈.堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。
我们知道,某个线程正在执行的方法称为此线程的当前方法.我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧。这个帧自然成为了当前帧.在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据。

Stack(栈)是JVM的内存指令区。Stack管理很简单,push一 定长度字节的数据或者指令,Stack指针压栈相应的字节位移;pop一定字节长度数据或者指令,Stack指针弹栈。
Heap(堆)是JVM的内存数据区。Heap 的管理很复杂,每次分配不定长的内存空间,专门用来保存对象的实例。

1)方法本身是指令的操作码部分,保存在Stack中; 
2)方法内部变量作为指令的操作数部分,跟在指令的操作码之后,保存在Stack中(实际上是简单类型保存在Stack中,对象类型在Stack中保存地址,在Heap 中保存值);上述的指令操作码和指令操作数构成了完整的Java 指令。 
3)对象实例包括其属性值作为数据,保存在数据区Heap 中。 
非静态的对象属性作为对象实例的一部分保存在Heap 中,而对象实例必须通过Stack中保存的地址指针才能访问到。因此能否访问到对象实例以及它的非静态属性值完全取决于能否获得对象实例在Stack中的地址指针。

非静态方法和静态方法的区别
当一个class文件被ClassLoader load进入JVM后,方法指令保存在Stack中,此时Heap 区没有数据。然后程序技术器开始执行指令,如果是静态方法,直接依次执行指令代码,当然此时指令代码是不能访问Heap 数据区的;如果是非静态方法,由于隐含参数没有值,会报错。因此在非静态方法执行前,要先new对象,在Heap 中分配数据,并把Stack中的地址指针交给非静态方法,这样程序技术器依次执行指令,而指令代码此时能够访问到Heap 数据区了。 
静态属性和动态属性
在JVM中,静态属性保存在Stack指令内存区,动态属性保存在Heap数据内存区。

1.4 Java对象的大小

在Java中,一个空Object对象的大小是8byte,这个大小只是保存堆中一个没有任何属性的对象的大小。看下面语句:

Object ob = new Object();

这样在程序中完成了一个Java对象的生命,但是它所占的空间为:4byte+8byte。4byte是上面部分所说的Java栈中保存引用的所需要的空间。而那8byte则是Java堆中对象的信息。因为所有的Java非基本类型的对象都需要默认继承Object对象,因此不论什么样的Java对象,其大小都必须是大于8byte。
有了Object对象的大小,我们就可以计算其他对象的大小了。

1.5 垃圾回收器

1.5.1 对象存活的判断

●很多判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器+1,当引用时效,计数器-1;任何时刻计数器都为0的章台就是不可能再被使用。
Java语言中没有选用计数算法来管理内存,最主要的原因是它很难解决对象之间的相互循环引用的问题。
●Java使用“根搜索算法(可达性算法)”判断对象是否存活。GC Roots Tracing,使用GC roots对象作为起点,从这些节点开始向下搜索,搜索引用链。
当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的
在Java语言里,可作为GC Roots对象的包括如下几种:
a.虚拟机栈(栈桢中的本地变量表)中的引用的对象
b.方法区中的类静态属性引用的对象
c.方法区中的常量引用的对象
d.本地方法栈中JNI的引用的对象 
●强引用、软引用、弱引用、虚引用
●判断是否需要执行finnalize()方法是那个垃圾回收的标志

1.5.2 垃圾回收算法(4种)

●标记-清除算法
1、最基础的算法;
2、分为“标记”和“清除”2阶段;
3、问题是效率低、空间产生碎片;
●复制算法
1、它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
2、现代商业虚拟机都使用这种算法来回收新生代。
3、缺点:对象存活较高时,需要大量的复制操作。
●标记-整理算法
解决复制算法的效率问题。针对老年代的特点,使用“标记-整理”算法,和“标记-清除”差不多,但是后续不直接清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
●分代收集算法
当前商业虚拟机的垃圾收集都采用”分代收集“(Generational Collection)算法。
 

1.5.3 垃圾收集器(5种)

收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。
●Serial 收集器(复制算法)
1、这个收集器是一个单线程的收集器,采用复制算法;
2、它进行垃圾收集时,必须暂停其他所有的工作线程(Sun将这件事情称之为”Stop The World“),直到它收集结束;
3、Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择;
4、Serail Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。
●ParNew收集器(复制算法)
1、ParNew收集器其实就是Serial收集器的多线程版本;
2、ParNew是许多运行在Server模式下的虚拟机中首选的新生代收集器;
3、ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果。
●Parallel Scavenge收集器(JVM新生代默认,复制算法)
1、Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
2、CMS等收集器的关注点尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量;
3、Parallel Old 是Prallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。(JVM老年代默认)
●CMS收集器(标记-清除)
1、CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,用于老年代,并发收集器。
2、CMS是基于“标记-清除”算法实现的。
缺点:
1、对CPU资源很敏感
2、产生碎片。
●G1收集器(标记-整理)
G1收集器,Java堆的内存布局是将整个Java堆分为多个大小相等的独立区域(Region),也保留了新生代 
和老年代的概念。但是新生代和老年代不再是物理隔离的,它们都是一部分Region的集合。G1跟踪各个Region里面的垃圾堆积的价值大小(也就是回收获得的空间大小以及回收需要的时间的经验值),在后台维护一个优先列表,每次根据允许的的收集时间,优先回收价值最大的Region。
1、它是基于“标记-整理”算法实现的收集器,就是说它不会产生空间碎片;
2、它可以非常精准的控制停顿;

jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1

1.5.4 内存分配与回收

●内存管理:给对象分配内存+回收分配的内存
●对象优先分配在Eden,有部分会移入到from Survivor。
设置两个Survivor区最大的好处就是解决了碎片化,保持存储的连续性
●大对象直接分配到老年代
1、所谓大对象就是指,需要大量连续内存空间的Java对象;
●长期存活的对象将进入老年代
●虚拟机并不总是要求对象的年龄必须到达MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
●空间分配担保(full gc就是发现进入老年代的大小大于剩余空间)
在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败:如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。
当执行新生代GC时,如果发现晋升老年代的大小高于老年代的剩余空间,就会执行Full GC。

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。
 

1.5.5 性能监控工具

●jps
用来查看基于HotSpot JVM里面所有进程的具体状态, 包括进程ID,进程启动的路径等等。
●jstat
查看classloader,compiler,gc相关信息,实时监控资源和性能 。jstat工具特别强大,可以用来监视VM内存内的各种堆和非堆的大小及其内存使用量。
●jinfo
●jmap
打印java进程的堆内存信息。
●jstack
jstack用于生成java虚拟机当前时刻的线程快照,主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。
●jconsole
jconsole可以监控Java应用程序(如jar应用、tomcat等),但被监视的应用程序必须和jconsole是用同一个用户运行的。jvisualvm的使用和jconsole类似。

1.6 调优参数

1.6.1 堆大小设置

JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。
典型设置

1、java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM初始内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
2、java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxPermSize=16m:设置持久代大小为16m(主要用来存放类对象的meta-data,使用永久代来实现方法区)。
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。

1.6.2 堆内存分配

JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的堆内存由-Xmx指定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;
空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、-Xmx 相等以避免在每次GC 后调整堆的大小。
说明:如果-Xmx 不指定或者指定偏小,应用可能会导致java.lang.OutOfMemory错误,此错误来自JVM,不是Throwable的,无法用try...catch捕捉。 

1.6.3 非堆内存分配

 JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。(还有一说:MaxPermSize缺省值和-server -client选项相关,
 -server选项下默认MaxPermSize为64m,-client选项下默认MaxPermSize为32m。这个我没有实验。)
 上面错误信息中的PermGen space的全称是Permanent Generation space,是指内存的永久保存区域。还没有弄明白PermGen space是属于非堆内存,还是就是非堆内存,但至少是属于了。
XX:MaxPermSize设置过小会导致java.lang.OutOfMemoryError: PermGen space 就是内存益出。 
说说为什么会内存益出: 
(1)这一部分内存用于存放Class和Meta的信息,Class在被 Load的时候被放入PermGen space区域,它和存放Instance的Heap区域不同。 
(2)GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的APP会LOAD很多CLASS 的话,就很可能出现PermGen space错误。
  这种错误常见在web服务器对JSP进行pre compile的时候。  
 

1.7 回收器

JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。

吞吐量优先的并行收集器(Parallel Scavenge)

如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
典型配置

1、java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
2、java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
3、java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
4、java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

1.7.1 响应时间优先的并发收集器(CMS)

如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。

典型配置:


1、java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。
2、java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片

1.7.2 Garbage Firest(G1)

分代垃圾回收方式确实也考虑了实时性要求而提供了并发回收器,支持最大暂停时间的设置,但是受限于分代垃圾回收的内存划分模型,其效果也不是很理想。
为了达到实时性的要求(其实Java语言最初的设计也是在嵌入式系统上的),一种新垃圾回收方式呼之欲出,它既支持短的暂停时间,又支持大的内存空间分配。可以很好的解决传统分代方式带来的问题。

增量收集的方式在理论上可以解决传统分代方式带来的问题。增量收集把对堆空间划分成一系列内存块,使用时,先使用其中一部分(不会全部用完),垃圾收集时把之前用掉的部分中的存活对象再放到后面没有用的空间中,这样可以实现一直边使用边收集的效果,避免了传统分代方式整个使用完了再暂停的回收的情况。

当然,传统分代收集方式也提供了并发收集,但是他有一个很致命的地方,就是把整个堆做为一个内存块,这样一方面会造成碎片(无法压缩),另一方面他的每次收集都是对整个堆的收集,无法进行选择,在暂停时间的控制上还是很弱。而增量方式,通过内存空间的分块,恰恰可以解决上面问题。

支持很大的堆
高吞吐量
      --支持多CPU和垃圾回收线程
      --在主线程暂停的情况下,使用并行收集
      --在主线程运行的情况下,使用并发收集
实时目标:可配置在N毫秒内最多只占用M毫秒的时间进行垃圾回收
当然G1要达到实时性的要求,相对传统的分代回收算法,在性能上会有一些损失。

G1可谓博采众家之长,力求到达一种完美。他吸取了增量收集优点,把整个堆划分为一个一个等大小的区域(region)。内存的回收和划分都以region为单位;同时,他也吸取了CMS的特点,把这个垃圾回收过程分为几个阶段,分散一个垃圾回收过程;而且,G1也认同分代垃圾回收的思想,认为不同对象的生命周期不同,可以采取不同收集方式,因此,它也支持分代的垃圾回收。为了达到对回收时间的可预计性,G1在扫描了region以后,对其中的活跃对象的大小进行排序,首先会收集那些活跃对象小的region,以便快速回收空间(要复制的活跃对象少了),因为活跃对象小,里面可以认为多数都是垃圾,所以这种方式被称为Garbage First(G1)的垃圾回收算法,即:垃圾优先的回收。

回收步骤:
1、初始标记(Initial Marking)
    G1对于每个region都保存了两个标识用的bitmap,一个为previous marking bitmap,一个为next marking bitmap,bitmap中包含了一个bit的地址信息来指向对象的起始点。
    开始Initial Marking之前,首先并发的清空next marking bitmap,然后停止所有应用线程,并扫描标识出每个region中root可直接访问到的对象,将region中top的值放入next top at mark start(TAMS)中,之后恢复所有应用线程。
2、并发标记(Concurrent Marking)
    按照之前Initial Marking扫描到的对象进行遍历,以识别这些对象的下层对象的活跃状态,对于在此期间应用线程并发修改的对象的以来关系则记录到remembered set logs中,新创建的对象则放入比top值更高的地址区间中,这些新创建的对象默认状态即为活跃的,同时修改top值。
3、最终标记暂停(Final Marking Pause)
    当应用线程的remembered set logs未满时,是不会放入filled RS buffers中的,在这样的情况下,这些remebered set logs中记录的card的修改就会被更新了,因此需要这一步,这一步要做的就是把应用线程中存在的remembered set logs的内容进行处理,并相应的修改remembered sets,这一步需要暂停应用,并行的运行。
4、存活对象计算及清除(Live Data Counting and Cleanup)
    值得注意的是,在G1中,并不是说Final Marking Pause执行完了,就肯定执行Cleanup这步的,由于这步需要暂停应用,G1为了能够达到准实时的要求,需要根据用户指定的最大的GC造成的暂停时间来合理的规划什么时候执行Cleanup。
 

1.7.3 辅助信息

JVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:

-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用
-XX:PrintHeapAtGC:打印GC前后的详细堆栈信息
-Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。

1.8 类文件(编译)

●计算机只能识别0和1的二进制格式,虚拟机的出现,我们的程序编译成二级制本地机器码(native code)不再是唯一选择,现在更多程序选择了与操作系统和机器指令无关的格式作为编译后的存储格式。
●各种不同平台的虚拟机与所有平台都统一使用的程序存储格式–字节码(ByteCode)是构成平台无关性的基石。
●Java语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的。

1.8.1 class文件

●Class文件是一组8位字节为基础单位的二进制流。
●每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。

1.9 虚拟机类加载机制(整套流程)

java代码的三个过程:Java编译,类加载、类执行
●在Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能被运行和使用。虚拟机把描述类的数据从Class文件加载到内存,并对数据继续进行校验、准备、解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
●Java字节代码的表现形式是字节数组(byte[]),而Java类在JVM中的表现形式是java.lang.Class类 的对象。
●一个Java类从字节代码到能够在JVM中被使用,需要经过加载、链接和初始化这三个步骤。
●加载--链接(验证/准备/解析)--初始化--使用--卸载,5个步骤。
●类内容的加载顺序,如果类还没有被加载: 
1、先执行父类的静态代码块和静态变量初始化,并且静态代码块和静态变量的执行顺序只跟代码中出现的顺序有关。 
2、执行子类的静态代码块和静态变量初始化。 
3、执行父类的实例变量初始化 
4、执行父类的构造函数 
5、执行子类的实例变量初始化 
6、执行子类的构造函数 
如果类已经被加载: 则静态代码块和静态变量就不用重复执行,再创建类对象时,只执行与实例相关的变量初始化和构造方法。
●站在Java虚拟机的角度讲,只存在两种不同的类加载器:
1、一种是启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分;
2、另外一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。
●从Java开发人员角度看,类加载器还可以划分得再细致一些:
1、启动类加载器(Bootstrap ClassLoader);
2、扩展类加载器(Extension ClassLoader);
3、应用程序类加载器(Application ClassLoader);
●双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载都是如此,因此所有加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索方位汇总没有找到所需的类)时,子加载器才会尝试自己去加载。
先检查是否已经被加载过,若没有加载则调用父加载器的loadClass方法, 如父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException 异常后,再调用自己的findClass方法进行加载。
●如果没有使用双亲委派模型,由个各类加载器自行去加载的话,如果用户自己写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为就无法保证。可以尝试写一个与rt.jar类库中已有类名重名的Java类,将会返现可以正常编译,但永远无法被加载运行。
 

1.10 破坏双亲委派

双亲委任模型不是一个强制性的约束模型,而是一个建议型的类加载器实现方式。在Java的世界中大部分的类加载器都遵循者模型,但也有例外,到目前为止,双亲委派模型有过3次大规模的“被破坏”的情况。 
第一次:在双亲委派模型出现之前—–即JDK1.2发布之前。 
第二次:是这个模型自身的缺陷导致的。我们说,双亲委派模型很好的解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API, 但没有绝对,如果基础类调用会用户的代码怎么办呢?
第三次:为了实现热插拔,热部署,模块化,意思是添加一个功能或减去一个功能不用重启,只需要把这模块连同类加载器一起换掉就实现了代码的热替换。

Tomcat的类加载器就破坏了双亲委派,为了解决多实例的隔离性问题。WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。

打破双亲委派机制则不仅要继承ClassLoader类,还要重写loadClass和findClass方法。
 

1.11 字节码加载和卸载面试

加载:
1、加载包括3部分:
<1>通过类的全限定名获取定义此类的二进制字节流。 
<2>将字节流的存储结构转化为方法区的运行时结构。 
<3>在内存中生成一个代表该类的Class对象,作为方法区各种数据的访问入口。
执行流程:加载->连接(验证->准备->解析)->初始化->使用->卸载
2、由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。
3、由用户自定义的类加载器加载的类是可以被卸载的
1、该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
2、加载该类的ClassLoader已经被GC。
3、该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法
虽然满足以上三个条件Class可以被卸载,但是GC的时机我们是不可控的,那么同样的我们对于Class的卸载也是不可控的。
 

二、OOM分析

Java应用程序在启动时会指定所需要的内存大小,它被分割成两个不同的区域:Heap space(堆空间)和Permgen(永久代)。

2.1 StackOverflowError

当一个函数被Java程序调用的时候,就会在调用栈上分配栈帧。栈帧包含被调用函数的参数、局部变量和返回地址。返回地址指示了当函数执行完毕之后下一步该执行哪里。如果创建栈帧时没有内存空间,JVM就会抛出StackOverflowError。
最常见的耗尽Java栈的案例是递归。在递归操作中,函数执行时会调用自己。

如何处理StackOverflowError
最简单的方法就是细致的检查stack trace,找出行号的重复模式。这些重复的行号代表了被递归调用的代码。仔细审查代码,理解为何递归不终止。
如果你确认递归实现没有问题,你可以通过-Xss参数增加栈的大小,这个参数可以在项目配置或命令行指定。

2.2 java.lang.OutOfMemoryError:Java heap space

java中堆空间是JVM管理的最大一块内存空间,可以在JVM启动时指定堆空间的大小,其中堆被划分成两个不同的区域:新生代(Young)和老年代(Tenured),新生代又被划分为3个区域:Eden、From Survivor、To Survivor

堆内存溢出,需要注意的是:即使有足够的物理内存可用,只要达到堆空间设置的大小限制,此异常仍然会被触发。

原因:
1/流量/数据量峰值
2/内存泄漏,造成被能被回收的对象堆积

区域的大小可以在JVM(Java虚拟机)启动时通过参数-Xmx设置,如果你没有显式设置,则将使用特定平台的默认值。

2.3 java.lang.OutOfMemoryError:GC overhead limit exceeded

Java运行时环境(JRE)包含一个内置的垃圾回收进程,默认情况下,当应用程序花费超过98%的时间用来做GC并且回收了不到2%的堆内存时,会抛出java.lang.OutOfMemoryError:GC overhead limit exceeded错误。具体的表现就是你的应用几乎耗尽所有可用内存,并且GC多次均未能清理干净。

如果你的应用程序确实内存不足,增加堆内存会解决GC overhead limit。

但如果你想确保你已经解决了潜在的问题,而不是掩盖java.lang.OutOfMemoryError: GC overhead limit exceeded错误,那么你不应该仅止步于此。你要记得还有profilers和memory dump analyzers这些工具。

2.4 java.lang.OutOfMemoryError:Permgen space

持久代主要存储的是每个类的信息,比如:类加载器引用、运行时常量池(所有常量、字段引用、方法引用、属性)、字段(Field)数据、方法(Method)数据、方法代码、方法字节码等等。我们可以推断出,PermGen的大小取决于被加载类的数量以及类的大小。

因此,我们可以得出出现java.lang.OutOfMemoryError: PermGen space错误的原因是:太多的类或者太大的类被加载到permanent generation(持久代)。

区域的大小可以在JVM(Java虚拟机)启动时通过参数-XX:MaxPermSize设置,如果你没有显式设置,则将使用特定平台的默认值。

解决运行时OutOfMemoryError,首先你需要检查是否允许GC从PermGen卸载类,JVM的标准配置相当保守,只要类一创建,即使已经没有实例引用它们,其仍将保留在内存中,特别是当应用程序需要动态创建大量的类但其生命周期并不长时,允许JVM卸载类对应用大有助益,你可以通过在启动脚本中添加以下配置参数来实现:
-XX:+CMSClassUnloadingEnabled

2.5 java.lang.OutOfMemoryError:Metaspace

持久代中包含了虚拟机中所有可通过反射获取到的数据,比如Class和Method对象。不同的Java虚拟机之间可能会进行类共享,因此持久代又分为只读区和读写区。
JVM用于描述应用程序中用到的类和方法的元数据也存储在持久代中。JVM运行时会用到多少持久代的空间取决于应用程序用到了多少类。除此之外,Java SE库中的类和方法也都存储在这里。
如果JVM发现有的类已经不再需要了,它会去回收(卸载)这些类,将它们的空间释放出来给其它类使用。Full GC会进行持久代的回收。

PermGen区域用于存储类的名称和字段,类的方法,方法的字节码,常量池,JIT优化等,但从Java8开始,Java中的内存模型发生了重大变化:引入了称为Metaspace的新内存区域,而删除了PermGen区域。请注意:不是简单的将PermGen区所存储的内容直接移到Metaspace区,PermGen区中的某些部分,已经移动到了普通堆里面(方法区移至Metaspace,字符串常量移至Java Heap)。

Java8做出如此改变的原因包括但不限于:
●应用程序所需要的PermGen区大小很难预测,设置太小会触发PermGen OutOfMemoryError错误,过度设置导致资源浪费。
●提升GC性能,在HotSpot中的每个垃圾收集器需要专门的代码来处理存储在PermGen中的类的元数据信息。从PermGen分离类的元数据信息到Metaspace,由于Metaspace的分配具有和Java Heap相同的地址空间,因此Metaspace和Java Heap可以无缝的管理。
●而且简化了FullGC的过程,以至将来可以并行的对元数据信息进行垃圾收集,而没有GC暂停。


元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小: 
 

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。 
  -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。 
  除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性: 
  -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集 
  -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
  -verbose参数是为了获取类型加载和卸载的信息

2.6 java.lang.OutOfMemoryError:Unable to create new native thread

出现java.lang.OutOfMemoryError:Unable to create new native thread就意味着Java应用程序已达到其可以启动线程数量的极限了。

// macOS 10.12上执行
$ ulimit -u
709

2.7 java.lang.OutOfMemoryError:Out of swap space?

Java应用程序在启动时会指定所需要的内存大小,可以通过-Xmx和其他类似的启动参数来指定。在JVM请求的总内存大于可用物理内存的情况下,操作系统会将内存中的数据交换到磁盘上去。

Out of swap space?表示交换空间也将耗尽,并且由于缺少物理内存和交换空间,再次尝试分配内存也将失败。

这个问题往往发生在Java进程已经开始交换的情况下,现代的GC算法已经做得足够好了,当时当面临由于交换引起的延迟问题时,GC暂停的时间往往会让大多数应用程序不能容忍。
java.lang.OutOfMemoryError:Out of swap space?往往是由操作系统级别的问题引起的,例如:
操作系统配置的交换空间不足。
系统上的另一个进程消耗所有内存资源。

解决这个问题有几个办法,通常最简单的方法就是增加交换空间,不同平台实现的方式会有所不同。

2.8 java.lang.OutOfMemoryError:Requested array size exceeds VM limit

Java对应用程序可以分配的最大数组大小有限制。不同平台限制有所不同,但通常在1到21亿个元素之间。

2.9 Out of memory:Kill process or sacrifice child

操作系统是建立在进程的概念之上,这些进程在内核中作业,其中有一个非常特殊的进程,名叫“内存杀手(Out of memory killer)”。当内核检测到系统内存不足时,OOM killer被激活,然后选择一个进程杀掉。哪一个进程这么倒霉呢?选择的算法和想法都很朴实:谁占用内存最多,谁就被干掉。

三、Java内存模型

3.1 线程安全与锁

●在Java里面,最基本的互斥同步手段就是synchronized关键字,synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。
●根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁。如果这个对象没被锁定或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将计数器减1,当计数器为0时,锁就被释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。
●首先,synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。其次,同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
●还可以使用java.util.concurrent包中的重入锁(ReentrantLock)来实现同步.
●ReentrantLock表现为API层面的互斥锁(lock()和unlock方法配合try/catch语句块来完成),synchronized表现为原生语法层面的互斥锁。
●ReentrantLock比synchronized增加了一些高级功能,主要有以下三项:等待可中断、可实现公平锁(公平锁:加锁前先查看是否有排队等待的线程,有的话优先处理排在前面的线程,先来先得),以及锁可以绑定多个条件。
●非阻塞同步,CAS指令,分别是内存位置(java中简单理解为内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。
●如J.U.C包里整数原子类,其中的compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作。
●为了让线程等待,我们就只需让线程执行一个忙循环(自旋),这就是所谓的自旋锁。

3.2 并发模型

●CPU只与寄存器中进行存取,于是 CPU<—>寄存器<----->内存 这就是它们之间的信息交换。
●寄存器与内存之间设置一个缓存
●内存模型就是描述多处理器之间内存可见性的描述。

3.3Java内存模型

●Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。
●Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
●Java内存模型规定了所有变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存(Working Memory,可与前面讲的处理器告诉缓存类比,线程的working memory只是cpu的寄存器和高速缓存的抽象描述,JVM是一个虚拟 的计算机),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。
●指令序列的重排序
1、编译器优化的重排序;
2、指令级并行的重排序;
3、内存系统的重排序。

3.4 volatile底层原理

3.4.1 基础

volatile的定义:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。

通俗点讲就是说一个变量如果用volatile修饰了,则Java可以确保所有线程看到这个变量的值是一致的,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新,这就是所谓的线程可见性。

主存---CPU高速缓存---CPU
解决缓存一致性的方案:
1、通过在总线加LOCK#锁方式;(CPU独占,其他CPU阻塞)
2、通过缓存一致性协议(MESI协议I(Modified Exclusive Shared Or Invalid))(确保每个缓存中使用的共享变量的副本是一致的):
当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。

在并发编程中我们一般都会遇到这三个基本概念:原子性、可见性、有序性。
原子性:
Java只保证了基本数据类型的变量和赋值操作才是原子性的(注:在32位的JDK环境下,对64位数据的读取不是原子性操作*,如long、double)要想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。
volatile是无法保证复合操作的原子性

可见性:
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
java提供了volatile来保证可见性。
当然,synchronize和锁都可以保证可见性。

有序性:
即程序执行的顺序按照代码的先后顺序执行。
在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序它不会影响单线程的运行结果,但是对多线程会有影响。
Java提供volatile来保证一定的有序性。

3.4.2 原理

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性

在JVM底层volatile是采用“内存屏障”来实现的(多核处理器上通常需要使用内存屏障指令来确保这种一致性)。
内存屏障指令仅仅直接控制CPU与其缓存之间,CPU与其准备将数据写入主存或者写入等待读取、预测指令执行的缓冲中的写缓冲之间的相互操作。这些操作可能导致缓冲、主内存和其他处理器做进一步的交互。
常见的处理器内存模型比JMM更弱,Java编译器在生成字节码时,会在执行指令序列的适当位置插入内存屏障来限制处理器的重排序。

volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令
lock前缀指令其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。volatile的底层就是通过内存屏障来实现的。

JMM基于保守策略的JMM内存屏障插入策略:
1.在每个volatile写操作的前面插入一个StoreStore屏障
2.在每个volatile写操作的后面插入一个SotreLoad屏障
3.在每个volatile读操作的前面插入一个LoadLoad屏障
4.在每个volatile读操作的后面插入一个LoadStore屏障

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2)它会强制将对缓存的修改操作立即写入主存;
  3)如果是写操作,它会导致其他CPU中对应的缓存行无效。 

   

两层语义;
1、保证可见性、不保证原子性
2、有序性,禁止指令重排序(对volatile变量的写操作 happen-before 后续的读操作。)

指令重排序:
在执行程序时为了提高性能,编译器和处理器通常会对指令做重排序:
指令重排序对单线程没有什么影响,他不会影响程序的运行结果,但是会影响多线程的正确性。
●编译器重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
●处理器重排序。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;

happen-before原则:
happen-before原则保证了程序的“有序性”,它规定如果两个操作的执行顺序无法从happens-before原则中推到出来,那么他们就不能保证有序性。

3.5 字符串常量池

1.intern()函数
intern函数的作用是将对应的符号常量进入特殊处理,在1.6以前 和 1.7以后有不同的处理;
在1.6中,intern的处理是 先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量,如果没有找到,则将该字符串常量加入到字符串常量区,也就是在字符串常量区建立该常量;
在1.7中,intern的处理是 先判断字符串常量是否在字符串常量池中,如果存在直接返回该常量,如果没有找到,说明该字符串常量在堆中;(调用该方法的字符串对象要么在堆区要么在常量池中的)

只有使用intern()在1.6和1.7中不同。

2.常量池的分类
(1)class文件常量池
在Class文件中除了有类的版本【高版本可以加载低版本】、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table)【此时没有加载进内存,也就是在文件中】,用于存放编译期生成的各种字面量和符号引用。

下面对字面量和符号引用进行说明:
字面量类似与我们平常说的常量,主要包括:
-文本字符串:就是我们在代码中能够看到的字符串,例如String a = “aa”。其中”aa”就是字面量。
-被final修饰的变量。
符号引用主要包括以下常量:
-类和接口和全限定名:例如对于String这个类,它的全限定名就是java/lang/String。
-字段的名称和描述符:所谓字段就是类或者接口中声明的变量,包括类级别变量(static)和实例级的变量。
-方法的名称和描述符。所谓描述符就相当于方法的参数类型+返回值类型。

(2)运行时常量池

我们知道类加载器会加载对应的Class文件,而class文件中的常量池,会在类加载后进入方法区中的运行时常量池【此时存在在内存中】。并且需要的注意的是,运行时常量池是全局共享的,多个类共用一个运行时常量池。并且class文件中常量池多个相同的字符串在运行时常量池只会存在一份。
注意运行时常量池存在于方法区中。

(3)字符串常量池
看名字我们就可以知道字符串常量池会用来存放字符串,也就是说常量池中的文本字符串会在类加载时进入字符串常量池。
上面我们说常量池中的字面量会在类加载后进入运行时常量池,其中字面量中有包括文本字符串,显然从这段文字我们可以知道字符串常量池存在于运行时常量池中。也就存在于方法区中。
到了JDK1.7以及之后的版本中,运行时常量池并没有包含字符串常量池,运行时常量池存在于方法区中,而字符串常量池存在于堆中。

3.问题解析

1)
String str1 = new String("1");
解析:首先此行代码创建了两个对象,在执行前会在常量池中创建一个"1"的对象,然后执行该行代码时new一个"1"的对象存放在堆区中;然后str1指向堆区中的对象;
str1.intern();
解析:该行代码首先查看"1"字符串有没有存在在常量池中,此时存在则直接返回该常量,这里返回后没有引用接受他。【假如不存在的话在 jdk1.6中会在常量池中建立该常量,在jdk1.7以后会在堆中】

String str2 = "1";
解析:此时"1"已经存在在常量池中,str2指向常量池中的对象;
System.out.println(str1 == str2);  //结果是 false or true?
解析:str1指向堆区的对象,str2指向常量池中的对象,两个引用指向的地址不同,输入false;

String str3 = new String("2") + new String("2");
解析:此行代码执行的底层执行过程是 首先使用StringBuffer的append方法将"2"和"2"拼接在一块,然后调用toString方法new出“22”;所以此时的“22”字符串是创建在堆区的;
t3.intern();
解析:此行代码执行时字符串常量池中没有"22",所以此时在jdk1.6中会在字符串常量池中创建"22",而在jdk1.7以后会把堆中该对象的引用放在常量池中;
String str4 = "22";
解析:此时的str4在jdk1.6中会指向方法区,而在jdk1,7中会指向堆区;
System.out.println(str3 == str4); //结果是 false or true?
解析:很明显了 jdk1.6中为false 在jdk1.7中为true;

2)
String str1 = "aaa";
解析:str1指向方法区;
String str2 = "bbb";
解析:str2 指向方法区
String str3 = "aaabbb";
解析:str3指向方法区
String str4 = str1 + str2;
解析:此行代码上边已经说过原理。str4指向堆区
String str5 = "aaa" + "bbb";
解析:该行代码重点说明一下,jvm对其有优化处理,也就是在编译阶段就会将这两个字符串常量进行拼接,也就是"aaabbb";所以他是在方法区中的;
System.out.println(str3 == str4); // false or true
解析:很明显 为false, 一个指向堆 一个指向方法区
System.out.println(str3 == str4.intern()); // true or false
解析:jdk1.6中str4.intern会把“aaabbb”放在方法区,1.7后在堆区,所以在1.6中会是true 但是在1.7中是false
System.out.println(str3 == str5);// true or false
解析:都指向字符串常量区,字符串长常量区在方法区,相同的字符串只存在一份,其实这个地方在扩展一下,因为方法区的字符串常量是共享的,在两个线程同时共享这个字符串时,如果一个线程改变他会是怎么样的呢,其实这种场景下是线程安全的,jvm会将改变后的字符串常量在字符串常量池中重新创建一个处理,可以保证线程安全。

3)
String t1 = new String("2");
解析:创建了两个对象,t1指向堆区
String t2 = "2";
解析:t2指向字符串常量池
t1.intern();
解析:字符串常量池已经存在该字符串,直接返回;
System.out.println(t1 == t2); //false or true
解析:很明显 false
String t3 = new String("2") + new String("2");
解析:过程同问题1 t3指向堆区
String t4 = "22";
解析:t4指向字符串常量池
t3.intern();
解析: 字符串常量池中已经存在该字符串 直接返回
System.out.println(t3 == t4); //false or true
解析: 很明显为 false 指向不同的内存区

4)

    Integer a = 1;
    Integer b = 2;
    Integer c = 3;
    Integer d = 3;
    Integer e = 321;
    Integer f = 321;
    Long g = 3L;

    System.out.println(c == d);
    System.out.Println(e == f);
    System.out.println(c == (a + b));
    System.out.println(c.equals(a+b));
    System.out.println(g == (a + b));
    System.out.println(g.equals(a + b));

(1). 内存中有一个java基本类型封装类的常量池。这些类包括Byte, Short, Integer, Long, Character, Boolean。需要注意的是,Float和Double这两个类并没有对应的常量池。
(2).上面5种整型的包装类的对象是存在范围限定的;范围在-128~127存在在常量池,范围以外则在堆区进行分配。
(3).包装类的“==”运行符在不遇到算术运算的情况下不会自动拆箱,以及他们的equals()方法不处理数据类型的关系,通俗的讲也就是 “==”两边如果有算术运算, 那么自动拆箱和进行数据类型转换处理,比较的是数值等不等能。
(4).Long的equals方法会先判断是否是Long类型。
(5).无论是Integer还是Long,他们的equals方法比较的是数值。
System.out.println(c == d)。
解析:由于常量池的作用,c与d指向的是同一个对象(注意此时的==比较的是对象,也就是地址,而不是数值)。因此为true。
System.out.println(e == f)。
由于321超过了127,因此常量池失去了作用,所以e和f数值虽然相同,但不是同一个对象,以此为false。
System.out.println(c == (a+b))。
此时==两边有算术运算,会进行拆箱,因此此时比较的是数值,而并非对象。因此为true。
System.out.println(c.equals(a+b))
c与a+b的数值相等,为true。
System.out.pirnln(g == (a + b))
由于==两边有算术运算,所以比较的是数值,因此为true。
System.out.println(g.equals(a+b))。
Long类型的equal在比较是时候,会先判断a+b是否为Long类型,显然a+b不是,因此false

四、JVM面试题


4.1 解释 Java 堆空间及 GC?

当通过Java命令启动Java进程的时候,会为它分配内存。内存的一部分用于创建堆空间,当程序中创建对象的时候就从空间中分配内存。
GC是JVM内部的一个进程,回收无效对象的内存用于将来的分配。

4.2 JRE、JDK、JVM 及 JIT 之间有什么不同?

JVM :英文名称(Java Virtual Machine),就是我们耳熟能详的 Java 虚拟机。它只认识 xxx.class 这种类型的文件,它能够将 class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作。所以说,jvm 是 Java 能够跨平台的核心,具体的下文会详细说明。
JRE :英文名称(Java Runtime Environment),我们叫它:Java 运行时环境。它主要包含两个部分,jvm 的标准实现和 Java 的一些基本类库。它相对于 jvm 来说,多出来的是一部分的 Java 类库。
JDK :英文名称(Java Development Kit),Java 开发工具包。jdk 是整个 Java 开发的核心,它集成了 jre 和一些好用的小工具。例如:javac.exe,java.exe,jar.exe 等。
显然,这三者的关系是:一层层的嵌套关系。JDK>JRE>JVM。
JIT 代表即时编译(Just In Time compilation),当代码执行的次数超过一定的阈值时,会将 Java 字节码转换为本地代码,如,主要的热点代码会被准换为本地代码,这样有利大幅度提高 Java 应用的性能。

4.3 32 位 JVM 和 64 位 JVM 的最大堆内存分别是多数?

理论上说上 32 位的 JVM 堆内存可以到达 2^32,即 4GB,但实际上会比这个小很多。不同操作系统之间不同,如 Windows 系统大约 1.5 GB,Solaris 大约 3GB。64 位 JVM允许指定最大的堆内存,理论上可以达到 2^64,这是一个非常大的数字,实际上你可以指定堆内存大小到 100GB。甚至有的 JVM,如 Azul,堆内存到 1000G 都是可能的。
2的32次方是:4294967296=1024*1024*1024*4
怎样通过 Java 程序来判断 JVM 是 32 位 还是 64 位?
你可以检查某些系统属性如 sun.arch.data.model 或 os.arch 来获取该信息。
String arch = System.getProperty("sun.arch.data.model");

4.4 JVM 选项 -XX:+UseCompressedOops 有什么作用?为什么要使用?

这个可以压缩指针,起到节约内存占用的新参数。
启用CompressOops后,会压缩的对象:
• 每个Class的属性指针(静态成员变量)
• 每个对象的属性指针
• 普通对象数组的每个元素指针
当你将你的应用从 32 位的 JVM 迁移到 64 位的 JVM 时,由于对象的指针从 32 位增加到了 64 位,因此堆内存会突然增加,差不多要翻倍。这也会对 CPU 缓存(容量比内存小很多)的数据产生不利的影响。因为,迁移到 64 位的 JVM 主要动机在于可以指定最大堆大小,通过压缩 OOP 可以节省一定的内存。通过 -XX:+UseCompressedOops 选项,JVM 会使用 32 位的 OOP,而不是 64 位的 OOP。


4.5 Serial 与 Parallel GC之间的不同之处?

Serial 与 Parallel 在GC执行的时候都会引起 stop-the-world。它们之间主要不同 serial 收集器是默认的复制收集器,执行 GC 的时候只有一个线程,而 parallel 收集器使用多个 GC 线程来执行。

4.6 64 位 JVM 中,int 的长度是多数?


Java 中,int 类型变量的长度是一个固定值,与平台无关,都是 32 位。意思就是说,在 32 位 和 64 位 的Java 虚拟机中,int 类型的长度是相同的。


4.7 你能保证 GC 执行吗?

不能,虽然你可以调用 System.gc() 或者 Runtime.gc(),但是没有办法保证 GC 的执行。

4.8 怎么获取 Java 程序使用的内存?堆使用的百分比?

可以通过 java.lang.Runtime 类中与内存相关方法来获取剩余的内存,总内存及最大堆内存。通过这些方法你也可以获取到堆使用的百分比及堆内存的剩余空间。
Runtime.freeMemory() 方法返回剩余空间的字节数,Runtime.totalMemory() 方法总内存的字节数,Runtime.maxMemory() 返回最大内存的字节数。

4.9 Java 中的编译期常量是什么?使用它又什么风险?

公共静态不可变(public static final )变量也就是我们所说的编译期常量,这里的 public 可选的。实际上这些变量在编译时会被替换掉,因为编译器知道这些变量的值,并且知道这些变量在运行时不能改变。这种方式存在的一个问题是你使用了一个内部的或第三方库中的公有编译时常量,但是这个值后面被其他人改变了,但是你的客户端仍然在使用老的值,甚至你已经部署了一个新的jar。为了避免这种情况,当你在更新依赖 JAR 文件时,确保重新编译你的程序。

4.10 JVM调优经验

4.10.1 总结

年轻代大小选择
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

年老代大小选择
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:
-并发垃圾收集信息
-持久代并发收集次数
-传统GC信息
-花在年轻代和年老代回收上的时间比例
减少年轻代和年老代花费的时间,一般会提高应用的效率
吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

4.10.2 较小堆引起的碎片问题

因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
1、-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
2、-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

4.10.3 回归问题

Q:为什么崩溃前垃圾回收的时间越来越长?
A:根据内存模型和垃圾回收算法,垃圾回收分两部分:内存标记、清除(复制),标记部分只要内存大小固定时间是不变的,变的是复制部分,因为每次垃圾回收都有一些回收不掉的内存,所以增加了复制量,导致时间延长。所以,垃圾回收的时间也可以作为判断内存泄漏的依据
Q:为什么Full GC的次数越来越多?
A:因此内存的积累,逐渐耗尽了年老代的内存,导致新对象分配没有更多的空间,从而导致频繁的垃圾回收
Q:为什么年老代占用的内存越来越大?
A:因为年轻代的内存无法被回收,越来越多地被Copy到年老代

猜你喜欢

转载自blog.csdn.net/sinat_37138973/article/details/89873814