Java基础篇--JVM

目录

JVM

JVM是什么?

JVM的内存分区

***:(实战)列举下面代码从类加载到main方法运行,各个变量所在的内存分区

Java的内存模型

Java的类加载机制

***:什么是符号引用,什么是直接引用?

***:什么是双亲委派机制?有什么作用?

JVM运行时内存

垃圾回收和回收算法

1.如何确定哪些是垃圾?

2.如何回收垃圾?

***:新生代中的对象何时会移入老年代?

***:不同类型引用与垃圾回收之间的关系

JVM调优

***:什么是内存泄漏,什么是内存溢出?

发生内存溢出的几种情况和解决措施:

其他的调优参数:


前言

带着问题学java系列博文之java基础篇。从问题出发,学习java知识。


JVM

JVM知识导图

 

JVM是什么?

JVM是Java Virtual Machine的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行,JVM正是java程序实现跨平台运行的基础。

Java虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上,任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。

 

JVM的内存分区

JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA 堆、方法区】和直接内存。线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存在/销毁跟随本地线程的生/死对应。线程共享区域则是随虚拟机的启动/关闭而创建/销毁直接内存并不是 JVM 运行时数据区的一部分, 但也会被频繁的使用:。在 JDK 1.4 引入的 NIO 提供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后使用DirectByteBuffer 对象作为这块内存的引用进行操作(详见: Java基础篇--IO 的NIO部分), 这样就避免了在 Java堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。

程序计数器(线程私有)

一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有” 的内存。在HotSpot VM内,正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址);如果执行的是 Native 方法,则为空(undefined)。这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。

虚拟机栈(线程私有)

是描述 java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。 每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。此外,引用类型对象实例的引用也保存在栈区。

栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派(Dispatch Exception)。 栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。

本地方法栈(线程私有)

本地方法区和虚拟机栈作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务, 如果一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。

堆(线程共享)--运行时数据区

堆内存是被线程共享的一块内存区域, 创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。 由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、 From Survivor 区和 To Survivor 区)和老年代。

方法区(线程共享)--永久代

方法区即我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、 常量、 静态变量、 即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存,而不必为方法区开发专门的内存管理器(永久代的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)。

运行时常量池(Runtime Constant Pool)是方法区的一部分。 Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中(比如String字符串)。 Java 虚拟机对 Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。

 

***:(实战)列举下面代码从类加载到main方法运行,各个变量所在的内存分区

public class Test {

    public static int i = 1;
    public static String a = "a";
    public static Person person1;
    public static Person person2 = new Person();

    private int j = 2;
    private String b = "b";
    private Person personField1;
    private Person person2Field = new Person();

    static {
        int k = 3;
        String s = "s";
        int l = i+k;
        person1 = new Person();
    }

    public static void main(String[] args) {
        new Test().test();
    }

    public void test(){
        int m = 4;
        String c = "c";
        Person person = new Person();
    }
}

从Test类被加载进内存,内存中生成class对象开始:Test在被加载进内存时,先初始化static的类变量,它们都是线程共享的,所以放在堆区;从上面的知识了解到,堆区细分了方法区,这些静态变量都是放在方法区。所以此时方法区有i、a、person1、person2四个变量名(引用),有1、“a”、new Person()生成的person对象共三个具体值;另外方法区又细分运行时常量池,所以“a”字符串常量是在常量池中;

继续往下,j、b、personField1和personField2是成员变量,都依赖于具体的实例对象,所以类加载过程中,这些变量不会被初始化,仅仅记录一下类信息(类信息存储在方法区);

然后到了static修饰的静态代码块,静态代码块可以理解成一个方法在执行。方法中定义的临时变量都是随方法的调用而生成或者销毁,它们都是线程私有的,不能共享。随着代码块的执行,首先生成栈帧入栈,然后在栈内存入基本类型变量名(引用)k,l和s,由于字符串的特殊处理,其中“s”存入常量池中,int型3以及4(i+k)属于基本类型,也是直接存入栈内;person是引用类型,所以在栈内存入变量名person1,再在堆区生成person对象。当static代码块执行完毕,退出代码块,所有的临时变量全部销毁,在堆区的引用对象也跟着销毁(系统GC检测到没有任何引用指向它,判定它为不可达的,所以会当做垃圾回收,这里的person对象就是);所以栈内的k、l、s和person1都出栈,在堆区的person对象也将会被GC回收;

至此,类加载进内存结束。

接着main方法被执行:main方法中,先是new Test()创建了一个Test对象实例,创建的对象实例是在堆区,其引用在栈区(因为main方法是由jvm调用的,所以调用者肯定是持有test实例的引用);Test类是有成员变量的,它们随着对象的创建而初始化,是线程共享的(只要线程持有这个对象,就可以共享它的成员变量),所以此时在堆区存入了j、b、personField1和personField2共四个变量名(引用,它们都是test对象实例的属性),基础类型和引用类型的值也都在堆区(2和new出来的一个person实例在堆区,“b”存入常量池)。

紧接着调用test实例的test():执行test()方法和执行static静态代码块类似,都是首先生成栈帧入栈,然后临时变量都在栈区(引用类型则是引用在栈区,具体的对象实例在堆区)。

总结:堆区属于线程共享区域,主要存储对象实例以及其成员属性等;其下又细分方法区,主要存储类信息(常量、静态变量、符号引用等)。栈区属于线程私有区域,主要是方法执行时入栈,方法的临时变量存入栈内(基本类型包括引用和值,引用类型则仅引用存入,引用类型的值在堆区)。栈区的数据往往随着方法的执行开始而创建,随着方法的执行结束而销毁,所以栈区不需要GC。堆区数据的生命周期受程序执行影响,当堆区的对象不存在任何可用引用指向它时,它就是可以被回收的,而我们总是在需要对象时就直接创建一个,从不去考虑该对象什么时候回收,对象的回收都交给jvm,GC机制正是java语言的魅力之一,java的GC也主要是对堆区进行回收。

 

Java的内存模型

概念:Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

像c/c++等语言,是直接使用物理硬件和操作系统的内存模型,所以在不同操作系统和硬件平台下常常表现不同,比如有些c/c++程序可能在windows平台运行正常,而在linux平台却运行有问题。Java语言可以做到“一次编译,到处运行”,就是因为jvm有自己的内存模型,不同系统的机器只需要安装对应的jdk,然后编译好的程序就可以正常运行了,不用担心不同系统的内存底层细节影响程序运行,因为访问操作系统的内存都交给jvm了。

现代的计算机cpu计算能力都很强,限制计算器处理速度的,主要还是操作内存/磁盘这类IO操作的速度太慢。为了解决cpu算力和IO速率之间的冲突,在内存和处理器之间加上高速缓存。高速缓存具有非常高的IO速率,不过造价高昂,所以往往容量较小(现在的计算机一般都有二级缓存,好一点的还有三级缓存)。它相当于一个缓冲区,cpu直接将运算结果存入到高速缓存,然后cpu就可以继续处理其他运算,最后再由高速缓存统一刷新入主内存。这样做可以很好的解决cpu算力和IO速率之间的冲突,但是也引进一个问题——多线程并发数据同步问题。比如线程A正在运算主内存的一个变量account(5),线程B也正在运算这个变量,A运算后得到一个结果10,存入到A对应的高速缓存;B运算后也得到了一个结果15,然后存入到B对应的高速缓存中;此时就发生了缓存不一致现象,最后主内存中account的值也肯定不是期望值。为了解决缓存不一致,数据不同步的问题,操作系统在高速缓存和主内存之间,增加了一个协议,要求操作高速缓存和主内存的数据时,要遵循缓存一致性协议。缓存一致协议也有多种,其中最著名的是Intel的MESI协议:当多个缓存副本持有共享变量,其中有一个cpu操作了某个缓存中的共享变量,则发出信号通知其他缓存该变量值无效,需要重新从内存读取。

由于java是在虚拟机JVM中运行,所以需要java自己实现一套内存模型,保证java程序在各种平台下对内存的访问都可以效果一致性。Java保证效果一致性的主要策略是:

1.限制处理器优化(禁止指令重排序)

2.内存屏障

 

Java的类加载机制

我们编码生成的是java文件,它遵循java语言规范,保存着具体的业务逻辑。但是JVM是无法直接执行它的,还需要java编译器javac对java文件进行编译生成.class字节码文件。class字节码文件保存着JVM需要执行的指令,当JVM需要某个类时,JVM会加载.class文件,并创建对应的class对象。将class文件加载进JVM内存,并创建class对象的过程称为类的加载。

触发类加载的时机主要有:

  • new 关键字创建对象实例(隐式类加载)
  • Class.forName()或者ClassLoader.loadClass()(显式类加载)
  • 调用类的静态方法、访问类的静态属性
  • 反射创建Class对象
  • 创建子类对象实例,父类将会被加载

从类被加载进内存到卸载出内存整个生命周期如下:

加载->连接(验证、准备、解析)->初始化->使用->卸载

加载:注意这仅仅是类加载的第一个过程,类加载器根据全限定名查找对应的字节码文件,根据字节码文件创建一个class对象;

验证(连接1):主要是文件格式的验证、元数据验证、字节码验证和符号引用验证;目的是确保该class文件符合当前JVM的规范,不会危害JVM的安全;

准备(连接2):为静态类变量分配内存并且设置该类变量的初始值,(如static int i = 5 这里只是将 i 赋值为0,在初始化的阶段再把 i 赋值为5),这里不包含final修饰的static ,因为final在编译的时候就已经分配了。这里不会初始化实例变量,类变量会分配在方法区中,实例变量会随着对象分配到Java堆中;

解析(连接3):主要是把常量池的符号引用替换成直接引用;

初始化:类加载的最后阶段,如果该类具有父类就进行对父类进行初始化,执行其静态代码块和初始化静态类变量。(前面已经对static 初始化了默认值,这里我们对它进行赋值,成员变量也将被初始化)

使用:通过创建的实例化对象,执行对象方法等;

卸载:将该class的相关信息从JVM中移除;

***:其中连接包含三个小阶段——验证、准备和解析

***:加载、连接和初始化是完整的类加载三大阶段

 

***:什么是符号引用,什么是直接引用?

符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标就行,符号引用与虚拟机的内存布局没有关系,引用的目标不一定需要已经加载到内存中。各种虚拟机的内存布局可以都不相同,但是他们能接受的符号引用必须是一致的。符号引用的字面量形式明确定义在JAVA虚拟机规范的Class文件中。

直接引用(Direct Reference):直接引用是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定在内存中存在。

 

***:什么是双亲委派机制?有什么作用?

java类加载的第一个阶段(加载类文件进内存),使用“双亲委派机制”,即加载时先委托父类加载器寻找目标类,如果父类加载器还有父类加载器,则继续向上传递,直到找到目标类,完成类加载;只有当父类加载器找不到的时候,再尝试降级使用子类加载器寻找并加载。

双亲委派机制的好处:1.加载的时候始终向上递归,所以当不同的子类共同加载某一个类时,最后都是委派到同一个父类加载器去完成类加载,父类加载器完成了类加载,则子类就不需要再重复加载了,这样就可以避免重复的类加载

2.分析一个场景:我们建立一个包(java.lang),然后自己实现了一个String类,我们其它的类有用到java的String类型(注意java的String类型也是java.lang包下)。此时当程序运行时,到底是java的String类型生效,还是我们自定义的String类生效呢?如果此时没有双亲委派机制,则我们自定义的类型很可能生效(看加载类),但是我们自定义的String类与java的String类型是完全不一样的,这会导致整个程序崩溃。而有了双亲委派机制,则向上传递,最后由启动类加载器进行加载,确保加载的是java的String类型,我们自定义的危害类String将无法影响到程序。防止核心类库被随意篡改,保证程序安全,是双亲委派机制的第二个作用。

 

JVM运行时内存

java内存的堆区从GC的角度可以细分为:新生代(Eden、SurvivorFrom和SurvivorTo)和老年代

新生代:用来存放新生的对象,一般占据堆区的 1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、 ServivorFrom、 ServivorTo 三个区。

新生代-Eden区:用来存放刚刚创建的对象(如果对象占用内存很大,则直接分配到老年代)。当Eden区内存不足时,就会触发MinorGC,对新生代进行垃圾回收;由于程序运行过程中对象的创建非常频繁,因此MinorGC会频繁触发。

新生代-SurvivorFrom和SurvivorTo区:因为新生代的MinorGC采用的是复制算法,所以需要两个Survivor区。一次MinorGC的过程,就是将Eden和SurvivorFrom区依然存活的对象复制到SurvivorTo区,然后清空Eden和SurvivorFrom区;然后再将SurvivorFrom和SurvivorTo区互换标志(即SurvivorFrom区变成SurvivorTo区,SurvivorTo区变成SurvivorFrom区)。

老年代:主要存放应用程序中生命周期长的内存对象或者占用很大内存的对象。

此外堆区还有一个方法区,用于存储类信息、元数据等,它们一般不会被清除,会一直存储在方法区,所以方法区也叫永久代。在主程序运行期GC也不会对永久区进行回收,因此当JVM加载的Class越来越多,有可能导致永久代溢出,最终抛出OOM异常。在Java8中移除了永久代,被一个“元数据区”所取代,它不再是堆区,而是使用本地内存,主要存储类的元数据,而类的静态变量和字符串常量池还是继续存储在堆中。

 

垃圾回收和回收算法

1.如何确定哪些是垃圾?

1.1  引用计数法

堆区存放的是引用对象,每个对象都是有引用指向它,如果一个对象没有任何引用指向它,则认为该对象是可以被回收的。引用计数法就是基于此理念设计的:当一个对象新增引用指向它,则其引用计数+1,减少一个引用指向它,则引用计数-1;当引用计数为0,则该对象可以被回收。该算法有一个问题:假如堆中对象A有一个引用指向对象B,对象B有一个引用指向对象A,即堆中对象A和B互相持有对方的引用,但是都没有任何其它引用指向它们。此时A和B都应该被回收,但由于各自的引用计数都是1,永远无法触发回收,导致内存泄漏。

1.2  可达性分析

为了解决引用计数法的循环引用问题,Java使用了可达性分析算法,从GC roots根搜索,如果和对象之间没有可达路径,则该对象是不可达的。注意,不可达并不意味着该对象可以立即回收。不可达对象变为可回收对象至少要经历两次标记过程,两次标记后仍然是不可达对象,则可以被GC。

 

2.如何回收垃圾?

2.1  标记清除算法

标记清除算法分为两个阶段:标记和清除。标记阶段标记出所有需要回收的对象,清除阶段清除所有被标记的对象,释放占用的空间。如下图:

从图中可以看到,标记清除算法有个最大的问题是,会导致内存碎片化严重,可用内存是散乱分布的,后续某个较大的对象可能无法找到可用的内存空间。

2.2  复制算法

复制算法是为了解决标记清除算法内存碎片化缺陷而提出的,它将内存划分为相同大小的两块,每次仅使用其中一块,当这一块存满后触发GC,将尚存活的对象复制到另外一块上去,然后清空这块内存。如下图:

复制算法实现简单,执行效率高,不易产生碎片;但是最大的问题是可用内存被压缩到了原本的一半,且存活对象过多的话,执行效率就会大大降低。

2.3  标记整理算法

标记整理算法是结合了标记清除和复制算法,避免了这两个算法各自的缺陷。它也有两个阶段——标记和整理,标记阶段和标记清除算法相同,区别是标记后不是清理对象,而是将继续存活的对象移向内存的一端,然后清除端边界外的标记回收对象。如下图:

2.4  分代收集算法

分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下GC 将堆划分为老年代(Tenured/Old Generation)和新生代(YoungGeneration)。老年代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

前面我们讲到运行时内存分区,新生代具有创建新对象频繁,新生对象存活时间短,新生代中的对象具有“朝生夕死”的特点。因此新生代最适合复制算法,java也是如此设计的,一块较大的Eden区,专门用来存放刚刚创建的对象,两块较小的相同大小的Survivor区,用来存放经历Minor GC后依然存活的对象。

老年代的内存回收则具有随机性,且无法确定是存活对象多还是回收对象多,所以采用综合性好标记整理算法,有效避免内存碎片化问题,且效率较高。

 

***:新生代中的对象何时会移入老年代?

新生代(当分配新生代内存发现空间不够时)Minor GC回收,非常频繁;新生代经历一次Minor GC后仍然存活则进入Survivor区,在survivor区每熬过一次Minor GC则成长一岁,当到达15岁后,进入老年代。

 

***:不同类型引用与垃圾回收之间的关系

前面我们讲到JVM判断一个对象是否可以被回收,是通过其引用分析可达性,当从GC root到对象之间没有可达路径,则该对象可被回收。因此引用是决定对象能否被回收的关键因素。为了满足不同场景的需要,java设计了四种类型的引用,对应着不同的垃圾回收情况。

强引用:将一个对象赋给一个引用变量,这个引用变量就是强引用。拥有强引用的对象处于可达状态,永远不会GC回收。(强引用是造成内存泄漏的主要原因之一)

软引用:依赖SoftReference类创建具体对象的软引用。对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。

        //创建person对象的软引用
        SoftReference<Person> pr = new SoftReference<>(new Person("lisi",28));
        //通过软引用get方法获得person对象
        System.out.println(pr.get().name);

弱引用:依赖WeakReference类创建具体对象的弱引用。对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

        //创建person对象的弱引用
        WeakReference<Person> pw = new WeakReference<>(new Person("lisi",28));
        //通过弱引用get方法获得person对象
        System.out.println(pw.get().name);

虚引用:依赖ReferenceQueue和PhantomReference联合创建具体对象的虚引用。虚引用的主要作用是跟踪对象被垃圾回收的状态。

        //创建引用队列
        ReferenceQueue<Person> pQueue = new ReferenceQueue<>();
        //创建person对象的虚引用
        PhantomReference<Person> ph = new PhantomReference<>(new Person("lisi",28),pQueue);
        Person person = ph.get();
        if (null != person){
            System.out.println(person.name);
        } else {
            System.out.println("获取不到具体的对象");
        }
        Reference<? extends Person> reference = pQueue.poll();
        if (reference != null) person = reference.get();
        if (null != person){
            System.out.println(person.name);
        } else {
            System.out.println("获取不到具体的对象");
        }

注意通过虚引用是无法得到具体的对象,也无法访问对象实例的属性、方法等;虚引用主要用于跟踪对象被回收的状态。

 

JVM调优

理解了JVM的类加载、内存分区、垃圾回收等,为了创建一个合适的JVM,既能满足程序的运行,又能避免占用过多系统资源造成浪费,所以我们需要配置合适的参数来创建JVM,而不是全部都使用默认参数。此外,当JVM抛出内存溢出错误时,我们可以根据JVM相关知识,对程序进行内存优化,保证程序可以稳定运行,没有异常。

***:什么是内存泄漏,什么是内存溢出?

内存泄漏:当程序中存在不再使用的对象或变量还在内存中,占用内存空间,就是内存泄漏。

内存溢出:当程序申请内存空间时,发现内存没有可用空间了,就会报OOM内存溢出。大量的内存泄漏累积,就会造成内存溢出。(堆内存溢出:GC效率不高,程序花费超过98%的时间来做GC,却只回收了不到2%的内存)

发生内存溢出的几种情况和解决措施:

  • 老年代内存溢出(java.lang.OutOfMemoryError: Java heap space)

最常见的内存溢出,主要是由于存在内存泄漏,导致内存中存在越来越多的无用且不可回收的对象,后续JVM分配内存时,发现无内存可用。

主要解决措施:通过堆快照分析具体的内存溢出点,优化代码,解决内存溢出(可依赖的工具有JProfile、MAT)

相关命令:1.让JVM在内存溢出时自动打印快照(在jar同级目录生成快照文件):-XX:+HeapDumpOnOutOfMemoryError  -XX:HeapDumpPath=./

                  2.手动导出快照文件:jmap -dump:format=b,file=heap.hprof 进程号

  • 栈溢出(java.lang.StackOverflowError)

这也是比较常见的内存溢出,该溢出的发生往往意味着存在方法的递归调用或者循环调用,但是忘记结束或者结束条件永远无法触发,导致方法调用栈无止境的加深,最后栈溢出。

主要解决措施:跟踪代码执行,找到具体的方法错误调用处,修正代码解决问题

  • 永久代溢出(java.lang.OutOfMemoryError: PermGen space/Metaspace)

永久代溢出,主要是方法区或元数据区被占满,导致后续JVM无法加载类。这个以前程序是很少发生的,不过由于现在的程序使用框架越来越多,各种框架都通过反射来加载类,导致永久代被占满。另外,不同的类加载器对同一个class类进行加载,jvm也无法判定是重复class类,而避免重复加载,相当于同一个class类被加载了多次,占用多份方法区内存空间,所以要尽量少使用自定义类加载器,使用的时候要注意避免重复加载同一个类。

主要解决措施:一般这种问题很少发生,如果真的发生了,除了检查是否自定义类加载器存在重复加载同一类的现象外,也没有很好的解决办法,只能适当扩大方法区。

相关命令:-XX:MaxPermSize=16m  (扩大方法区为16M)

  • 线程堆栈溢出(Fatal: Stack size too small)

java中一个线程的空间大小是有限制的,JDK5.0以后这个值是1M,与这个线程相关的数据将会保存在其中。当线程空间被占满了以后,就会出现上面异常。

主要解决措施:首先我们要分析这个线程是否存在内存泄漏,一般1M的内存空间足够一个线程使用了,解决内存泄漏问题;如果确实需要较大的空间,则可以适当调整JVM给线程分配的空间大小。

相关命令:-Xss 2m(调整线程的空间大小为2M)

其他的调优参数:

参数名称含义默认值 
-Xms初始堆大小物理内存的1/64(<1GB)默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.
-Xmx最大堆大小物理内存的1/4(<1GB)默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
-Xmn年轻代大小(1.4or lator) 注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。
整个堆大小=年轻代大小 + 年老代大小 + 持久代大小.
增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8
-XX:NewSize设置年轻代大小(for 1.3/1.4)  
-XX:MaxNewSize年轻代最大值(for 1.3/1.4)  
-XX:PermSize设置持久代(perm gen)初始值物理内存的1/64 
-XX:MaxPermSize设置持久代最大值物理内存的1/4 
-Xss每个线程的堆栈大小 JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右
一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长)
和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:"”
-Xss is translated in a VM flag named ThreadStackSize”
一般设置这个值就可以了。
-XX:ThreadStackSizeThread Stack Size (0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.]
-XX:NewRatio年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) -XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。
-XX:SurvivorRatioEden区与Survivor区的大小比值 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
-XX:LargePageSizeInBytes内存页的大小不可设置过大, 会影响Perm的大小 =128m
-XX:+UseFastAccessorMethods原始类型的快速优化  
-XX:+DisableExplicitGC关闭System.gc() 这个参数需要严格的测试
-XX:MaxTenuringThreshold垃圾最大年龄 如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率
该参数只有在串行GC时才有效.
-XX:+AggressiveOpts加快编译  
-XX:+UseBiasedLocking锁机制的性能改善  
-Xnoclassgc禁用垃圾回收  
-XX:SoftRefLRUPolicyMSPerMB每兆堆空闲空间中SoftReference的存活时间1ssoftly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap
-XX:PretenureSizeThreshold对象超过多大是直接在旧生代分配0单位字节 新生代采用Parallel Scavenge GC时无效
另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象.
-XX:TLABWasteTargetPercentTLAB占eden区的百分比1% 
-XX:+CollectGen0FirstFullGC时是否先YGCfalse 

并行收集器相关参数

-XX:+UseParallelGCFull GC采用parallel MSC
(此项待验证)
 

选择垃圾收集器为并行收集器.此配置仅对年轻代有效.即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集.(此项待验证)

-XX:+UseParNewGC设置年轻代为并行收集 可与CMS收集同时使用
JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值
-XX:ParallelGCThreads并行收集器的线程数 此值最好配置与处理器数目相等 同样适用于CMS
-XX:+UseParallelOldGC年老代垃圾收集方式为并行收集(Parallel Compacting) 这个是JAVA 6出现的参数选项
-XX:MaxGCPauseMillis每次年轻代垃圾回收的最长时间(最大暂停时间) 如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值.
-XX:+UseAdaptiveSizePolicy自动选择年轻代区大小和相应的Survivor区比例 设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开.
-XX:GCTimeRatio设置垃圾回收时间占程序运行时间的百分比 公式为1/(1+n)
-XX:+ScavengeBeforeFullGCFull GC前调用YGCtrueDo young generation GC prior to a full GC. (Introduced in 1.4.1.)

CMS相关参数

-XX:+UseConcMarkSweepGC使用CMS内存收集 测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明.所以,此时年轻代大小最好用-Xmn设置.???
-XX:+AggressiveHeap  试图是使用大量的物理内存
长时间大内存使用的优化,能检查计算资源(内存, 处理器数量)
至少需要256MB内存
大量的CPU/内存, (在1.4.1在4CPU的机器上已经显示有提升)
-XX:CMSFullGCsBeforeCompaction多少次后进行内存压缩 由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生"碎片",使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理.
-XX:+CMSParallelRemarkEnabled降低标记停顿  
-XX+UseCMSCompactAtFullCollection在FULL GC的时候, 对年老代的压缩 CMS是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。
可能会影响性能,但是可以消除碎片
-XX:+UseCMSInitiatingOccupancyOnly使用手动定义初始化定义开始CMS收集 禁止hostspot自行触发CMS GC
-XX:CMSInitiatingOccupancyFraction=70使用cms作为垃圾回收
使用70%后开始CMS收集
92为了保证不出现promotion failed(见下面介绍)错误,该值的设置需要满足以下公式CMSInitiatingOccupancyFraction计算公式
-XX:CMSInitiatingPermOccupancyFraction设置Perm Gen使用到达多少比率时触发92 
-XX:+CMSIncrementalMode设置为增量模式 用于单CPU情况
-XX:+CMSClassUnloadingEnabled   

辅助信息

-XX:+PrintGC  

输出形式:

[GC 118250K->113543K(130112K), 0.0094143 secs]
[Full GC 121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails  

输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]
[GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

-XX:+PrintGCTimeStamps   
-XX:+PrintGC:PrintGCTimeStamps  可与-XX:+PrintGC -XX:+PrintGCDetails混合使用
输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
-XX:+PrintGCApplicationStoppedTime打印垃圾回收期间程序暂停的时间.可与上面混合使用 输出形式:Total time for which application threads were stopped: 0.0468229 seconds
-XX:+PrintGCApplicationConcurrentTime打印每次垃圾回收前,程序未中断的执行时间.可与上面混合使用 输出形式:Application time: 0.5291524 seconds
-XX:+PrintHeapAtGC打印GC前后的详细堆栈信息  
-Xloggc:filename把相关日志信息记录到文件以便分析.
与上面几个配合使用
  

-XX:+PrintClassHistogram

garbage collects before printing the histogram.  
-XX:+PrintTLAB查看TLAB空间的使用情况  
XX:+PrintTenuringDistribution查看每次minor GC后新的存活周期的阈值 

Desired survivor size 1048576 bytes, new threshold 7 (max 15)
new threshold 7即标识新的存活周期的阈值为7。


以上系个人理解,如果存在错误,欢迎大家指正。原创不易,转载请注明出处!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值