一篇带你立马搞定jvm内存,类加载机制全过程,java内存模型,分代垃圾回收机制,垃圾回收算法和垃圾收集器 这篇文章有点长哦,希望你耐着性子看完,然后会有所收获!!!!
通过以下思维导图,进而对于Java虚拟机诞生有一个整体的认识。从上世纪90年代SUN公司开发Green项目,基于所处的软硬件环境,面临当时的困局,针对问题,进而创造出了Java语言和其运行的环境JVM。有了简易清晰的整体认识之后,如果有兴趣,可以自己搜索更多的信息了解。
思维导图的作用,就是为了帮助大家简易理解Java诞生的背景。
jvm内存分析
运行时数据区分为几个部分
类加载机制全过程
java内存模型
分代垃圾回收机制
垃圾回收算法
GC(垃圾收集器)
GC的执行机制
其他
废话不多说,直接上干货。。。。。。。
Java内存结构是每个java程序员必须掌握理解的,这是Java的核心基础,对我们编写代码特别是并发编程时有很大帮助。由于Java程序是交由JVM执行的,
所以我们在谈Java内存区域划分的时候事实上是指JVM内存区域划分。
分析:如上图所示,首先Java源代码文件(
.java后缀
)会被Java编译器编译为字节码文件(.class后缀
),然后由JVM中的类加载器加载各个类的字节码文件,加载完毕之后,交由JVM执行引擎执行。在整个程序执行过程中,JVM会用一段空间来存储程序执行期间需要用到的数据和相关信息,这段空间一般被称作为Runtime Data Area(运行时数据区)
,也就是我们常说的JVM内存。因此,在Java中我们常常说到的内存管理就是针对这段空间进行管理(如何分配和回收内存空间)。
那么本篇文章主要是要分析Runtime Data Area(运行时数据区)的结构。
根据 JVM 规范,JVM 内存共分为
虚拟机栈
、堆
、方法区
、程序计数器
、本地方法栈
五个部分。(1.8过后分为6个区:元数据区
)
- 方法区是java虚拟机规范去中定义的一种概念上的区域,具有什么功能,但并没有规定这个区域到底应该位于何处,因此对于实现者来说,如何来实际方法区是有着很大自由度的。
永生代是hotspot vm中的一个概念,其他jvm实现未必有,例如jrockit vm就没这东西。java8之前,hotspot vm使用在内存中划分出一块区域来存储类的元信息、类变量,常量,符号引用以及内部字符串(interned string,也就是字面量)等内容,称之为永生,把它作为方法区来使用
。[JEP122][2]提议取消永生代,方法区作为概念上的区域仍然存在。原先永生代中类的元信息会被放入本地内存(元数据区,metaspace),将类的静态变量和内部字符串(也就是字面量)放入到Java Heap中,符号引用(Symbols)转移到了native heap。
为了搞清楚方法区那么需要解释两个名词:永久代和元空间
PermGen(永久代):
JVM规范方法区的一种实现“java.lang.OutOfMemoryError: PremGen space”
异常。这里的“PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是JVM的规范,而后者则是JVM规范的一种实现,并且只有HotSpot才有“PermGen space”,而对于其他类型的虚拟机,如JRockit(Oracle)、J9(IBM)并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。并且JDK 1.8中参数PermSize和MaxPermSize已经失效。
元空间:
JVM规范方法区的一种实现PS: JDK1.8对JVM架构的改造将类元数据放到本地内存中(也就是元数据区),另外,将常量池和静态变量放到Java堆里。HotSpot VM将会为类的元数据明确分配和释放本地内存。在这种架构下,类元信息就突破了原来
-XX:MaxPermSize
的限制,现在可以使用更多的本地内存。这样就从一定程度上解决了原来在运行时生成大量类造成经常Full GC问题,如运行时使用反射、代理等。所以升级以后Java堆空间可能会增加。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间的最大区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数指定元空间的大小:
参数 | 解释 |
---|---|
-XX:MetaspaceSize | 初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对改值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。 |
-XX:MaxMetaspaceSize | 最大空间,默认是没有限制的。 |
-XX:MinMetaspaceFreeRatio | 在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集。 |
-XX:MaxMetaspaceFreeRatio | 在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。 |
总结:
所以对于方法区,Java8之后的变化:移除了永久代(PermGen),替换为元空间(元数据区Metaspace);永久代中的 class metadata(类的元数据) 转移到了 native memory(元数据区,本地内存(直接内存),而不是虚拟机内存);永久代中的 interned Strings (内部字符串:也就是字面量),常量,和 class static variables(类的静态变量) 转移到了 Java heap;永久代中的Symbols(符号引用)转移到了native heap;永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)
为更好的理解Java线程栈和堆,我们简单的认为Java内存模型把Java虚拟机内部划分为线程栈和堆。这张图演示了Java内存模型的逻辑视图。
每一个运行在Java虚拟机里的线程都拥有自己的线程栈。
这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问自己的线程栈。一个线程创建的本地变量对其它线程不可见,仅自己可见。即使两个线程执行同样的代码,这两个线程任然在在自己的线程栈中的代码来创建本地变量。因此,每个线程拥有每个本地变量的独有版本。所有原始类型的本地变量都存放在线程栈上,因此对其它线程不可见。
一个线程可能向另一个线程传递一个原始类型变量的拷贝,但是它不能共享这个原始类型变量自身。堆上包含在Java程序中创建的所有对象,(即对象实例)无论是哪一个对象创建的。
这包括原始类型的对象版本。如果一个对象被创建然后赋值给一个局部变量,或者用来作为另一个对象的成员变量,这个对象任然是存放在堆上。(has-a关系)PS:
线程本地变量即线程自身的局部变量线程自身的本地变量各自存放于线程栈中,线程所引用的对象的成员变量跟随对象自身存放于堆中,而对象的局部变量每个线程都拥有这个局部变量的私有拷贝。
下面这张图演示了调用栈和本地变量存放在线程栈上,对象存放在堆上。
一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量任然存放在线程栈上,即使这些方法所属的对象存放在堆上。
一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。
它们将会共享这个对象的成员变量
,但是每一个线程都拥有这个对象的方法中的局部变量的私有拷贝,存放于 各自的线程栈中。注意:
这个共享对象(Object 3)持有Object2和Object4一个引用作为其成员变量(如图中Object3指向Object2和Object4的箭头)。通过在Object3中这些成员变量引用,这两个线程就可以访问Object2和Object4。分支
、循环
、跳转
、异常处理
、线程恢复
等基础功能都需要依赖这个计数器来完成。如果正在执行的是Natvie 方法,这个计数器值则为空(Undefined)。
PS:此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。
虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
PS:与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
即时编译(just in time)
技术将方法编译成机器码后再执行。所以可以得出结论:hotspot即是vm的执行引擎,而hotspot vm中带有JIT(just in time)编译器,可以通过HotSpot VM的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知JIT编译器以方法为单位进行编译。标准编译
和OSR(栈上替换)编译
动作。示例图:
类的生命周期是从被加载到虚拟机内存中开始,到卸载出内存结束。过程共有七个阶段,其中到初始化之前的都是属于类加载的部分: 加载----验证----准备----解析-----初始化---- 使用-----卸载 (类的生命周期)现在我们详细的来看看JVM在加载、验证、准备、解析和初始化阶段做了些什么事情。
PS: 蓝色:表示类加载的部分 蓝色+红色:类的生命周期
加载阶段是“类加载机制”中的一个阶段,这个阶段通常也被称作“装载”,这个阶段JVM主要完成三件事:
(包名与类名)
来获取定义此类的二进制字节流(Class文件)。而获取的方式,可以通过jar包、war包、网络中获取、JSP文件生成等方 式。这里只是转化了数据结构,并未合并数据。
(方法区就是用来存放已被加载的类信息
,常 量
, 静态变量
,即时编译后的代码
的运行时内存区域。注意:
这个Class对象是存放在Java堆内存中。PS:相对于类加载过程的其他阶段,加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)是开发期可控性最强的阶段,因为加载阶段可以使用系统提供的类加载器(ClassLoader)来完成,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。
在介绍双亲委派模型之前先说下类加载器。
也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不相同)。如果是同一个类加载器,那么同一个类只有一个Class对象。
站在程序员的角度来看,Java 类加载器可以分为四种:
需要继承ClassLoader类。
代码示例:
package com.sprjjs.classloader;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
/**
*
* 文件系统自定义类加载器,主要是用来加载硬盘,网络等其他来源的字节码文件,的需要继承ClassLoader类
* @author HKM
*
*/
public class FileSystemClassLoader extends ClassLoader{
//class文件的根目录
private String rootDir;
//通过构造函数来进行初始化成员变量
public FileSystemClassLoader(String rootDir) {
this.rootDir=rootDir;
}
//需要重写findClass方法,当调用loadClass()加载某个类时会调用该重写方法
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
//判断该类是否已被加载
Class<?> clazz = findLoadedClass(className);
if(clazz!=null){
System.out.println("该类已经被加载到内存了");
return clazz;
}else{
try{
/*委托给父类加载器,使用了双亲委托机制,即将类一级一级的往上给最高级别的类加载器进行加载
也就是引导类加载器,如果引导类加载不了,则向下传递,直到应用程序加载不了,则报错。
此时可以使用自定义类加载器进行加载。
*/
ClassLoader parent = this.getParent();
clazz= parent.loadClass(className);
}catch(Exception e){
//e.printStackTrace();
}
if(clazz!=null){
return clazz;
}
else{
//将读取的字节码文件转成字节数组加载到jvm内存中
byte[] classDate=getClassDate(className);
clazz = defineClass(className,classDate, 0, classDate.length);
return clazz;
}
}
}
//读字节码文件,并将转成字节数组
public byte[] getClassDate(String className) {
//获取class文件的路径
String path=rootDir+className.replace(".", "/")+".class";
//通过字节型数组流将读取到的字节转成字节数组-->io流操作
InputStream is=null;
ByteArrayOutputStream baos=null;
int length=0;
byte[] buff=new byte[1024];
try {
baos=new ByteArrayOutputStream();
is=new FileInputStream(path);
//从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。以整数形式返回实际读取的字节数
while((length=is.read(buff))!=-1){
//将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此 byte 数组输出流。
baos.write(buff, 0, length);
}
//将输出流中的内容转成字节数组
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
//关闭资源
if(baos!=null){
baos.close();
}
}catch (Exception e) {
e.printStackTrace();
}
try {
//关闭资源
if(is!=null){
is.close();
}
}catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
双亲委派模型突然让我联想到朱元璋同志,这个同志当上了皇帝之后连宰相都不要了,所有的事情都亲力亲为,只有自己没精力没时间做的事才交给大臣们去干。
使用双亲委派模型有一个很明显的好处,那就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,这对于保证 Java 程序的稳定运作很重要。
上文中曾提到,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等——双亲委派模型能够保证同一个类最终会被特定的类加载器加载。
注:【加载阶段的第一步 “ 通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流 ”】这个动作主要是通过类加载器及双亲委派机制。详细说明可见这篇搏文:jvm 类加载机制(二)【类加载器及双亲委派模型】
类的加载过程后生成了类的java.lang.Class对象,接着会进入连接阶段,连接阶段负责将类的二进制数据合并入JRE(Java运行时环境)中。类的连接大致分三个阶段。
验证阶段主要包括四个检验过程:文件格式验证
、元数据验证
、字节码验证和符号引用验证
。
文件格式验证:
验证字节流是否符合class文件格式规范。
例如: class文件是否已魔术0xCAFEBABE开头 , 主、次版本号是否在当前虚拟机处理范围之内、常量池中的常量是否否有不被支持的类型 等。
元数据验证:
对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范要求。
验证点可能包括:这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)、这个类是否继承了不允许被继承的类(被final修饰的)、如果这个类的父类是抽象类,是否实现了其父类或接口中要求实现的所有方法。
字节码验证:
进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。
如:保证访法体中的类型转换有效,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但不能把一个父类对象赋值给子类数据类型、
保证跳转命令不会跳转到方法体以外的字节码命令上。
符号引用验证:
符号引用中通过字符串描述的全限定名是否能找到对应的类、符号引用类中的类,字段和方法的访问性 (private、protected、public、default)是否可被当前类访问。
static 修饰的变量
),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。public static int value = 12;那么变量value在准备阶段过后的初始值为0而不是12
,因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器《clinit》()方法之中,所以把value赋值为12的动作将在初始化阶段才会被执行。public static final int value = 123;编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value设置为123。
CONSTANT_Class_info
、CONSTANT_Field_info
、CONSTANT_Method_info
等类型的常量。下面我们解释一下符号引用和直接引用的概念:
符号引用 :
与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用:
可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info
、CONSTANT_Fieldref_Info
、CONSTANT_Methodef_Info
、CONSTANT_InterfaceMethoder_Info
四种常量类型。
初始化阶段是执行类构造器《clinit》() 方法的过程。
什么是类构造器?
类构造器 《clinit》() 方法是由编译器自动收藏类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生,当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
注意:
如果直接父类又有直接父类,则系统会再次重复这三个步骤来初始化这个父类,依次类推,JVM最先初始化的总是java.lang.Object类。当程序主动使用任何一个类时,系统会保证该类以及所有的父类都会被初始化。
package com.sprjjs.classloader;
import java.util.TreeSet;
/**
* 类的主动引用(一定会发生类的初始化--执行静态初始化块)
1.new一个类的对象
2.调用类的静态变量(除了final修饰的静态常量)和静态方法
3.使用java.lang.reflect包下的方法对类进行反射调用
4.当虚拟机启动.java Hello,则一定会初始化Hello类。说白了
就是先启动main方法所在的类
5.当初始化一个类,如果父类没有被初始化,则先会初始化他的父类
类的被动引用(不会发生类的初始化--执行静态初始化块)
1.当访问一个静态域时(也就是静态变量或静态方法)时,只有真正声明这个
静态域的类才会被初始化(通过子类引用父类的静态变量,不会导致子类的初始化,但父类会被初始化)
2.通过数组定义类引用,不会触发此类的初始化和父类的初始化
3.引用静态常量不会触发此类的初始化(常量在编译阶段就被存入调用类的常量池中了)和父类的初始化
* @author HKM
*
*/
public class AutoReferenceAndPassiveReference {
final int a=0;
static{
System.out.println("静态初始化Demo01");
}
public static void main(String[] args) throws Exception {
String str="7";
int a=2;
int b=2;
System.out.println(str+a+b);
System.out.println("Demo01的main方法!");
//获取存放class文件的目录
//System.out.println(System.getProperty("java.class.path"));
/**
* 类主动引用,如果父类没有初始化,都会先初始化父类
*/
//1.new 一个类的对象
/* new A();
//2.调用类的静态变量(除了final修饰的静态常量)和静态方法
System.out.println(A.width);
//3.反射
Class.forName("com.sprjjs.classloader.A");
*/
/**
* 被动引用
*/
//1.调用父类A的静态变量,不会导致该子类初始化,只会初始化父类
System.out.println(B.width);
//2.通过数组定义类引用,不会触发此类的初始化和父类的初始化
A[] as = new A[10];
//3.调用final修饰的常量不会导致初始化该类和父类
System.out.println(A.MAX);
}
}
class B extends A {
static {
System.out.println("静态初始化B");
}
}
class A extends A_Father {
public static int width=100; //静态变量,静态域 field
public static final int MAX=100; //静态常量
static {
System.out.println("静态初始化类A");
width=300;
}
public A(){
System.out.println("创建A类的对象");
}
}
class A_Father extends Object {
static {
System.out.println("静态初始化A_Father");
}
}
类的主动引用
(一定会发生类的初始化(也就是执行类构造器《clinit》() )–执行静态初始化块为静态变量赋初始值)java类中对静态变量指定初始值有两种方式:
1、声明类变量时指定初始值;
2、使用静态初始化块为类变量指定初始值;
类的初始化作用(执行静态初始化块):为静态变量赋初始值 。
对象初始化作用(执行构造器):为实例变量赋初始值。
3、2 类的被动引用
(不会发生类的初始化(也就是执行类构造器())–执行静态初始化块为静态变量赋初始值)也就是静态变量或静态方法,静态代码块,是一个整体
)时,只有真正声明这个 静态域的类才会被初始化(通过子类引用父类的静态变量,不会导致子类的初始化,但父类会被初始化)。用final修饰某个类变量时,它的值在编译时就已经确定好放入常量池了,所以在访问该类变量时,等于直接从常量池中获取,并没有初始化该类。
举个栗子:
public class B{
public static B t1 = new B();
public static B t2 = new B();
{
System.out.println("构造块");
}
static
{
System.out.println("静态块");
}
public static void main(String[] args)
{
B t = new B();
}
}
执行流程:构造块->构造块->静态块->构造块
分析:(非常详细的解析)看了几个大神的解析,茅塞顿开,总结一下:
1.程序入口main方法要执行首先要加载类B 。
2.静态域:分为静态变量,静态方法,静态块。这里面涉及到的是静态变量和静态块,当执行到静态域时,按照静态域的顺序加载。并且静态域只在类的第一次加载时执行 。
3.每次new对象时,会执行一次构造块和构造方法,构造块总是在构造方法前执行(当然,第一次new时,会先执行静态域,静态域〉构造块〉构造方法)
注意:加载类时并不会调用构造块和构造方法,只有静态域会执行。
4.根据前三点,首先加载类B,执行静态域的第一个静态变量,static b1=new B(),输出构造块和构造方法(空)。ps:这里为什么不加载静态方法呢?因为执行了静态变量的初始化,意味着已经加载了B的静态域的一部分,这时候不能再加载另一个静态域了,否则属于重复加载 了(静态域必须当成一个整体来看待。否则加载会错乱) 于是,依次static b2 =new B(),输出构造块,再执行静态块,完成对整个静态域的加载,再执行main方法,new b(),输出构造块。
在如下几种情况下,Java虚拟机将结束生命周期:
下图就是java内存模型,但是一般讨论的时候不会画这个图,一般画的是java内存模型抽象结构图(在下文)。Thread Stack就是java内存模型抽象结构图中的工作内存,Heap就是java内存模型抽象结构图中的主内存。接下来介绍下图中两个线程内存分配的概念。
堆是由垃圾回收来负责的,
堆的优势是可以动态的分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的。java的垃圾收集器会自动回收不再使用的数据。缺点是:由于在运行时动态分配内存,因此存取速度相对慢一些。
栈的优势是存取速度比heap要快,仅次于计算机里的寄存器。
它的缺点是Stack中的数据大小和生存期必须是确定的。缺乏灵活性。Stack中主要存放基本类型的变量。比如int,short,long,float,double,byte,char等。如果两个线程同时调用同一个对象上的同一个方法,它们将会共享这个对象的成员变量,但是每一个线程都拥有这个对象的方法中的局部变量的私有拷贝,存放于各自的线程栈中。
首先介绍cpu,现在计算机通常用两个或多个cpu,其中一些cpu还有多核,从这一点我们可以看出,在一个有两个或者多个cpu的计算机上,同时运行多个线程是非常有可能的,而且每个cpu在某一个时刻,运行一个线程是肯定没有问题的。这意味着如果java程序是多线程的,在java程序中每个cpu上的一个线程是可能同时并发执行的。
介绍完cpu,然后是cpu寄存器(CPU Registers),每个cpu都包含一系列的寄存器,它们是cpu内存的基础,cpu在寄存器上执行操作的速度远大于在主存上执行的速度,这是因为cpu访问寄存器的速度远大于主存。
高速缓存Cache,由于计算机的存储设备与处理器的运算速度之间有几个数量级的差距。所以现在计算机系统都不得不加入一层读写速度都尽可能接近处理器运算速度的高级缓存来作为内存与处理器之间的缓存,将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后,在从缓存同步到主存中。这样处理器就无需等待缓慢的内存读写。CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度还是要慢一点。
每个CPU可能有一个CPU的缓存层,一个CPU还有多层缓存。在某一时刻一个或多个缓存行可能被读到缓存,一个或多个缓存行可能被刷新回主存。同一时间点可能有很多操作在里面。
CPU内存,所有的cpu都可以访问主存,主存通常比cpu的缓存大的多。
PS:通常情况下,当一个cpu需要读取主存的时候它会将主存的部分读取到cpu缓存中。它甚至会将缓存的部分内容读到内部寄存器里,然后在寄存器中执行操作,当cpu需要将结果回写到主存的时候,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将缓存中的值刷新回主存。
通过图可以看出java内存模型与硬件架构之间存在一些差异,硬件内存架构它没有区分线程栈和堆,对于硬件而言所有的线程栈和堆都分布在主内存里,部分cpu栈和堆可能出现cpu缓存中和cpu内部的寄存器里面。
java内存模型抽象结构图:
java内存模型(Java Memory Model, JMM)是一种规范,它规范了java虚拟机与计算机内存是如何协同工作的。它规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值。以及在必须时如何同步的访问共享变量 。
Java 线程只能直接对其的私有工作内存进行读取和写入数据操作,而不能对主内存直接进行读取和写入操作。
主内存对所有的 Java 线程都可见,即所有的 Java 线程都可以通过其工作内存来间接的修改主内存中的数据。
线程的工作内存只对其对应的 Java 线程可见,不同的 Java 线程不共享其工作内存。
线程之间的共享变量存储在主内存里面,每个线程都有一个私有的工作内存(也叫本地内存),工作内存是java内存模型的抽象概念,它并不是真实存在的。它涵盖了缓存,写缓存区,寄存器以及其他硬件和编译器的优化。本地内存存储了该线程以读或写共享变量拷贝的副本。比如线程A要是用共享变量的副本它首先要拷贝到本地内存A。从更低的层次来说主内存就是硬件内存,是为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
Java内存模型中的线程的工作内存是cpu的寄存器和高速缓存的抽象描述。
而JVM静态存储模型就是jvm内存模型,它只是对内存的物理划分而已,它只局限于内存,而且只局限于jvm内存。线程之间通信必须要通过主内存(主内存其实是堆内存)。
总结:对于操作系统内存模型来说:主内存就是硬件内存,本地内存(工作内存)就是cpu寄存器和cpu高速缓存内存,java线程就是cpu处理器。对于java内存模型来说:
主内存就是堆内存,所有的线程共享,本地内存(工作内存)就是线程栈,每个线程私有。
分代垃圾回收机制,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。我们将对象分为三种状态:年轻代
、年老代
、持久代
。JVM将堆内存划分为 Eden
、Survivor
和 Tenured/Old
空间。
Eden
、Survivor1
,Survivor2
三个区域,比例为8:1:1
,Minor GC(也叫Scavenge GC)所有新生成的对象首先都是放在Eden区。 年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,对应的是Minor GC,每次 Minor GC 会清理年轻代的内存,算法采用效率较高的复制算法,频繁的操作,但是会浪费内存空间。当“年轻代”区域存放满对象后,就将对象存放到年老代区域。Tenured/Old区域
,Major GC
和Full GC(全量回收)
在年轻代中经历了N(默认15
)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。年老代对象越来越多,我们就需要启动Major GC
和Full GC(全量回收)
,来一次大扫除,全面清理年轻代区域和年老代区域。持久代对垃圾回收没有显著影响。
Minor GC:用于清理年轻代区域。Eden区满了就会触发一次Minor GC。清理无用对象,将有用对象复制到“Survivor1”、“Survivor2”区中(这两个区,大小空间也相同,同一时刻Survivor1和Survivor2只有一个在用,一个为空
)。
Major GC:用于清理老年代区域。
Full GC:用于清理年轻代、年老代区域。 成本较高,会对系统性能产生影响。
新创建的对象,绝大多数都会存储在Eden中。
同时将Eden区中的不能清空的对象,也复制到S1中,保证Eden和S1,均被清空。
默认15次
)Survivor1中没有被清理的对象,Survivor2没有足够的空间存放没有的被清理的对象时,则会复制到老年代Old(Tenured)区中。当Old区满了,则会触发Major GC和一个一次完整地垃圾回收(FullGC),之前新生代的垃圾回收称为(minorGC)。
Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。
PS:内存泄露是指该内存空间使用完毕之后未回收,在不涉及复杂数据结构的一般情况下,Java 的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有时也将其称为“对象游离”。
Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:
算法分析 :
引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。
当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。
优缺点:
无法检测出循环引用。
如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。示例:
public class Main {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();
object1.object = object2;
object2.object = object1;
object1 = null;
object2 = null;
}
}
jvm内存结构如下:
分析:
最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。
算法分析:
第一种解释:根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
第二种解释:从 GC Root 开始向下搜索,搜索所走过的路径称为引用链。
当一个对象到 GC Root 没有任何引用链相连时,则证明此对象是可以被回收的。
java中可作为GC Root的对象有:
1、 虚拟机栈中引用的对象(本地变量表)
2、方法区中静态属性引用的对象
3、方法区中常量引用的对象
4、本地方法栈中引用的对象(Native对象)
标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。一个对象面(Eden区)
和多个空闲面(Survivor0,Survivor1区
), 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。
java虚拟机垃圾收集器关注的内存结构如下:
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。
年轻代(Young Generation)
8:1:1
的比例分为一个eden区
和两个survivor(survivor0,survivor1)区
。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
不一定等Eden区满了才触发
)。年老代(Old Generation)
默认15次
)后垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
当老年代内存满时触发Major GC即Full GC
,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。持久代(Permanent Generation)
PS:实线表示可以搭配使用
新生代单线程收集器,标记和清理都是单线程,优点是简单高效。
老年代单线程收集器,Serial收集器的老年代版本。
新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现,且可以缩短安全点的暂停时间。
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。
适合后台应用等对交互相应 要求不高的场景。
Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。
真正意义上的高并发GC收集器
、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。
总结:Serial,Serial Old,ParNew,Parallel Scavenge,Parallel OldGC线程工作时,都需要暂停用户线程,即stop the world,而CMS则用户线程和GC线程可以并行工作。
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,
对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动 到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。
因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快
、效率高的算法
,使Eden去能尽快空闲出来。
对整个堆进行整理,包括Young、Tenured和Perm。
Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:
静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。
代码示例:
Static Vector v = new Vector();
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}
分析:在这个例子中,代码栈中存在Vector 对象的引用 v 和 Object 对象的引用 o 。在 For 循环中,我们不断的生成新的对象,然后将其添加到 Vector 对象中,之后将 o 引用置空。问题是当 o 引用被置空后,如果发生 GC,我们创建的 Object 对象是否能够被 GC 回收呢?答案是否定的。因为, GC 在跟踪代码栈中的引用时,会发现 v 引用,而继续往下跟踪,就会发现 v 引用指向的内存空间中又存在指向 Object 对象的引用。也就是说尽管o 引用已经被置空,但是 Object 对象仍然存在其他的引用,是可以被访问到的,所以 GC 无法将其释放掉。如果在此循环之后, Object 对象对程序已经没有任何作用,那么我们就认为此 Java 程序发生了内存泄漏。
各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。
监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
感言:写完竟然有很强的成就感,哈哈哈。。。能认认真真看完上面,从而读到我的感言的小伙伴们,那肯定是。。。。。。我感动的泪水都要下来了。。。。。
总而言之:知易行难,文章的结束,正是行动的开始,愿你用行动,给自己创造一片繁花似锦。
希望本文对想掌握JVM这个技术点的你有帮助,提醒:这些知识在面试的时候可能很加分哦!!!
记得点个赞,也希望能分享给更多的朋友哦,么么哒。。。。
友情提示:
本文由昵称:某一个有b格的程序yuan博主原创,转载请注明来源:一篇带你立马搞定jvm内存,类加载机制全过程,java内存模型,分代垃圾回收机制,垃圾回收算法和垃圾收集器,谢谢~~~