1、JDK的基本概念

我们可以把Java程序设计语言、Java虚拟机、 Java API类库这三部分统称为JDK( Java Development Kit),JDK是用于支持Java程序开发的最小环境。

另外,可以把Java API类库中的Java SE API子集和Java虚拟机这两部分统称为JRE( Java RuntimeEnvironment),JRE是支持Java程序运行的标准环境。

2、JVM的基本原理

JVM是在java编译器和os平台之间的虚拟处理器。它是一种基于下层的操作系统和硬件平台并利用软件方法来实现的抽象的计算机,可以在上面执行java的字节码程序。JVM运行原理java编译器只需面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译器,编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。

JVM执行程序的过程 :

  1.     加载.class文件
  2.     管理并分配内存
  3.     执行垃圾收集JRE(java运行时环境)包含JVM的java程序的运行环境

JVM是Java程序运行的容器,但是它同时也是操作系统的一个进程,因此它也有它自己的运行的生命周期,也有自己的代码和数据空间。JVM在整个jdk中处于最底层,负责与操作系统的交互,用来屏蔽操作系统环境,提供一个完整的Java运行环境,因此也叫虚拟计算机。

操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境。

    1.创建JVM装载环境和配置

    2..装载JVM.dll

    3.初始化JVM.dll并挂接到JNIENV(JNI调用接口)实例

    4.调用JNIEnv实例装载并处理class类。(JVM运行原理如下图)

https://i-blog.csdnimg.cn/blog_migrate/016ce08b4e4d052f4e795df509476ed0.png

 

JVM实例对应了一个独立运行的java程序,它是进程级别;JVM执行引擎实例则对应了属于用户运行程序的线程,它是线程级别的。

JVM的生命周期:

(1)JVM实例的诞生:当启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。

(2)JVM实例的运行main()作为该程序初始线程的起点,任何其他线程均由该线程启动。JVM内部有两种线程:守护线程和非守护线程,main()属于非守护线程,守护线程通常由JVM自己使用,java程序也可以标明自己创建的线程是守护线程。

(3)JVM实例的消亡:当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。

 

3、JVM结构


https://i-blog.csdnimg.cn/blog_migrate/f0ea5c355f16deafab9092de0d243b6e.jpeg

JVM的内部体系结构中各部分的主要功能:

  • 类加载器:JVM启动,程序开始执行时,负责将class字节码加载到JVM内存区域中
  • 执行引擎:负责执行class文件中包含的字节码指令
  • 本地方法库:主要是调用C或C++实现的本地方法及返回结果
  • 运行时数据区:方法区、java堆、java栈、本地方法栈和程序计数器。

(1)方法区(线程共享的)

用于存储类结构信息的地方,包括类信息、常量池、静态变量、构造函数以及即时编译器编译后的代码等数据

(2)java堆(线程共享的)

存储java对象实例。java堆是Java虚拟机所管理的内存中最大的块。同时也是GC(垃圾回收机制)处理的的主要区域。

(3)java栈((线程私有的)

java栈总是和线程关联在一起,每当创建一个线程时,JVM就会为这个线程创建一个对应的java栈。在这个java栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值、动态链接等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。所以java栈是现成私有的。

(4)程序计数器(线程私有的)

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

(5)本地方法栈(线程私有的)

和java栈的作用差不多,只不过是为JVM使用到的native方法服务的。

 

4、JVM类加载机制

类加载器图:
https://i-blog.csdnimg.cn/blog_migrate/7ccd341d392d417f1ffd70c5250434c4.png

 

JVM类加载机制主要采用的是双亲委派模型

https://i-blog.csdnimg.cn/blog_migrate/a4f51b0b7429fb7d000d5aa7dbee9944.png

  • 启动类加载器 Bootsrap ClassLoader

它是最顶层的类加载器,是由C++编写而成, 已经内嵌到JVM中了。在JVM启动时会初始化该ClassLoader,它主要用来读取Java的核心类库JRE/lib/rt.jar中所有的class文件,

如果需要将自己写的类加载器加载请求委派给引导类加载器,那直接使用null代替即可。

  • 扩展类加载器 Extension ClassLoader

负责加载\lib\ext目录中的jar包。

  • 应用程序类加载器 Application ClassLoader

是类加载器ClassLoader.getSystemClassLoader()方法的返回值,因此称为系统类加载器,负责加载用户路径上指定的类库。一般情况下是默认的类加载器。

  • 自定义类加载器 Custom ClassLoader

负责加载用户自定义的jar包

以上4种类加载器之间的这种层次关系称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不会以继承的关系来实现,而是都是用组合关系来复用父加载器的代码。

 

双亲委派的工作过程:


如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。

双亲委派机制的最大优点就是使得java类随着它的类加载器一起具备了一种带有优先级的层次关系。尤其是保证了基础类的统一性,保证了java程序的稳定运行。

 

5、JVM方法区结构(内存模型)


https://i-blog.csdnimg.cn/blog_migrate/9750508d65178bb45061efb83d2d0d80.jpeg

图1 java虚拟机运行时数据区

5.1 程序计数器:

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能,会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选,取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线,程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined )。此内存区域,是唯一一个在Java虚拟机规范中没有规定任何OutOtMemoryError清况的区域。

5.2 Java虚拟机栈

与程序计数器一样,Java虚拟机栈( Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个機帧在虚拟机栈中从入栈到出栈的过程。

经常有人把Java内存区分为堆内存(Heap)和栈内存( Stack),这种分法比较粗糙,Java内存区域的划分实际上远比这复杂。这种划分方式的流行只能说明大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。其中所指的“堆”在后面会专门讲述,而所指的“栈”就是现在讲的虚拟机,或者说是虚拟机栈中的局部变量表部分。

局部变量表存放了编译期可知的各种基本数据类型( boolean、byte、char、short、int、foat、long、double)、对象引用( reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和 returnaddress类型(指向了一条字节码指令的地址)。

其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出Stackoverflowerror异常:如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出 Outofmemoryerror异常。

5.3 本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚全有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和DutOfMemoryError异常。

5.4 Java堆

对于大多数应用来说,Java堆( Java Heap)是Java虚拟机所管理的内存中最大的块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几平所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”( GarbageCollected Heap)。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代再细致一点的有Eden空间、 From Survivor空间、 To Survivor空间等。如果从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓神区( Thread LocalAllocation Buffer,TLAB)。不过,无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。

5.5 方法区

方法区( Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做NonHeap(非堆),目的应该是与Java堆区分开来。

5.6 运行常量池

运行时常量池( Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池( Constant PoolTable),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

Java虚拟机对Class文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可装载和执行。但对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String类的intern()方法。既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出 Outofmemoryerror异常。

5.7 直接内存

直接内存( Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致Outofmemoryerror异常出现,所以我们放到这里一起讲解在JDK1.4中新加入了NIO( New Input/Output)类,引入了一种基于通道(Channel)与缓神区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的Direct Byte Buffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制包括物理上的和操作系统级的限制),从而导致动态扩展时出现 Outofmemory Error异常。

5.8 总结


 

6、JVM垃圾回收GC原理

随着程序的运行,内存中的实例对象、变量等占据的内存越来越多,如果不及时进行回收,会降低程序运行效率,甚至引发系统异常。在五个java内存区域中,有3个是不需要进行垃圾回收的:本地方法栈、程序计数器、虚拟机栈。因为他们的生命周期是和线程同步的,随着线程的销毁,他们占用的内存会自动释放。所以,只有方法区和堆区需要进行垃圾回收,回收的对象就是那些不存在任何引用的对象。

6.1查找算法

垃圾收集器一般必须完成两件事:检测出垃圾;回收垃圾。怎么检测出垃圾?一般有以下几种方法:

1 引用计数法

引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到环的存在。每个对象添加到引用计数器,每被引用一次,计数器+1,失去引用,计数器-1,当计数器在一段时间内为0时,即认为该对象可以被回收了。它很难解决对象之间的相互循环引用问题。

2 可达性分析算法

根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证对明此象是不可用的。

总之,JVM在做垃圾回收的时候,会检查堆中的所有对象是否会被这些根集对象引用,不能够被引用的对象就会被垃圾收集器回收。

1)对象引用类型

  • 强引用 (不会回收)

    类似于“Person p = new Person()”这类的引用;垃圾收集器不会回收掉被强引用的对象。

  • 软引用 (缓存内存快溢出时回收)

    有用但非必须的对象,jdk中提供SoftReference类来实现软引用;系统在发生内存溢出异常之前,会把只被软引用的对象进行回收。用途是可以做缓存。

  • 弱引用 (WeakReference类都会回收)

    非必须的对象,jdk中提供了WeakReference类来实现软引用,比软引用弱一些;垃圾回收不论内存是否不足都会回收只被弱引用关联的对象。弱引用类型的对象只能生存到下一次垃圾收集发生之前。

  • 虚引用 (PhantomReference类回收时通知)

    对被引用对象的生存时间不影响;无法通过虚引用来取得一个对象实例;为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知;jdk提供PhantomReference类来实现虚引用。

6.2垃圾回收算法

垃圾回收是什么?简单的说垃圾回收就是回收内存中不再使用的对象。垃圾回收的基本步骤回收的步骤有2步:

    1.查找内存中不再使用的对象

    2.释放这些对象占用的内存。总得来说就产生了三种类型的回收算法。

 

6.2.1标记-清除法

最基础的收集算法是“标记一清除”(Mark- Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实在前一节讲述对象标记判定时已经基本介绍过了。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高:另外一个是空间向題,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。标记一清除算法的执行过程如图3-2所示


 

6.2.2复制算法

为了解决效率问题,一种称为“复制”( Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,未免太高了一点。复制算法的执行过程如图3-3所示。


 

6.2.3标记-整理算法

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。根据老年代的特点,有人提出了另外一种“标记一整理”(Mark- Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,“标记整理”算法的示意图如图3-4所示。


 

6.2.4分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”( Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或“标记-整理”算法来进行回收。

 

6.3垃圾收集器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。

垃圾收集器是垃圾回收算法(标记-清除算法、复制算法、标记-整理算法)的具体实现,不同商家、不同版本的JVM所提供的垃圾收集器可能会有很在差别.


https://i-blog.csdnimg.cn/blog_migrate/c1d9827450b18060bb5a7e3f4c11e7c8.png

1、图中展示了7种不同分代的收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;

2、而它们所处区域,则表明其是属于新生代收集器还是老年代收集器:

新生代收集器:Serial、ParNew、Parallel Scavenge;老年代收集器:Serial Old、Parallel Old、CMS;整堆收集器:G1;

3、两个收集器间有连线,表明它们可以搭配使用:

Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1

6.3.1 Seria收集器

 

6.3.2 Parnew收集器

 

6.3.3 Parallel Scavenge收集器

 

6.4 Serial Old收集器

 

6.5 Parallel Old收集器

 

6.6 CSM收集器

 

6.7 G1收集器

 

6.8 总结

 

7、JVM内存分配及回收策略

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。关于回收内存这一点,我们已经使用了大量的篇幅去介绍虚拟机中的垃圾收集器体系及其运作原理,现在我们再一起来探讨一下给对象分配内存的那点事儿对象的内存分配,往大方向上讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地在上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则井不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置接下来我们将会讲解几条最普遍的内存分配规则,并通过代码去验证这些规则。本节中的代码在测试时使用 Client模式虚拟机运行,没有手工指定收集器组合,换句话说,验证的是使用 Serial/ Serial Old 收集器下( Pardew/ Serial Old收集器组合的规则也基本一致)的内存分配和回收的策略。读者不妨根据自己项目中使用的收集器写一些程序去验证一下使用其他几种收集器的内存分配策略。

7.1对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次 Minor GC.

新生代GC( Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多事具备朝生夕灭的特性,所以 Minor GC非常频繁,一般回收度也比较快。

老年代GC( Major GC/ Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少ー次的 Minor GC(但非绝对的,在 Parallelscavenge收器的收集策咯里就有直接进行 Major GC的策略选择过程)。 MajorGC的速度一般会比Minor GC慢10倍以上。

7.2大对象直接进入老年代

所谓大对象就是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组(笔者例子中的bye数组就是典型的大对象)。大对象对虚拟机的内存分配来说就是一个坏消息(替Java虚拟机抱怨一句,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

虚拟机提供了一个-XX: Pretenuresize Threshold参数,令大于这个设置值的对象直接在老年代中分配。这样做的目的是避免在Edcn区及两个 Survivor区之间发生大量的内存拷贝(复习一下:新生代采用复制算法收集内存)

注意:PretenureSizeThreshold参数只对 Serial和 Pardew两收集器有数, ParallelScavenge收渠器不认识这个数, Parallel Scavenge收集器一般并不需要设置。果遇到必频使用此参数的场合,可以考虑 Pardew加CMS的收集器组合。

7.3长期存活的对象将进入老年代

虚拟机既然采用了分代收集的思想来管理内存,那内存回收时就必须能识別哪些对象应当放在新生代,哪些对象应放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次 Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到 Survivor空间中,并将对象年龄设为1。对象在 Survivor区中每熬过一次 Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX: Max Tenuring Threshold来设置。

7.4动态对象年龄判定

为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到Max'tenuring Threshold才能普升老年代,如果在 Survivor空间中相同年龄所有对象大小的总和大于 Survivor 3空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 Max Tenuring T hreshold中要求的年龄。

 

总结分配策略就是:

  1. 对象优先在Eden分配
  2. 大对象直接进老年代
  3. 长期存活的对象将进入老年代
  4. 动态对象进行年龄判定再分代

 

8、JVM内存泄露和溢出

8.1.定义

内存泄露:指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用。

内存溢出:指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。

从定义上可以看出内存泄露是内存溢出的一种诱因,不是唯一因素。

Java 堆内存的OutOfMemoryError异常是实际应用中最常见的内存溢出异常情况。出现Java 堆内存溢出时,异常堆栈信息“java.lang.OutOfMemoryError”会跟着进一步提示“Java heapspace”。

8.2.常见内存泄露的几种场景

1、长生命周期的对象持有短生命周期对象的引用

这是内存泄露最常见的场景,也是代码设计中经常出现的问题。例如:在全局静态map中缓存局部变量,且没有清空操作,随着时间的推移,这个map会越来越大,造成内存泄露。

2、修改hashset中对象的参数值,且参数是计算哈希值的字段

当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段,否则对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中删除当前对象,造成内存泄露。

3、机器的连接数和关闭时间设置

长时间开启非常耗费资源的连接,也会造成内存泄露。

8.3.内存溢出的几种情况

1、堆内存溢出(outOfMemoryError:Java heap space)

在jvm规范中,堆中的内存是用来生成对象实例和数组的。

如果细分,堆内存还可以分为年轻代和年老代,年轻代包括一个eden区和两个survivor区。

当生成新对象时,内存的申请过程如下:

a、jvm先尝试在eden区分配新建对象所需的内存;

b、如果内存大小足够,申请结束,否则下一步;

c、jvm启动youngGC,试图将eden区中不活跃的对象释放掉,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;

d、Survivor区被用来作为Eden及old的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;

e、 当OLD区空间不够时,JVM会在OLD区进行full GC;

f、full GC后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”: outOfMemoryError:java heap space

2、方法区内存溢出(outOfMemoryError:permgem space)

在jvm规范中,方法区主要存放的是类信息、常量、静态变量等。

所以如果程序加载的类过多,或者使用反射、gclib等这种动态代理生成类的技术,就可能导致该区发生内存溢出,一般该区发生内存溢出时的错误信息为: outOfMemoryError:permgem space

3、线程栈溢出(java.lang.StackOverflowError)

线程栈时线程独有的一块内存结构,所以线程栈发生问题必定是某个线程运行时产生的错误。

一般线程栈溢出是由于递归太深或方法调用层级过多导致的。

发生栈溢出的错误信息为:

java.lang.StackOverflowError

 

9、怎么样避免发生内存泄露和溢出

1、尽早释放无用对象的引用

2、使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域

3、尽量少用静态变量,因为静态变量存放在永久代(方法区),永久代基本不参与垃圾回收

4、避免在循环中创建对象

5、开启大型文件或从数据库一次拿了太多的数据很容易造成内存溢出,所以在这些地方要大概计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。

 

10、jdk1.7与jdk1.8的区别

jdk1.7:
https://i-blog.csdnimg.cn/blog_migrate/2506c2f51726cb9bb6c0a4ef7e42a8e8.png

jdk1.8:

https://i-blog.csdnimg.cn/blog_migrate/406f93ec99cc2e0fa29ffb325de3b077.png

 

 

————————————————
版权声明:本文为CSDN博主「快乐的一只小喵喵」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_34801169/article/details/88791938

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐