秋招准备-Java-JVM-内存区域+GC

11 篇文章 0 订阅
1 篇文章 0 订阅

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 三种垃圾收集算法

        1.标记-清除(Mark-Sweep)算法,分为“标记”和“清除”两个阶段:首先标记需要被回收的对象,标记完成后统一回收被标记的对象。(效率问题&空间问题)

        2.复制算法:将可用内存分为两块,一次只用其中的一块,当GC时,将这块的存活对象复制到另一块中,然后对这块整体清理。

        3.标记-整理算法:标记过程与标记算法类似,而后面的整理过程则是,先将存活的对象移动到一端,然后清理端边界以外的内存。

        现在的虚拟机都采用“分代收集”算法。对新生代,因为每次垃圾收集都有大批对象死去,只有少量存活,所以采用复制算法。对老年代,因为对象存活率高,没有额外空间进行分配担保,一般就用标记-清除与标记-整理算法。

        

        在新生代的复制算法中,是用一个Eden区与两个Survivor区,每次使用一个Eden区和一个Survivor区,当需要GC时,将Eden区和这个Survivor区的存活对象移到另一个Survivor区中,如果能够装得下,那就完成GC,然后填满这个Survivor区再装Eden区,直到下一次GC。但如果装不下,那么就需要占用老年代的空间,这就是分配担保。

        而Eden区和Survivor区的比例,则是复制算法效率的关键,Survivor区越小,那么可以利用的内存区域就越多,但随之而来的就是如果对象的生存率过高,需要频繁担保,占用老年代空间,一般来说,默认Eden区与Survivor区的比例是8:1.


    3.2 可达性分析 

        怎样判断对象的存活情况:通过一系列的“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)

        


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值