JVM内存管理机制和垃圾回收机制(1)

JVM内存管理机制

Jvm内存管理机制

java中是把内存的管理交给java虚拟机来管的,有java虚拟机中的垃圾回收机制来清理内存

Java虚拟机(Java virtualmachine)实现了Java语言最重要的特征:即平台无关性。

平台无关性原理:编译后的 Java程序(.class文件)由 JVM执行。JVM屏蔽了与具体平台相关的信息,使程序可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。因此实现Java平台无关性。

本文主要介绍JVM中的架构知识,转载请注明出处:http://blog.****.net/seu_calvin/article/details/51404589

1. JVM结构图

JVM内存管理机制和垃圾回收机制(1)

 

 

 

JVM = 类加载器 classloader+ 执行引擎 executionengine + 运行时数据区域 runtime data area

首先Java源代码文件被Java编译器编译为字节码文件,然后JVM中的类加载器加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM中的运行时数据区(内存)会用来存储程序执行期间需要用到的数据和相关信息。

因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。

2. ClassLoader

classloader把硬盘上的class文件加载到JVM中的运行时数据区域,但是它不负责这个类文件能否执行,而这个是执行引擎负责的。

3. 执行引擎 

作用:执行字节码,或者执行本地方法。

4. Runtime DataArea

 JVM在运行期间,在运行时数据区对JVM内存空间的划分和分配,划分为了以下几个区域来存储。

JVM内存管理机制和垃圾回收机制(1)

(图注:JDK1.7已经把常量池转移到堆里面了!)

PC计数器(The pc Register)

(1)每一个Java线程都有一个PC寄存器,用以记录比如在线程切换回来后恢复到正确的执行位置。

(2)如该线程正在执行一个Java方法,则计数器记录的是正在执行的虚拟机字节码地址,如执行native方法,则计数器值为空。

(3)此内存区域是唯一一个在JVM中没有规定任何OutOfMemoryError情况的区域。

JVM栈(Java Virtual MachineStacks)

(1)JVM栈是线程私有的,并且生命周期与线程相同。并且当线程运行完毕后,相应内存也就被自动回收。

(2)栈里面存放的元素叫栈帧,每个方法从调用到执行结束,其实是对应一个栈帧的入栈和出栈。

栈帧用于存储执行方法时的一些数据,如局部变量表、操作数栈(执行引擎计算时需要),方法出口等等。

(3)这个区域可能有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出*Error异常(如:将一个函数反复递归自己,最终会出现这种异常)。如果JVM栈可以动态扩展(大部分JVM是可以的),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。

本地方法栈(Native Method Stacks)

(1)本地方法栈与虚拟机栈所发挥的作用很相似,他们的区别在于虚拟机栈为执行Java代码方法服务,而本地方法栈是为Native方法服务。

(2)和JVM栈一样,这个区域也会抛出*Error和OutOfMemoryError异常。

方法区(Method Area)

(1)方法区域是全局共享的,比如每个线程都可以访问同一个类的静态变量。在方法区中,存储了已被JVM加载的类的信息、静态变量、编译器编译后的代码等。如,当程序中通过getName、isInterface等方法来获取信息时,这些数据来源于方法区。

(2)由于使用反射机制的原因,虚拟机很难推测哪个类信息不再使用,因此这块区域的回收很难!另外,对这块区域主要是针对常量池回收,值得注意的是JDK1.7已经把常量池转移到堆里面了。

(3)同样,当方法区无法满足内存分配需求时,会抛出OutOfMemoryError。

运行时常量池(Runtime Constant Pool)

(1)存放类中固定的常量信息、方法引用信息等,其空间从方法区域(JDK1.7后为堆空间)中分配。

(2)Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有就是常量表,用于存放编译期已可知的常量,这部分内容将在类加载后进入方法区(永久代)存放。但是Java语言并不要求常量一定只有编译期预置入Class的常量表的内容才能进入方法区常量池,运行期间也可将新内容放入常量池(最典型的String.intern()方法)。

(3)当常量池无法在申请到内存时会抛出OutOfMemoryError异常.

Java堆

(1)Java堆是JVM所管理的最大的一块内存。它是被所有线程共享的一块内存区域,在虚拟机启动时创建。

(2)几乎所有的实例对象都是在这块区域中存放。(JIT编译器貌似不是这样的)。

(3)Java堆是垃圾收集管理的主要战场。所有Java堆可以细分为:新生代和老年代。再细致分就是把新生代分为:Eden空间、FromSurvivor空间、To Survivor空间。

(4)根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

5. 堆和栈的区别

这是一个非常常见的面试题,主要从以下几个方面来回答。

(1)各司其职

最主要的区别就是栈内存用来存储局部变量和方法调用信息。
而堆内存用来存储Java中的对象。无论是成员变量、局部变量还是类变量,它们指向的对象都存储在堆内存中。

(2)空间大小

栈的内存要远远小于堆内存,如果你使用递归的话,那么你的栈很快就会充满并产生*Error。

(3)独有还是共享

栈内存归属于线程的私有内存,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见。
而堆内存中的对象对所有线程可见,可以被所有线程访问。

(4)异常错误

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出*Error异常。

如果JVM栈可以动态扩展(大部分JVM是可以的),当扩展时无法申请到足够内存则抛出OutOfMemoryError异常。

而堆内存没有可用的空间存储生成的对象,JVM会抛出java.lang.OutOfMemoryError。

JVM内存管理机制和垃圾回收机制(1)

虚拟机对象探秘

对象的创建

1.检查 

  虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程

2.分配内存 

  接下来将为新生对象分配内存,对象所需内存在类加载完毕之后就可以完全确定,为对象分配内存空间的任务等同于把一块确定的大小的内存从Java堆中划分出来。

  假设Java堆中内存是绝对规整的,所有用过的内存放在一遍,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针指向空闲空间那边挪动一段与对象大小相等的距离,这个分配方式叫做“指针碰撞”

  如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式成为“空闲列表

  选择那种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

  在分配内存的时候会出现并发的问题,比如在给A对象分配内存的时候,指针还没有来得及修改,对象B又同时使用了原来的指针进行了内存的分片。

  有两个解决方案:

  1、对分配的内存进行同步处理:CAS配上失败重试的方式保证更新操作的原子性

  2、把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中分配一块小内存,称为本地缓冲区,那个线程需要分配内存,就需要在本地缓冲区上进行,只有当缓冲区用完并分配新的缓冲区的时候,才需要同步锁定,

3. Init

  执行new指令之后会接着执行Init方法,进行初始化,这样一个对象才算产生出来

对象的内存布局

在HotSpot虚拟机中,对象在内存中储存的布局可以分为3块区域:对象头、实例数据和对齐填充

  对象头包括两部分

  a) 储存对象自身的运行时数据,如哈希码、GC分带年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳

  b) 另一部分是指类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例(不是所有的虚拟机都必须在对象数据上保留类型指针的)

        java数组的话,还得有一个用于记录数组长度的数据。

  实例数据:

  是对象正常储存的有效信息,也是程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录下来。

  对齐填充:

  不是必然存在的,仅仅是起到占位符的作用。对象的大小必须是8字节的整数倍,而对象头刚好是8字节的整数倍(1倍或者2倍),当实例数据没有对齐的时候,就需要通过对齐填充来补全

对象的访问定位

  1、使用句柄访问

  Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址

  优势:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改

JVM内存管理机制和垃圾回收机制(1)

2、使用直接指针访问

  Java堆对象的布局就必须考虑如何访问类型数据的相关信息,而refreence中存储的直接就是对象的地址

     优势:速度更快,节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本

JVM内存管理机制和垃圾回收机制(1)

OutOfMemoryError 异常(OOM)

1.

Java堆溢出

  Java堆用于存储对象实例,只要不断的创建对象,并且保证GCRoots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在数量到达最大堆的容量限制后就会产生内存溢出异常

如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置

如果不存在泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗

   产生了堆上的内存溢出以后,可以通过内存映像分析工具来分析通过dump下来的内存堆转储快照来分析具体原因,看一下是内存泄露还是内存溢出。

-Xmx 设置堆最大的长度

-Xms设置堆最小的长度

2.

虚拟机栈和本地方法栈溢出

  对于HotSpot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:

  如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出*Error

  如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

  在单线程下,无论由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是*Error异常

  如果是多线程导致的内存溢出,与栈空间是否足够大并不存在任何联系,这个时候每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。解决的时候是在不能减少线程数或更换64为的虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程

3.

方法区和运行时常量池溢出

String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用

  由于常量池分配在永久代中,可以通过-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量。

Intern():

  JDK1.6 intern方法会把首次遇到的字符串实例复制到永久代,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是一个引用

  JDK1.7 intern()方法的实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个

4.本地直接内存内存溢出

-XX:MaxDirectMemorySize制定直接内存大小,如果不指定的话,和堆最大值一样。

由直接内存抛出的内存溢出很显著的一点是dump文件不会看见明显的异常。(如果oom之后,dump文件很小,且有用到NIO,则可以考虑一下是直接内存溢出)

参考博客:http://www.cnblogs.com/prayers/p/5515245.html

JDK8废弃永久代

JDK8 永久代变化如下图:

JVM内存管理机制和垃圾回收机制(1)

1.新生代:Eden+From Survivor+To Survivor

2.老年代:OldGen

3.永久代(方法区的实现) : PermGen----->替换为Metaspace(本地内存中)

 

 二、为什么废弃永久代(PermGen)

 2.1 官方说明

参照JEP122:http://openjdk.java.net/jeps/122,原文截取:

Motivation

This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.

 即:移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。

 2.2 现实使用中易出问题

由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen

三、深入理解元空间(Metaspace)

3.1元空间的内存大小

元空间是方法区的在HotSpot jvm 中的实现,方法区主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。,理论上取决于32位/64位系统可虚拟的内存大小。可见也不是无限制的,需要配置参数。

3.2常用配置参数

1.MetaspaceSize

初始化的Metaspace大小,控制元空间发生GC的阈值。GC后,动态增加或降低MetaspaceSize。在默认情况下,这个值大小根据不同的平台在12M到20M浮动。使用Java -XX:+PrintFlagsInitial命令查看本机的初始化参数

2.MaxMetaspaceSize

限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。在本机上该参数的默认值为4294967295B(大约4096MB)。

3.MinMetaspaceFreeRatio

当进行过Metaspace GC之后,会计算当前Metaspace的空闲空间比,如果空闲比小于这个参数(即实际非空闲占比过大,内存不够用),那么虚拟机将增长Metaspace的大小。默认值为40,也就是40%。设置该参数可以控制Metaspace的增长的速度,太小的值会导致Metaspace增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致Metaspace增长的过快,浪费内存。

4.MaxMetasaceFreeRatio

当进行过Metaspace GC之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会释放Metaspace的部分空间。默认值为70,也就是70%。

5.MaxMetaspaceExpansion

Metaspace增长时的最大幅度。在本机上该参数的默认值为5452592B(大约为5MB)。

6.MinMetaspaceExpansion

Metaspace增长时的最小幅度。在本机上该参数的默认值为340784B(大约330KB为)。

3.3测试并追踪元空间大小

 3.3.1.测试字符串常量

JVM内存管理机制和垃圾回收机制(1)

 1 public class StringOomMock {
 2     static String  base = "string";
 3     
 4     public static void main(String[] args) {
 5         List<String> list = new ArrayList<String>();
 6         for (int i=0;i< Integer.MAX_VALUE;i++){
 7             String str = base + base;
 8             base = str;
 9             list.add(str.intern());
10         }
11     }
12 }

JVM内存管理机制和垃圾回收机制(1)

在eclipse中选中类--》run configuration-->java application--》new 参数如下:

JVM内存管理机制和垃圾回收机制(1)

 由于设定了最大内存20M,很快就溢出,如下图:

JVM内存管理机制和垃圾回收机制(1)

 可见在jdk8中:

1.字符串常量由永久代转移到堆中。

2.持久代已不存在,PermSize MaxPermSize参数已移除。(看图中最后两行)

3.3.2.测试元空间溢出

根据定义,我们以加载类来测试元空间溢出,代码如下:

 

JVM内存管理机制和垃圾回收机制(1)

 1 package jdk8;
 2 
 3 import java.io.File;
 4 import java.lang.management.ClassLoadingMXBean;
 5 import java.lang.management.ManagementFactory;
 6 import java.net.URL;
 7 import java.net.URLClassLoader;
 8 import java.util.ArrayList;
 9 import java.util.List;
10 
11 /**
12  * 
13  * @ClassName:OOMTest
14  * @Description:模拟类加载溢出(元空间oom)
15  * @author diandian.zhang
16  * @date 2017年4月27日上午9:45:40
17  */
18 public class OOMTest {  
19     public static void main(String[] args) {  
20         try {  
21             //准备url  
22             URL url = new File("D:/58workplace/11study/src/main/java/jdk8").toURI().toURL();  
23             URL[] urls = {url};  
24             //获取有关类型加载的JMX接口  
25             ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();  
26             //用于缓存类加载器  
27             List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();  
28             while (true) {  
29                 //加载类型并缓存类加载器实例  
30                 ClassLoader classLoader = new URLClassLoader(urls);  
31                 classLoaders.add(classLoader);  
32                 classLoader.loadClass("ClassA");  
33                 //显示数量信息(共加载过的类型数目,当前还有效的类型数目,已经被卸载的类型数目)  
34                 System.out.println("total: " + loadingBean.getTotalLoadedClassCount());  
35                 System.out.println("active: " + loadingBean.getLoadedClassCount());  
36                 System.out.println("unloaded: " + loadingBean.getUnloadedClassCount());  
37             }  
38         } catch (Exception e) {  
39             e.printStackTrace();  
40         }  
41     }  
42 }  

JVM内存管理机制和垃圾回收机制(1)

 

为了快速溢出,设置参数:-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=80m,运行结果如下:

JVM内存管理机制和垃圾回收机制(1)

 

 上图证实了,我们的JDK8中类加载(方法区的功能)已经不在永久代PerGem中了,而是Metaspace中。可以配合JVisualVM来看,更直观一些。

总结废弃原因:

1、字符串存在永久代中,容易出现性能问题和内存溢出。

  2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

  3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

  4、Oracle 可能会将HotSpot 与 JRockit 合二为一。

https://blog.****.net/guhong5153/article/details/70196354