大家好,我是oldou,这次文章介绍的是关于JVM的相关知识,学习来源还是观看B站狂神的视频学习、同时在网上查找了很多资料进行整理【文末有参考地址】,毕竟网上的学习资料很多很多,当然要好好的利用起来进行学习,文章内容可能整理得不是特别好的那种,但是看完绝对是有收获的,如果文中有不对的地方还请各位指正,在此感激不尽,如果本文对你有所帮助,希望点赞支持一下哈,谢谢各位!【关于JVM的整体图我整理好之后会发出一个链接】
目录
- 前言
- JVM的初识(了解即可)
-
- 定义
- 作用
- 工作原理
- JVM的体系结构(掌握)
- 类加载器(Class Loader)
-
- 类加载器的作用
- 类加载器的类别
- 类加载器之间的关系
- 类加载的过程图
- 双亲委派机制
-
- 什么是双亲委派机制?
- 源码分析
- 委派机制的流程图
- 双亲委派机制的作用
- 沙箱安全机制
-
- 什么是沙箱?
- Java中的安全模型
- 组成沙箱的基本组件
- Native关键字
- 方法区【Method Area】
- 理解一下栈【Stack】
- 堆【Heap】
-
- 什么是堆内存?
- 堆内存的特点是什么?
- New对象在堆中如何分配?
- 堆和栈的区别
- 堆内存的模型图
- 新生代
-
- 为什么堆要分代呢?
- 新生代的介绍
- 新生代中的GC
-
- 一个对象的一辈子
- 有关年轻代的JVM参数
- 老年代
- 永久区【转】
- 元空间【转】
- JVM垃圾回收流程
- 堆内存的调优
- OOM原因分析
- GC之引用计数法
- GC之复制算法
- GC之标记压缩清除算法
- GC总结
前言
- 请你谈谈对JVM的理解?Java8虚拟机和之前的变化更新有什么不一样?
- 什么是OOM?什么是栈溢出StackOverFlowError?怎么分析?
- JVM的常用调优参数有哪些?
- 内存快站如何抓取?怎么分析Dump文件?
- 谈谈你对JVM中的类加载器的认识
- …
一问到这些问题,说实话没学过JVM的同学一般都会大皱眉头,然后默默地…
不过也别灰心,遇见不会的就说明我们还有进步的空间,毕竟现在不会不代表我们以后不会,不会的我们可以去学,所以我们要努力不断的学习新的技术、新的知识,因为人生本来就需要不断的学习,下面我们开始进入正文吧。
JVM的初识(了解即可)
定义
JVM就是java虚拟机,它是一个虚构出来的计算机,可在实际的计算机上模拟各种计算机的功能。JVM有自己完善的硬件结构,例如处理器、堆栈和寄存器等,还具有相应的指令系统。
作用
- JVM是java字节码执行的引擎,还能优化java字节码,使之转化成效率更高的机器指令。
- JVM中类的装载是由类加载器和它的子类来实现的,类加载是java运行时一个重要的系统组件,负责在运行时查找和装入类文件的类。
- 不同的平台对应着不同的JVM,在执行字节码(class文件)时,JVM负责将每一条要执行的字节码送给解释器,解释器再将其翻译成特定平台换将的机器指令并执行,这样就实现了跨平台运行。
工作原理
JVM在整个JDK中处于最底层,负责与操作系统的交互。操作系统装入jvm是通过JDK中的java.exe来实现的,具体步骤如下:
- a、创建JVM装载环境和配置;
- b、装载jvm.dll;
- c、初始化jvm.dll;
- d、调用JNIEnv实例装载并处理class类;
- e、运行java程序
JVM的体系结构(掌握)
完整图
简略图
当我们运行一个Java代码的时候,会按照上图步骤依次进行,下面简单解释上图中的JVM运行时数据区域:
-
1、程序计数器:指向当前线程正在执行的字节码的地址,行号。线程私有,无GC;
-
2、Java栈:存储当前线程运行方法所需要的数据,指令,返回地址。线程私有,无GC;
-
3、本地方法栈:与Java栈相同,不同的是它存的是本地方法的数据。
-
4、方法区:存储类信息(字段方法的字节码,部分方法的构造器),常量、静态变量,JIT(即时编译的信息)。线程共享,无GC,非堆区;(
java.lang.OutOfMemoryError:PermGen space
)。 -
5、堆-heap:存储类实例,一个JVM实例只有一个堆内存,线程共享,需要GC。;
-
6、JNI【Java Native Interface】(Java本地方法接口)
凡是带了native关键字的方法就会进入到本地方法栈,其他的就是进入Java栈; -
7、 Navite Interface 本地接口
本地接口的作用就是融合不同的编程语言为Java所用,它的初衷就是融合C/C++程序,Java刚诞生的时候是C/C++横行的时候,那个时候想要立足就必须由调用C/C++的程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法就是再Nativa Method Stack中登记native方法,再(Execution Engine)执行引擎执行的时候加载Native Libraies。
目前该方法的使用越来越少了,除非是与硬件相关的应用,使用Java玩嵌入式等等。由于限制的异构领域间通信很发达,可以使用Socket通信等等。 -
8、Native Method Stack 本地方法栈
它的具体做法就是Native Method Stack中登记native方法,在(Execution Engine)执行引擎执行的时候加载Native Libraies【本地库】。
各个版本之间的区别
- JDK1.6以及之前:有永久代,字符串常量池和运行时常量池都在方法区;
- JDK1.7:有永久代,但已经逐步“去永久代”,字符串常量池移到堆中,运行时常量池还在方法区中(永久带);
- JDK1.8之后:无永久代,字符串常量池在堆中,运行时常量池在元空间;
类加载器(Class Loader)
类加载器的作用
类加载器,顾名思义就是用来加载类的,但是它的作用不仅仅用于加载类,因为对于任意一个类都需要加载它的类加载器和这个类本身以此确立它在Java虚拟机中的唯一性,而每一个类加载器都拥有一个独立的类名称空间。
说直白点,类加载器的作用就是比较两个类是否“相等”,只有它们是由同一个类加载器加载的时候才有意义,对于同一个类,如果由不同的类加载器加载,那么它们必然不想等。(相等包括Class对象的equals方法、isAssignableFrom()方法、isInstance()方法返回的结果,也包括用instanceof关键词判断的情况)。
类加载器的类别
(1)BootstrapClassLoader(启动类加载器,又名根加载器)
由C++
编写,用于加载Java核心库 java.*
(例如:java.lang.*
),构造ExtClassLoader(扩展类加载器)
和AppClassLoader(系统类加载器)
。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许程序员直接通过引用进行操作。
(2)ExtClassLoader(标准扩展类加载器)
该类加载器由Java
编写,用于加载扩展类库,主要负责加载【jre/lib/ext
】目录下的一些扩展的jar,例如classpath中的jre
,javax.*
或者java.ext.dir指定位置的类,开发者可以直接使用扩展类加载器。
(3)AppClassLoader(系统类加载器)
由Java
编写,主要负责加载应用程序的主函数类,加载程序所在的目录,例如user.dir
所在的位置的class
。
(4)CustomClassLoader(用户自定义类加载器)
由Java
编写,用户自定义的类加载器,可加载指定路径的class
文件。
开发者角度的类加载器位置
-
根类加载器,加载位于/jre/lib目录中的或者被参数
-Xbootclasspath
所指定的目录下的核心Java类库。此类加载器是Java虚拟机的一部分,使用native
代码(C++)
编写。如图所示,rt.jar
这个jar包就是Bootstrap根类加载器
负责加载的,其中包含了java各种核心的类如java.lang
、java.io
、java.util
、java.sql
等 -
扩展类加载器,加载位于
/jre/lib/ext
目录中的或者java.ext.dirs
系统变量所指定的目录下的拓展类库。此加载器由sun.misc.Launcher$ExtClassLoader
实现。 -
系统类加载器,加载用户路径(ClassPath)上所指定的类库。此加载器由
sun.misc.Launcher$AppClassLoader
实现。
类加载器之间的关系
图中的层次关系,称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的根类加载器以外,其余的类加载器都应该有自己的父类加载器(一般不是以继承实现,而是使用组合关系来复用父加载器的代码)。
如果一个类收到类加载请求,它首先请求父类加载器去加载这个类,只有当父类加载器无法完成加载时(其目录搜索范围内没找到需要的类),子类加载器才会自己去加载。
类加载的过程图
双亲委派机制
什么是双亲委派机制?
当某个类加载器需要加载某个.class
文件的时候,这个类加载器会首先将这个任务委托给它的上级类加载器(父级加载器),递归这个操作,如果上级的类加载器没有进行加载,那么这个类加载器才会自己去加载这个.class
文件。
源码分析
【我们在java.lang
包中首先找到ClassLoader
,然后打开ClassLoader
类,Ctrl+F
搜索loadClass
方法,下面为该方法的源码】
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查这个class是否已经被加载过了
Class<?> c = findLoadedClass(name);
//如果 c==null就表示该class没有被加载
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果有父类的加载器就将该class委托父类加载器进行加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空,则说明递归到bootStrapClassloader根加载器了
//bootStrapClassloader比较特殊,无法通过get获取
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) { }
//如果 c==null就表示该class在父加载器那边没有被加载
if (c == null) {
//如果bootstrapClassLoader 仍然没有加载过,则会一层一层的递归回来,并且尝试自己去加载这个class
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
这里需要注意的一点就是long t0 = System.nanoTime();
这个的源码public static native long nanoTime();
中的native
,使用这个关键字声明的方法表示告知JVM调用,该方法在外部定义,可能使用C/C++去实现了,这个关键字详细的解释可以百度去查一下,这里不做过多的介绍。下面使用流程图来解释一下源码中的流程。
委派机制的流程图
【这里我画的那张图太大了不好截图,引用了别人的图,图地址我放在文末了。后面我会将我自己画的整个JVM的图分享出来。】
从上图中我们就更容易理解了,当一个.class
这样的文件要被加载时。不考虑我们自定义类加载器的话,首先就会在AppClassLoader
中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass
方法。父类中同理会先检查自己是否已经加载过,如果没有再往上。注意这个过程,知道到达Bootstrap classLoader
之前,都是没有哪个加载器自己选择加载的。如果父加载器无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException
。
双亲委派机制的作用
- 保证数据安全,能够防止重复加载同一个.class文件。通过往父类加载器去委托,如果已经加载过了那么就不用再加载一遍;
- 保证核心 .class不能被篡改。通过委托方式,不会去篡改核心 .class,即使篡改了也不会去加载,即使加载也不会是同一个 .class对象了。不同的加载器加载同一个 .class也不是同一个 Class对象。这样保证了 Class执行安全。
沙箱安全机制
什么是沙箱?
Java安全模型的核心就是Java沙箱(sandbox),那么什么是沙箱呢?沙箱就是限制程序运行的环境,沙箱机制就是Java代码限定JVM虚拟机特定的运行范围中,并且严格限制代码对本地系统资源的访问,通过这样的措施来保证对代码的有效隔离,以防止对本地系统造成破坏。
沙箱主要限制系统资源的访问,那系统资源包括哪些呢?----CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也会不一样。但所有的Java程序运行都可以指定沙箱,可以定制安全策略。
Java中的安全模型
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱 (Sandbox
) 机制。如下图所示
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的Java1.1
版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。如下图所示
在 Java1.2 版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示
当前最新的安全机制实现,则引入了域 (Domain
) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain
),对应不一样的权限 (Permission
)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示
以上提到的都是基本的 Java 安全模型概念,在应用开发中还有一些关于安全的复杂用法,其中最常用到的 API 就是doPrivileged
。doPrivileged
方法能够使一段受信任代码获得更大的权限,甚至比调用它的应用程序还要多,可做到临时访问更多的资源。有时候这是非常必要的,可以应付一些特殊的应用场景。例如,应用程序可能无法直接访问某些系统资源,但这样的应用程序必须得到这些资源才能够完成功能。
组成沙箱的基本组件
(1) 字节码校验器(bytecode verifler
):确保Java类文件遵循Java语言规范。这样可以帮助到Java程序实现内存保护。但并不是所有的类都会经过字节码校验,比如核心类。
(2)类加载器(Class Loader
):其中类加载器在以下三个方面对Java沙箱起作用
- 它防止恶意代码去干涉善意代码;(使用了双亲委派机制)
- 它守护了被信任的类库边界;
- 它将代码归入到保护域,确定了代码可以进行哪些操作。
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。
类装载器采用的机制是双亲委派模式。
- 1、从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
- 2、由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
(3)存取控制器(access controller):存取控制器可以控制和讯API对操作系统的存取权限,而这个控制的策略设定可以由用于指定。
(4)安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
(5)软件安全包(security package):Java.security下的类和扩展包下的类,运行用户为自己的应用增加新的安全也行,包括以下:
- 安全提供者
- 消息摘要
- 数字签名
- 加密
- 鉴别
Native关键字
我们在源码中经常看见Native
这个关键字,例如我们最常用的线程方法,
public static void main(String[] args) {
new Thread(()->{
},"myThread").start();
}
我们点进去这个start()
方法,在该源码中我们发现这个start0()
方法
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
我们找到start0()
的定义发现竟然是private native void start0();
这样的,很惊奇,这个native
干了啥呢?仅仅就是这样声明了一下就完事了,它到底是弄了什么操作呢?是不是有很多的问号,没关系我们来学习一下。、
首先来看一下这个图【注意标红的地方】:
-
凡是使用了
native
关键字修饰的,就说明Java的作用范围已经达不到了,这个时候就会去调用底层的C语言库。 -
凡是带了
native
关键字的会进入本地方法栈:当类加载进来的时候,将堆、栈内存分配好之后就会进入到本地方法栈调用start0()
,而本地方法栈里的东西Java范围是作用不到的,那么本地方法栈就会调用本地方法接口【JNI:Java Native Interface
,作用写在下面】,通过JNI加载本地方法库中的方法去执行操作。 -
【JNI的作用】:扩展
Java
的使用,它可以融合不同的编程语言为Java
所用,最初是C
、C++
,后续添加了其他语言。原因就是:Java刚诞生的时候C
语言和C++
那个时候超级流行,而想要立足的话就必须要有调用C
、C++
的程序,于是就会在内存区域中专门开辟一个标记区域【Navicat Method Stack
】用于登记native方法【只是登记而不用执行】,并且在最终执行的时候通过JNI
加载本地方法库中的方法。
注意:本地方法栈、本地方法接口还有本地方法库的介绍在上面JVM体系结构部分已经解释,请往上查看。
方法区【Method Area】
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量【static】、常量【final】、类信息(构造方法、接口定义)【Class】、运行时的常量池存在方法区中【常量池】,但是实例变量存在堆内存中,和方法区无关 。
举例子,根据代码简单画一下内存图【对象刚加载的时候是什么样子的】:
public class Test {
private int a;
private String name="oldou";
public static void main(String[] args) {
Test test = new Test();
test.a=1;
System.out.println(test.a+"\t"+test.name);
}
}
理解一下栈【Stack】
注意:概念也许对你来说有些枯燥,但是当你真正沉下心看并且弄懂的时候,真的很有趣。下面的每句话基本上都需要理解。
栈(stack
)又名堆栈,它是一种数据结构【运算受限的线性表】。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。如下图所示:
-
首先得搞清楚,栈的意思相当于存储货物或者供旅客住宿的地方,就是指数据暂时存储的地方,因此才会由出栈、进栈的说法。
-
栈作为一种数据结构,是一种只能在一端进行插入【
push
】和删除操作【pop
】的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。栈具有记忆作用,对栈的插入与删除操作中,不需要改变栈底指针。 -
每一个执行的方法都会产生一个栈帧【以上就有两个栈帧:方法A、方法B】,程序正在执行的方法一定在栈的顶部,方法执行完之后就会被弹出栈,直到全部执行完毕。栈的执行原理本来就是先进后出,后进先出,先进入的方法就会被后进的方法压住【又名:压栈】。栈就像一个桶一样,如果栈堆满了就会抛出错误 【
StackOverflowError
】。 -
栈是允许在同一端进行插入和删除操作的特殊线性表。允许进行插入和删除操作的一端称为栈顶(
top
),另一端为栈底(bottom
);栈底固定,而栈顶浮动;栈中元素个数为零时称为空栈。插入一般称为进栈(PUSH
),删除则称为退栈(POP
)。栈也称为先进后出表。与栈类似的队列遵循FIFO
原则【First Input First Output
,先进先出】。 -
这就是为什么我们程序中的
main()
方法先执行,结果最后结束的原因,因为main()
方法入栈后被压在栈低,上面的方法栈帧执行一个就POP一个最后main()
出栈后才结束main()
方法。 -
栈内存主管着程序的运行,生命周期以及线程同步,线程结束后,栈内存就会释放,所以对于栈来说,不存在垃圾回收问题,因为一旦线程结束了,栈就Over了。
栈中主要存放一些基本类型的变量(byte、short、int、long、float、double、boolean、char)、对象引用、实例的方法。
【注】关于栈的代码实现之类的我就不放了,网上很多都有,后面再进行整理。
堆【Heap】
什么是堆内存?
堆内存是是Java内存中的一种,它的作用是用于存储引用类型,当new实例化得到一个引用变量【对象或者数组】的时候,java虚拟机会在堆内存中开辟一个不一定是连续的空间分配给该实例,根据零散的内存地址,实则是根据哈希算法生成一长串数字指向该实例的物理地址,相当于门牌号起到标识作用。当引用丢失了,会被垃圾回收机制回收,但不是立马释放堆内存。
堆是一种数据结构,它是存储的单位,一个JVM只有一个堆内存,并且堆内存的大小是可以调节的。
- 堆中存储的全部是对象实例,每个对象都包含一个与之对应的class的信息(class信息存放在方法区)。
- jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身,几乎所有的对象实例和数组都在堆中分配。
堆内存的特点是什么?
堆内存的特点就是:
- 堆内存可以看作是一个管道,FIFO【先进先出,后进后出】。
- 堆可以动态地分配内存大小,生存期不需要事先告诉编译器,缺点是由于要在运行时动态的分配内存,所以存取的速度慢。
New对象在堆中如何分配?
由Java虚拟机的自动垃圾回收器来进行管理
堆和栈的区别
-
存放的东西不同:堆内存用于存放由new创建的对象或者数组,栈内存用于对象引用和基本数据类型等等;
-
存储数据的原则不一样:堆遵循FIFO原则【先进先出,后进后出】,而栈是先进后出,后进先出;
-
当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
-
在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。
堆内存的模型图
Java堆主要用于存放各种类的实例对象和数组,它是垃圾收集器管理的主要区域,因此很多时候被称为“GC堆”
,在Java中堆被分为两个区域:新生代和老年代,【注意这里不包括元空间(方法区),元空间原来叫永久代,JDK1.8之后改名为元空间】。
下面就开始逐一的介绍一下新生代、老年代以及元空间。
新生代
为什么堆要分代呢?
为什么要给堆分代呢?当然咯,不分代也是可以的,只是分代的话可以优化GC性能,假如不分代的话,那我们创建的所有对象都放在一块,当需要垃圾回收的时候我们需要去找哪些对象没用,这样就会对整个堆区域进行全面扫描,这样耗性能啊,如果分带的话,将新创建的对象放在某一个区域,当需要GC的时候就去扫描回收,多方便是不是。
新生代的介绍
新生代主要用于存储新生的对象,一般需要占据堆的1/3的空间,由于频繁创建对象,所以新生代会频繁的触发Minor GC
进行垃圾回收。
新生代有分为Eden区
【伊甸园区】、SurvivorFrom
、SurvivorTo
三个区域,下面依次介绍一下:
Enden
区:是Java新对象的出生地(如果新创建的对象占用内存很大,则会被直接分配到老年代),当Eden区内存不够的时候就会出发Minor GC,对新生代区进行一次垃圾回收。SurvivorTo
:保留了一次Minor GC过程中的幸存者;SurvivorFrom
:上一次GC的幸存者,作为这一次GC的被扫描者;
新生代中的GC
HotSpot JVM把年轻代分为了三部分:1个Eden
区【伊甸园区】和2个Survivor
区【幸存区,分别是From、To
】,默认的比例为8:1,一般情况下,新创建的对象都会被分配到Eden
区,这些对象再经过第一次的Minor GC
后,如果还存活就会被转移到Survivor
区。对象在Survivor
区中每熬过一次Minor GC
,年龄就会被增长一次,当它们的年龄达到一次岁数的时候就会被移到老年代中。
因为新生代中的对象基本上都是朝生夕死(80%
以上),所以在新生代中的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只使用其中的一块,当这一块用完之后就将还或者的对象复制到另一块上面,复制算法并不会产生内存碎片。
在GC开始的时候,对象只会存在于Eden
区和名为“From
”的Survivor
区,Survivor
区“To”
是空的。紧接着进行GC,Eden
区中所有存活的对象都会被复制到“To”
,而在“From”
区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold
来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden
区和From
区已经被清空。这个时候,“From”
和“To”
会交换他们的角色,也就是新的“To”
就是上次GC前的“From”
,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To
的Survivor
区域是空的。Minor GC
会一直重复这样的过程,直到“To”
区被填满,“To”
区被填满之后,会将所有对象移动到年老代中。
一个对象的一辈子
我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了老年代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在老年代里,我生活了20年(每次GC加一岁),然后被回收。
【文章地址附在文末,搜了很多篇进行学习,感觉这个看着懂一些,描述得感觉蛮到位的…】
有关年轻代的JVM参数
-
(1)
-XX:NewSize和-XX:MaxNewSize
:用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。 -
(2)
-XX:SurvivorRatio
:用于设置Eden和其中一个Survivor的比值,这个值也比较重要。 -
(3)
-XX:+PrintTenuringDistribution
:这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。 -
(4)
-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold
:用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。
老年代
老年代用于存放新生代中经过多次垃圾回收仍然存活的对象。在老年代的对象都比较稳定,因此MajorGC不会频繁执行,而在进行MajorGC之前一般都会进行一次MinorGC,使得新生代的对象晋入老年代,一般是空间不够用时才触发,当无法找到足够大的连续空间分配给新创建的大对象的时候也会触发一次MajorGC进行垃圾回收腾出空间。
- MajorGC采用标记-清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。
- MajorGC的耗时比较长,因为要先扫描然后再回收。
- MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。
- 当老年代也满了装不下的时候,就会抛出OOM(
Out of Memory
)异常。
下面来测试一下,模拟一下这个OOM错误:
测试代码:
public class hello {
public static void main(String[] args) {
String str = "hello world";
while (true){ //死循环
//通过不断的产生新对象,然后堆内存溢出
str = str + new Random().nextInt(888888888)
+ new Random().nextInt(999999999);
}
}
}
运行报错:
关于OOM的问题,后面再稍微详细的说明,这里不做过多的介绍。
永久区【转】
永久代,指的是内存的永久保存区域,主要用于存放Class和Meta(元数据)的信息,Class在被加载的时候放入到永久区域,它和存放实例的区域不一样,GC不会在主程序运行期间对永久代区域进行清理,所以也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
元空间【转】
其实,移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap
或者是 Native Heap
。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols
)转移到了native heap
;字面量(interned strings)
转移到了java heap
;类的静态变量(class statics
)转移到了java heap
。
我们还是通过以上模拟OOM错误分别在JDK1.6、JDK1.7、JDK1.8中运行:
JDK 1.6 的运行结果:
JDK 1.7的运行结果:
JDK 1.8的运行结果:
从上述结果可以看出,JDK 1.6下,会出现“PermGen Space”的内存溢出,而在 JDK 1.7和 JDK 1.8 中,会出现堆内存溢出,并且 JDK 1.8中 PermSize 和 MaxPermGen 已经无效。因此,可以大致验证 JDK 1.7 和 1.8 将字符串常量由永久代转移到堆中,并且 JDK 1.8 中已经不存在永久代的结论。现在我们看看元空间到底是一个什么东西?
结论:
元空间的本质和永久代类似,都是对JVM规范中方法区的实现,不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory、字符串池和类的静态变量放入java堆中.。这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。
元空间的大小可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize
,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize
时,适当提高该值。-XX:MaxMetaspaceSize
,最大空间,默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio
,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集-XX:MaxMetaspaceFreeRatio
,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
【注:以上图片文字摘自于:https://www.cnblogs.com/paddix/p/5309550.html】
后续更新中…
JVM垃圾回收流程
堆内存的调优
OOM原因分析
GC之引用计数法
GC之复制算法
GC之标记压缩清除算法
GC总结
类加载参考:https://zhuanlan.zhihu.com/p/44670213
双亲委派机制参考:https://www.jianshu.com/p/1e4011617650
沙箱安全机制参考:https://www.cnblogs.com/MyStringIsNotNull/p/8268351.html
栈参考:https://baike.baidu.com/item/%E6%A0%88/12808149?fr=aladdin
新生代参考:http://ifeve.com/jvm-yong-generation/
永久代参考:https://www.cnblogs.com/paddix/p/5309550.html