Tips:
1.Java内存区域,5个区域的存储内容,常量池与直接内存。
- 内存区域与内存模型区别
- JVM栈中存储的局部变量表(基本类型与引用类型)与栈帧
- JVM栈中抛出异常的情况
- JVM堆的分区
- JVM堆的要点(要结合后面的来讲)
- 方法区和常量池-联系字节码与类加载
2.对象的分配、布局、访问
- 创建对象的具体流程
- 对象的大小计算
- 引用如何对应到对象实例。
3.GC算法
- 3种垃圾收集算法
- 什么对象需要被GC
4.常用GC收集器,组合,区别,效率。
- 新生代收集器有哪些
- 老年代收集器有哪些
5.内存分配,配置参数。
- Minor GC & Full GC
- 常用的jvm配置参数
1.Java内存区域
首先说一下内存区域与内存模型这两者,从我看的资料来说,很多博客都直接把JVM运行时数据区当成了内存模型来说,就我的理解而言:
内存区域就是将JVM所管理的内存区域,根据存储内容与生命周期等不同,而划分成的不同区域。
内存模型则应该是线程工作的一种模型层面的理解,它说的是怎么保证线程与主存,线程与线程之间的通信交互。
内存模型后面再说,先来说说内存区域,先盗张图:
这张图出于《深入理解Java虚拟机》,作者周志明对这张图的注释是Java虚拟机运行时的数据区,所以它和内存模型,应该确实是两个意思。当然,实际上也就是两种说法,内存模型中的主存,实际上就是数据区里的堆和方法区,线程工作内存,也可以差不多等同线程自己的线程栈。
接下来说明的是这个内存区域,各个部分到底存储的是什么内容。
1.1 PC计数器:
内存较小,无OOM,线程私有,每一个线程有一个对应的PC计数器,当前线程执行的字节码的行号指示器。
(大概要结合执行引擎来理解?先放)
1.2 JVM栈:
线程私有,生命周期与线程相同(分配和结束时间固定明确,不用专门的GC),一个线程有一个对应的栈,栈里有描述方法的栈帧(Stack Frame),栈帧里存储局部变量表、操作数表、动态链接、方法出口。每个方法的调用到执行完成对应着栈帧在栈中的入栈与出栈。
以main()方法启动的主线程为例,主线程里可以有不同的方法,可以定义局部变量。
public static void main(String[] args)
{//举例,没赋值
int a,b,c;
Object d;
func1(a);
func2(b,c);
func3(d);
}
那么在主线程这个栈中,首先它有自己的局部变量表,上面存储了3个int基本变量,1个Object引用变量。
在栈的局部变量表中,基本变量直接存储,那也就是3个4byte的区域,直接存储着这3个变量。
而引用变量,按32位JVM和64位JVM的区别,分别对应着一个存储了4byte或8byte大小的地址引用。
如果是Object d = new Object(),那么则是4byte的引用变量d存放在栈中,8byte大小的对象实例new Object()存放在堆。(空Object对象大小8byte,任何类都继承于Object所以类的大小就是8byte加上类中成员的大小)
局部变量表之后就是方法,方法有自己的内容如上述,那么接下来的三个方法,如果不涉及递归调用,那么就会依次入栈出栈。在执行的时候,就可以将执行这个方法需要的信息当做栈帧。
(字节码执行引擎什么的好难理解啊,还不能和栈帧对应起来,这是后面类加载的内容了,到这里,知道栈里面,局部变量表存储局部变量和引用变量,栈帧是用来执行方法的就行了。)
JVM栈中,有两种抛出异常的情况,栈溢出与内存溢出,我暂且简单的理解成:
StackOverflowError(写个无限递归方法,调用它,自然栈就炸了,抛出这个错误)
OutOfMemoryError(既然栈里会存局部变量,那么只要在线程结束之前,定义足够多的int变量,也会引起OOM?)
1.3 本地方法栈:
和JVM栈类似,JVM栈对java执行方法(字节码)服务,本地方法栈则为虚拟机使用到的Native方法服务。
HotSpot将两者合二为一了。能抛出的异常与栈相同。
1.4 Java堆:
内存中最大的一块,线程共享,虚拟机启动时创建,用以存放对象实例。
Java堆中的分区是基于内存回收的,是垃圾回收器管理的主要区域。可以分为新生代与老年代,新生代又可以分为Eden区,和两个Survivor区。
说下新生代和老年代,新生代自然是新生成的对象,这种对象的生命周期不确定,就是不能确定它能存活多久,为什么要区分新生代和老年代,可以简单理解为,“一次性对象”与“常用对象”的回收机制肯定要区分开来,回收才会有效率。那么,要考虑的问题就是:在新生代中,将一些常用对象丢到老年代的情况。然后新生代和老年代采用不同的回收算法。
堆的tip有:分区(与GC一起讲),参数(JVM调优首选?避免OOM同时提高性能),对象创建(堆中位置,访问…)
1.5 方法区:
线程共享,用于存储虚拟机加载的类信息,常量,静态变量、即使编译器编译后的代码等数据。
也可以称为永久代(Permanent Generation),缘由是HotSpot团队将GC分代扩展到了方法区,也就是将方法区等于是和堆结合在一起用一套GC来处理,但最后应该还是不鼓励吧,JDK1.7后,HotSpot将永久代里的字符串常量池移出。
这部分区域的回收主要是对常量池的回收和对类的卸载。
内存不足时会抛出OOM。
1.6 运行时常量池:
是方法区的一部分。
先说一下Class文件,在.java文件经过编译器编译以后,会生成.class文件,这个文件里存储的是16位8byte的字节流文件,有一系列明确的格式规定,第几位的字节是用来标识什么信息,而这些信息就是方法区中存放的那些,类信息:接口父类,常量,修饰符等。除此之外,还有就是常量池,Constant_Pool.
class文件编译完后,会由类加载器等工具加载到JVM中,而class文件中常量池的那一部分,就会被加载到这边内存的运行时常量池。
运行时的常量池,到底存什么?一方面可以用String类来举例,String对象是基于常量池的,新生成一个String的字面量的话,如果常量池里没有,那么就会加入到常量池,这也是运行时常量池的动态特性。
而常量池里还有什么?其实最主要的就是一个Constant_Utf8_info,这个是一个字节码里的固定表达格式,用来存储一个指定length的UTF-8编码的字符串,在一个类或者方法中,基本上都要用到这个,因为类名需要这个,方法名需要这个,你自己定义的基本类型的标识符也要用这个表示。也就是说,对于类、方法、基本类型,他们的名字都要以这个格式存储到常量池里,当然还需要一些其他的项,这里只是一说。
1.7 直接内存:
它并不是虚拟机运行时数据区的一部分,而是通过一些本地方法,直接与实际的内存相连。
//搬运工:如NIO中,引入的一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
2.对象分配、布局、访问
基于HotSpot虚拟机2.1 对象的创建
分析一条new语句的实现:
1.要创建类的实例,则类必须要被加载,(类加载后面再聊),所以第一先确认该对象的类有没有被加载过,没加载则加载。
2.确认加载完类后,开始分配内存(这里只说在堆中分配),一个对象的内存大小是可以算的固定值,所以需要考虑的是把这个固定大小的对象放在哪。
这里要考虑一个因素,堆中的存储是否规整。规整则可以“紧挨着”开辟相应大小的空间存储,不规整,则要在空闲区域找一份大于该对象大小的空间存储。
前者的实现叫做“指针碰撞”,在规整区域已存与未存空间的中间有个分界指针,那么分配空间只需移动相应长度的指针。
后者的实现叫做“空闲列表”,用额外的一个表来记录哪块内存可用,并且需要比较大小。
而规整与否,又取决于GC算法的选择,并且在实现上又有另一些问题:使用指针碰撞引起的线程安全问题。使用压缩算法移动对象导致的引用地址变动。
说明1:内存规整则需要GC算法带有压缩功能,即复制算法与标记整理算法。 不规整则是标记清除算法。对应的垃圾回收器下面再说。
说明2:使用指针碰撞的话,在多线程new对象时,第一个对象的指针可能还没移完第二个对象就来了。解决方案有两种:第一种进行同步。第二种,先给每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),开关参数:-XX:+/-UseTLAB
3.在堆上分配好内存之后,虚拟机需要将分配到的内存空间都初始化为零值。接下来,对对象进行必要的设置,如对象是哪个类的实例、怎么找类的元数据信息(方法区)、对象的哈希码(一开始对象的哈希码就是对象地址)、对象的GC分代年龄。这些信息存放在对象的对象头(Header)中。
4.接下来,才是Java语义上的初始化工作。初始化完成之后,对象的创建完成。
2.2 对象的内存布局
对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象头分两部分:1.存储对象自身的运行数据 2.类型指针-指向它的类元数据的指针(类的信息存储在方法区)
实例数据就是对象的真正数据。
对齐填充,是HotSpot虚拟机的自动内存管理系统要求对象起始地址必须为8字节的整数倍,没对齐则需要进行填充。
了解完对象布局后可以计算出一个对象到底需要多少内存。
2.3 对象的访问定位
对象的访问就是栈中的引用对应到堆中的对象的过程,其具体实现取决于虚拟机。主流的实现有两种:
1.使用句柄访问,则在堆中额外分出一块内存作为句柄池,引用变量则存储的是句柄的地址,句柄中含有对象实例数据与类型数据各自的地址。这种方式的好处是引用变量存储的是稳定的句柄地址,在对象被移动(GC)时只会改变句柄的实例数据指针。
2.使用直接指针访问,则需要考虑如何放置类型数据的信息,引用变量存储的则直接是对象地址,其好处是访问速度更快,节省了一次指针定位的时间开销。
3.GC算法
3.1 三种垃圾收集算法
在新生代的复制算法中,是用一个Eden区与两个Survivor区,每次使用一个Eden区和一个Survivor区,当需要GC时,将Eden区和这个Survivor区的存活对象移到另一个Survivor区中,如果能够装得下,那就完成GC,然后填满这个Survivor区再装Eden区,直到下一次GC。但如果装不下,那么就需要占用老年代的空间,这就是分配担保。
怎样判断对象的存活情况:通过一系列的“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明此对象不可用。
来一段代码分析:
class Test
{
int a;
Object b;
}
class TestThread extends Thread
{
public void run()
{
Test t = new Test();
//t.func();代表堆t的一些操作
}
}
public class Main
{
public static void main(String[] args)
{
TestThread thread = new TestThread();
t.start();
}
}
线程thread的作用是实例化一个Test对象,并且用引用变量t指向它,然后对它进行一些操作。
首先来说一下放置内存的情况:
线程thread有了一个自己的线程栈,这个栈里有一个方法run(),run()方法的栈帧的局部变量表里定义了一个引用变量t,与一个实例化对象的操作。
调用new Test()后,由上面知识可知,进行创建对象的流程,方法区会保存类Test的一些信息,然后创建的对象实例会先进入堆中的新生代,一般是Eden区。
这些都完成后,在run()没有结束之前(func()操作时),此时,t就会作为一个GC Roots,这时进行可达性分析的话,在堆Eden区这个new Test()的对象处,就可以分析到有一个GC Roots的引用链指向了它,它就是存活状态。如果此时发生GC,它就是存活对象,那就移到Survivor区,并且年代+1。
然后,当run()结束,线程结束,线程栈与PC寄存器自己解决自己的回收,比如引用变量t的空间,就会被收回去。那么此时,再进行可达性分析,就不会有GC Roots的引用链,再指向刚才那个new Test()对象了,如果此时发生GC,它就是GC回收的对象。
之后再来看可以作为GC Roots的对象,就好理解很多了:虚拟机栈中引用的对象+方法区中类静态属性引用的对象+方法区中常量引用的对象+本地方法栈JNI引用的对象。
4.GC收集器
4.1 Serial
最基本最悠久的收集器,单线程(一个线程完成收集工作,且还要stop the world停止其他工作线程)。
简单高效,单线程则没有线程交互的开销,有单线程的最高收集效率。用于新生代,采用复制算法。
Serial-Serial Old收集器搭配,Serial新生代复制,Serial Old老年代标记整理。
4.2 ParNew
Serial的多线程版本,基本也用于新生代GC,采用复制算法,在性能上,在多CPU的环境下,能达到一个较好的效果。
因为能与CMS搭配而使用广泛,是-XX:UseConcMarkSweepGC选项下默认的新生代收集器。
4.3 Parallel Scavenge
与ParNewl类似,其特殊是在于期望达到可控制的吞吐量或者停顿时间。
最大停顿时间 -XX:MaxGCPauseMillis 吞吐量大小-XX:GCTimeRatio。
拥有自适应调节策略 -XX:UseAdaptiveSizePolicy。打开这个开关参数后,将不需要设置-Xmn,-XX:SurvivorRatio,-XX:PretenureSizeThreshold等细节参数,虚拟机会自己根据设置的最大停顿时间或者吞吐量来调节,(有时是要付出新生代空间等代价的。)
4.4 Serial Old
Serial的老年代版本,采用标记-整理算法,能与各种新生代收集器搭配,JDK1.5之前与Parallel Scavenge搭配较多,之后则一般只作为CMS的备案收集器。
4.5 Parallel Old
Parallel Old是Parallel Scavenge的老年代版本,使用多线程与标记-整理算法,在JDK1.6后出现,开始与Parallel Scavenge搭配,在吞吐量优先的场合使用。
4.6 CMS
CMS(Concurrent Mark Sweep),以获得最短回收停顿时间为目的的收集器。
并发,标记-清除,用在重视服务响应速度的互联网站或B/S系统上。
过程:初始标记-并发标记-重新标记-并发清除
问题:CPU资源占用,浮动垃圾,产生空间碎片
4.7 G1(有点迷)
Garbage-First,面向服务端应用,有可能代替CMS。
并行并发,分代收集,空间整合,可预测停顿。
5.内存分配
5.1 Minor GC & Full GC
新生代GC(Minor GC):发生在新生代的垃圾收集动作,因为Java对象大多有朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也较快。
老年代GC(Major GC/Full GC):发生在老年代的垃圾收集动作,Full GC有时需要考虑碎片空间的处理问题,有的回收器如CMS是提供压缩功能的(-XX:+UseCMSComactAtFullCollection,-XX:CMSFullGCsBeforeCompation)。
5.2 内存分配
1.对象优先在Eden区分配,Eden区不足时会发起一次Minor GC。
2.大对象直接进入老年代,避免大对象频繁在Survivor区复制,-XX:PretenureSizeThreshold
3.长期存活的对象进入老年代,经过一次Minor GC后仍存活,年龄+1,-XX:MaxTenuringThreshold.
4.动态对象年龄判断:在Survivor空间中,相同年龄所有对象大小的总和大于Survivor空间的一般,年龄大于等于该对象的可以直接进入老年代。
5.空间分配担保,发生Minor GC前,会检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果满足,那任何极端情况都不会发生担保失败(HandlePromotonFailure),即引起老年代内存不足。如果不满足,就要看是否允许担保失败,(一般来说都允许,而且担保失败之后也就是让老年代进行一次Full GC,只是会更让停顿时间更久),在不允许,和允许但其空间小于晋升老年代对象平均值时,直接就要进行一次Full GC。
5.3 常用参数配置
设置新生代大小 -Xmn 设置堆最大内存 -Xmx 设置堆初始值 -Xms
设置新生代与老年代比例 -XX:NewRatio 设置Eden区与Survivor区比例 -XX:SurvivorRatio
设置永久代大小 -XX:MaxPermSize
-XX:PretenureSizeThreshold -XX:MaxTuringThreshold
-UseSerialGC 使用Serial+Serial Old收集器组合进行回收,Client模式下默认值。
-UseConcMarkSweepGC 使用ParNew+CMS+Serial Old收集器组合进行回收。
-UseParallelOldGC 使用Parallel Scavenge+Parallel Old收集器组合进行回收
(-ParallelGCThreads -GCTimeRatio -MaxGCPauseMillis -UseAdaptiveSizePolicy)