深入理解JVM-内存模型(jmm)和GC - 简书 (jianshu.com)

 

JVM内存模型


 

1.程序计数器Program Counter Register

为了线程切换可以恢复到正确执行位置,每个线程都需要一个私有的内存空间,相当于当前线程的指令行号指示器

又称指令计数器,位于CPU内部,是速度最快的存储区。

 

2.Java栈(虚拟机栈JVM Stacks)

线程私有,生命周期与线程相同。栈描述的是Java方法执行的内存模型。

每个方法被执行的时候都会创建一个栈帧,用于存储局部变量表,操作栈,动态链接,方法出口等信息。

每个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。

 

位于RAM。

Java虚拟机栈可能出现两种类型的异常

线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。比如无限递归

虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。

 

3.本地方法栈Native Method Stacks

本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++。我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。

 

4.堆Heap

堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的场景也需要同步机制。

所有对象实例及数组都要在堆上分配内存。

注意:它是所有线程共享的,它的目的是存放对象实例。同时它也是GC所管理的主要区域,因此常被称为GC堆,又由于现在收集器常使用分代算法,Java堆中还可以细分为新生代和老年代。

 

堆内存分配,又称为动态内存分配,比如用new申请的实例,生存期由我们决定,使用灵活,但问题也最多。

 

堆可能出现两种类型的异常

当前主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemory Error)

 

堆内存和栈内存的设置

-Xms 堆内存的初始大小,默认为物理内存的1/64。

-Xmx 堆内存的最大大小,默认为物理内存的1/4~1/2。

-Xmn 堆内新生代的大小。通过这个值也可以得到老年代的大小:-Xmx减去-Xmn。

-Xss 设置每个线程可使用的内存大小,即栈的大小。

如上参数设置不当,会导致fullgc频繁。

fullgc频繁的表现:CPU飙高、内存异常。也可能存在内存泄漏out of Memory。

 

常规做法: Xms=Xmx=物理内存的1/2。 Xmn=1/2Xmx

 

在固定物理内存下,减小Xss这个值能生成更多的线程,当然操作系统对一个进程内的线程数还是有限制的,不能无限生成。线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大。如果该值设置过大,就有影响到可创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。

 

5.方法区Method

方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。

 

用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。

 

全局变量、静态变量等在程序的整个运行期间都存在。

 

运行时常量池Runtime Constant Pool

是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。

内存溢出Out of Memory

两种内存溢出异常(注意内存溢出是error级别的

1.StackOverFlowError:当请求的栈深度大于虚拟机所允许的最大深度

2.OutOfMemoryError:虚拟机在扩展栈时无法申请到足够的内存空间[一般都能设置扩大]

 

Java常见的内存溢出:

Java.lang.OutOfMemoryError: Java heap space

Java应用程序创建的对象存放在这片区域,垃圾回收(Garbage Collection)也发生在这块区域。通常一些比较“重型”的操作可能会导致该异常,该错误是JVM 堆空间不足,此时只需要调整-Xms 和-Xmx

 

Java.lang.OutOfMemoryError: PermGen space

指内存的永久保存区域,乃是永久存储区设置太小,不能满足系统需要的大小,此时只需要调整-XX:PermSize 和-XX:MaxPermSize

 

Java.lang.OutOfMemoryError: GC overhead limit exceeded

指的是GC占用CPU太多的的堆空间,一般是应用程序在有限的内存上创建大量的临时对象或者弱引用对象

Java常用的内存查看工具

在JDK bin目录下jconsole.exe和jvisualvm.exe 可以很清楚的看出哪里的内存不足和内存峰值,从而依据实际情况设置内存大小。

 

常用的Java内存优化

避免大量使用new Boolean()

public static final Boolean TRUE = new Boolean(true);

public static final Boolean FALSE = new Boolean(false);

需要的时候只要取这两个变量就可以了,

比如:

ps.setBoolean("isClosed",Boolean.TRUE);

 

避免大量使用new Integer()

SUN SDK中对Integer的实例化进行了优化,Integer类缓存了-128到127这256个状态的Integer,如果使用 Integer.valueOf(int i),传入的int范围正好在此内,就返回静态实例。这样如果我们使用Integer.valueOf代替new Integer的话也将大大降低内存的占用。

 

用StringBuffer代替字符串相加

一个拼装SQL语句的方法中竟然最多构造了将近100个String实 例。

 

过滥使用哈希表

这在提高系统速度的同时也加大了系统的内存占用,特别是当缓存的资料比较多的时候。

使用操作系统中的缓存,比如ehcache、oscache等

 

尽量避免使用Static变量

类内私有常量可以用final来代替。

 

GC垃圾回收机制 Garbage Collection

Java应用的内存由虚拟机JVM自动动态分配和回收。

 

在Java内存模型中,程序计数器、虚拟机栈和本地方法栈,由线程而生,随线程而灭。其中栈中的栈帧随着方法的进入顺序的执行的入栈和出栈的操作,一个栈帧需要分配多少内存取决于具体的虚拟机实现并且在编译期间即确定下来,当方法或线程执行完毕后,内存就随着回收,因此无需关心。

 

而Java堆、方法区则不一样。方法区存放着类加载信息,但是一个接口中多个实现类需要的内存可能不太一样,一个方法中多个分支需要的内存也可能不一样(只有在运行期间才可知道这个方法创建了哪些对象没需要多少内存),这部分内存的分配和回收都是动态的,gc关注的也正是这部分的内存。

 

Java堆是GC回收的“重点区域”。堆中基本存放着所有对象实例,GC进行回收前,第一件事就是确认哪些对象存活,哪些死去[即不可能再被引用]。

 

无论什么GC都难以避免停顿,即使是g1也会在初始标记阶段发生,stw并不可怕,可以尽可能的减少停顿时间。

 

注意:System.gc()函数不保证JVM的垃圾收集器一定会执行。System.gc()函数不保证JVM的垃圾收集器一定会执行。

新生代和老年代

为了高效的回收,jvm将堆分为三个区域

1.新生代(Young Generation)NewSize和MaxNewSize分别可以控制年轻代的初始大小和最大的大小

2.老年代(Old Generation)

3.永久代(Permanent Generation)【1.8以后采用元空间,就不在堆中了】

 

判断对象是否存活算法

可达性分析算法

目前主流的商用语言[如java、c#]采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。

它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的。

可作为GC Roots的对象有四种

①虚拟机栈(栈桢中的本地变量表)中的引用的对象。

②方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中。

③方法区中的常量引用的对象,

④本地方法栈中JNI(native方法)引用的对象

 

垃圾收集算法

在JVM中,可达性分析算法帮我们解决了哪些对象可以回收的问题,而垃圾收集算法则关心怎么回收。

 

三大垃圾收集算法

1.标记/清除算法【最基础】

2.复制算法

3.标记/整理算法

JVM采用“分代收集算法”对不同区域采用不同的回收算法。

新生代采用复制算法。

老年代采用标记/清除算法或标记/整理算法。

 

垃圾收集器

如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是具体实现。JVM会结合针对不同的场景及用户的配置使用不同的收集器。

 

年轻代收集器

Serial、ParNew、Parallel Scavenge

老年代收集器

Serial Old、Parallel Old、CMS收集器

特殊收集器

G1收集器[新型,不在年轻、老年代范畴内]

 

Minor GC、Major GC、FULL GC、mixed GC

Minor GC

在年轻代Young space(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC, Minor GC只会清理年轻代.

 

Major GC

Major GC清理老年代(old GC),但是通常也可以指和Full GC是等价,因为收集老年代的时候往往也会伴随着升级年轻代,收集整个Java堆。所以有人问的时候,需问清楚它指的是Full GC还是old GC。

 

Full GC

full GC是对新生代、老年代、永久代【jdk1.8后没有这个概念了】统一的回收。

 

【知乎R大的回答:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)、元空间(1.8及以上)等所有部分的模式】

 

mixed GC【g1特有】

混合GC

 

收集整个young gen以及部分old gen的GC。只有G1有这个模式

 

查看GC日志

简单日志查看

要看得懂并理解GC,需要看懂GC日志。

 

这边我在idea上试了个小例子,需要在idea配置参数(-XX:+PrintGCDetails)。

Attachments: