JVM总结

一、内存区域

 
这里写图片描述

1.1.程序计数器

功能:

① 字节码控制程序执行

② 确保多线程的线程轮流切换机制中线程的正确恢复

1.2.java虚拟机栈

方法执行时创建一个栈帧,存放:局部变量(编译期分配内存空间,包括基本数据类型、对象引用、returnAddress)、操作数、动态链接、方法出口等

异常:

① 栈的深度大于虚拟机允许深度 StackOverflowError

② 动态扩展时无法申请到足够空间 OutOfMemoryError

1.3.本地方法栈

(1)本地方法:

一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现。在定义一个native method时,并不提供实现体(有些像定义一个java interface),因为其实现体是由非java语言在外面实现的。

标识符native可以与所有其它的java标识符连用,但是abstract除外。这是合理的,因为native暗示这些方法是有实现体的,只不过这些实现体是非java的,但是abstract却显然的指明这些方法无实现体。native与其它java标识符连用时,其意义同非Native Method并无差别,比如native static表明这个方法可以在不产生类的实例时直接调用,这非常方便,比如当你想用一个native method去调用一个C的类库时。

本地方法非常有用,因为它有效地扩充了jvm。事实上,我们所写的java代码已经用到了本地方法,在sun的java的并发(多线程)的机制实现中,许多与操作系统的接触点都用到了本地方法,这使得java程序能够超越java运行时的界限。有了本地方法,java程序可以做任何应用层次的任务。

为什么要使用Native Method
与java环境外交互:
与操作系统交互:
Sun's Java

更多参考:https://blog.csdn.net/itmrchen/article/details/53065883

(2)异常同虚拟机栈

1.4.java堆

可以处于物理上不连续的内存空间,只要逻辑上连续即可

扩展:-Xmx堆内存最大值  -Xms初始化堆内存大小

1.5.方法区

存储已加载的类信息、常量、静态变量、即时编译器编译的代码

ps:即使编译器:

把Java的字节码(包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序。属于动态编译(dynamic compilation指的是“在运行时进行编译”;与之相对的是事前编译(ahead-of-time compilation,简称AOT),也叫静态编译(static compilation))。

垃圾回收的目标:常量池的回收和对类型的卸载。

从jdk1.7已经开始准备“去永久代”的规划,jdk1.7的HotSpot中,已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中,(常量池除字符串常量池还有class常量池等),这里只是把字符串常量池移到堆内存中;在jdk1.8中,方法区已经不存在,原方法区中存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)。(出自:https://blog.csdn.net/huyuyang6688/article/details/81490570
这里写图片描述
去永久代的原因有: 
(1)字符串存在永久代中,容易出现性能问题和内存溢出。 
(2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。 
(3)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

运行时常量池

方法区的一部分。Class文件包括:1、类的描述信息 2、常量池(存放编译期生成的字面量和符号引用,类加载后进入运行时常量池)

运行时常量池相对于class文件常量池的特征:动态性(String的intern()方法)

String的intern()方法:从字符串常量池中查询当前字符串是否存在,存在返回当前字符串的引用,若不存在就会将当前字符串放入常量池中再返回变量的引用。(以下出自https://www.cnblogs.com/wxgblogs/p/5635099.html

String s = new String("abc")创建了2个对象,第一个对象是”abc”字符串存储在常量池中,第二个对象在JAVA Heap中的 String 对象。

public static void main(String[] args) {
    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);
 
    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);
}

打印结果是

  • jdk6 下false false
  • jdk7 下false true
public static void main(String[] args) {
    String s = new String("1");
    String s2 = "1";
    s.intern();
    System.out.println(s == s2);
 
    String s3 = new String("1") + new String("1");
    String s4 = "11";
    s3.intern();
    System.out.println(s3 == s4);
}

打印结果为:

  • jdk6 下false false
  • jdk7 下false false

jdk6中的解释

  注:图中绿色线条代表 string 对象的内容指向。 黑色线条代表地址指向。

  如上图所示。首先说一下 jdk6中的情况,在 jdk6中上述的所有打印都是 false 的,因为 jdk6中的常量池是放在 Perm 区中的,Perm区和正常的 JAVA Heap 区域是完全分开的。上面说过如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在 JAVA Heap 区域。所以拿一个 JAVA Heap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用String.intern方法也是没有任何关系的。

2.jdk7中的解释

  在 Jdk6 以及以前的版本中,字符串的常量池是放在堆的Perm区的,Perm区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用 intern 是会直接产生java.lang.OutOfMemoryError:PermGen space错误的。在 jdk7 的版本中,字符串常量池已经从Perm区移到正常的Java Heap区域了。

  • 在第一段代码中,先看 s3和s4字符串。String s3 = new String("1") + new String("1");,这句代码中现在生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap中的 s3引用指向的对象。中间还有2个匿名的new String("1")我们不去讨论它们。此时s3引用对象内容是”11″,但此时常量池中是没有 “11”对象的。
  • 接下来s3.intern();这一句代码,是将 s3中的"11"字符串放入String 常量池中,因为此时常量池中不存在"11"字符串,因此常规做法是跟 jdk6 图中表示的那样,在常量池中生成一个"11"的对象,关键点是 jdk7 中常量池不在Perm区域了,这块做了调整。常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向s3引用的对象。 也就是说引用地址是相同的。
  • 最后String s4 = "11"; 这句代码中”11″是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向s3引用对象的一个引用。所以s4引用就指向和s3一样了。因此最后的比较 s3 == s4 是 true。
  • 再看s和 s2 对象。String s = new String("1"); 第一句代码,生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象。s.intern(); 这一句是 s 对象去常量池中寻找后发现 “1” 已经在常量池里了。
  • 接下来String s2 = "1"; 这句代码是生成一个 s2的引用指向常量池中的“1”对象。 结果就是 s 和 s2 的引用地址明显不同。图中画的很清晰。

  • 来看第二段代码,从上边第二幅图中观察。第一段代码和第二段代码的改变就是 s3.intern(); 的顺序是放在String s4 = "11";后了。这样,首先执行String s4 = "11";声明 s4 的时候常量池中是不存在“11”对象的,执行完毕后,“11“对象是 s4 声明产生的新对象。然后再执行s3.intern();时,常量池中“11”对象已经存在了,因此 s3 和 s4 的引用是不同的。
  • 第二段代码中的 s 和 s2 代码中,s.intern();,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码String s = new String("1");的时候已经生成“1”对象了。下边的s2声明都是直接从常量池中取地址引用的。 s 和 s2 的引用地址是不会相等的。
String s = new String(“abc”); 
String s1 = “abc”; 
String s2 = new String(“abc”); 
System.out.println(s == s1.intern()); 
System.out.println(s == s2.intern()); 
System.out.println(s1 == s2.intern()); 

结果:false,false,true。

2.对象的创建

(1) new指令检查定位类(得到符号引用->判断类是否加载、解析、初始化)

(2) 分配内存

①内存绝对规整的采用“指针碰撞”移动指针分配内存 ②内存不规整的采用“空闲列表”找到足够大的空间划分   

并发状况:①CAS同步处理 ②按照线程划分在不同空间,即本地线程分配缓冲(TLAB)

(3) 内存空间初始化为0

(4) 设置

(5) <init>方法

3.内存溢出

https://blog.csdn.net/liyanan21/article/details/81814110

(1) 堆内存溢出 OutOfMemoryError

内存中加载的数据过多如一次从数据库中取出过多数据;集合对对象引用过多且使用完后没有清空;代码中存在死循环或循环产生过多重复对象;堆内存分配不合理;网络连接问题、数据库问题等。

(2) 栈内存溢出

① StackOverflowError

在线程较少的时候,某个线程请求深度过大  可以把-Xss的值设置大一些

② OutOfMemoryError

在虚拟机产生线程时,无法为该线程申请栈空间了

(3) 方法区溢出

方法区主要用于存储虚拟机加载的类信息、常量、静态变量,以及编译器编译后的代码等数据,所 方法区溢出的原因就是没有足够的内存来存放这些数据。

(4) 本机直接内存溢出

本机直接内存(DirectMemory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但Java中用到NIO相关操作时(比如ByteBuffer的allocteDirect方法申请的是本机直接内存),也可能会出现内存溢出的异常。

二、垃圾回收

为什么要分代

分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。 

在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。 

在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。

1.对象已死吗

(1) 引用计数

缺点:相互引用

(2) 可达性分析

GC Roots:虚拟机栈中对象、方法区中类静态属性、方法区中常量引用、本地方法栈中对象

进行可达性分析后对象和GC Roots之间没有引用链相连时,对象将会被进行一次标记,接着会判断如果对象没有覆盖Object的finalize()方法或者finalize()方法已经被虚拟机调用过,那么它们就会被清除;如果对象覆盖了finalize()方法且还没有被调用,则会执行finalize()方法中的内容,所以在finalize()方法中如果重新与GC Roots引用链上的对象关联就可以拯救自己。

(3) 引用类型判断

2.垃圾回收机制

新生代:一般新生代中的对象基本上都是朝生夕灭的,每次只有少量对象存活,因此采用复制算法(一半一半),只需要复制那些少量存活的对象就可以完成垃圾收集;

老年代:老年代中的对象存活率较高,就采用标记-清除和标记-整理算法来进行回收。 
这里写图片描述

在这些区域的垃圾回收大概有如下几种情况:

  • 大多数情况下,新的对象都分配在Eden区,当Eden区没有空间进行分配时,将进行一次Minor GC,① 清理Eden区中的无用对象。② 清理后,Eden和From Survivor中的存活对象如果小于To Survivor的可用空间则进入To Survivor,否则直接进入老年代);Eden和From Survivor中还存活且能够进入To Survivor的对象年龄增加1岁(虚拟机为每个对象定义了一个年龄计数器,每执行一次Minor GC年龄加1),当存活对象的年龄到达一定程度(默认15岁)后进入老年代,可以通过-XX:MaxTenuringThreshold来设置年龄的值。

    其中From Survivor和To Survivor交替使用
  • 当进行了Minor GC后,Eden还不足以为新对象分配空间(那这个新对象肯定很大),新对象直接进入老年代。

  • 占To Survivor空间一半以上且年龄相等的对象,大于等于该年龄的对象直接进入老年代,比如Survivor空间是10M,有几个年龄为4的对象占用总空间已经超过5M,则年龄大于等于4的对象都直接进入老年代,不需要等到MaxTenuringThreshold指定的岁数。

  • 在进行Minor GC之前,会判断老年代最大连续可用空间是否大于新生代所有对象总空间,如果大于,说明Minor GC是安全的,否则会判断是否允许担保失败,如果允许,判断老年代最大连续可用空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则执行Minor GC,否则执行Full GC。

  • 当在java代码里直接调用System.gc()时,会建议JVM进行Full GC,但一般情况下都会触发Full GC,一般不建议使用,尽量让虚拟机自己管理GC的策略。

  • 永久代(方法区)中用于存放类信息,jdk1.6及之前的版本永久代中还存储常量、静态变量等,当永久代的空间不足时,也会触发Full GC,如果经过Full GC还无法满足永久代存放新数据的需求,就会抛出永久代的内存溢出异常。

  • 大对象(需要大量连续内存的对象)例如很长的数组,会直接进入老年代,如果老年代没有足够的连续大空间来存放,则会进行Full GC。

3.垃圾收集器

https://blog.csdn.net/liyanan21/article/details/81814110

三、类加载

1.Class文件格式

class文件是一组以8位字节为基础的二进制流,这些二进制流分别以一定形式表示着魔数(头4个字节,用于标识是否是一个能被虚拟机接收的Class文件)、版本号(5,6字节次版本号,7,8字节主版本号)、常量池入口访问标志(access_flags,2个字节,访问信息)、索引字段表方法表属性表等内容。代码编译为class文件后,需要通过类加载器把class文件加载到虚拟机中才能运行和使用。

2.1.类加载步骤
这里写图片描述

类加载的时机:主动引用

主动引用:(1)new、getstatic、putstatic、invokestatic  (2)反射调用  (3)初始化类时,先初始化父类 (4)主类  (5)动态语言支持

被动引用:1. 通过子类引用父类的静态字段,子类不会初始化 
                  2. 数组定义引用类,不会触发类的初始化  SuperClass[]
                  3. 常量在编译阶段会存入常量池中,不会触发定义常量的类的初始化

1.加载

① 通过一个类的全限定名(比如com.danny.framework.t)来获取定义该类的二进制字节流(不一定从Class文件中获取); 
     非数组,可以定义类加载器(重写loadClass()方法)控制字节流的获取方式;
     数组,本身不通过类加载器创建,有虚拟机直接创建
② 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构; 
③ 在内存中生成一个代表这个类的java.lang.Class对象,作为程序访问方法区中这个类的外部接口。(对于HotSpot虚拟机,Class对象在方法区中)

2.验证

验证的目的是为了确保class文件的字节流包含的内容符合虚拟机的要求,且不会危害虚拟机的安全。

  • 文件格式验证:主要验证class文件中二进制字节流的格式,比如魔数是否已0xCAFEBABY开头、版本号是否正确等。
            保证字节流能正确解析并存储在方法区中。

  • 元数据验证:主要对字节码描述的信息进行语义分析,保证其符合Java语言规范,比如验证这个类是否有父类(java.lang.Object除外),如果这个类不是抽象类,是否实现了父类或接口中没有实现的方法,等等。
             对类的元数据信息进行语义校验。

  • 字节码验证:字节码验证更为高级,通过数据流和控制流分析,确保程序是合法的、符合逻辑的。

  • 符号引用验证:对类自身以外的信息进行匹配性校验,比如通过类的全限定名能否找到对应类、在类中能否找到字段名/方法名对应的字段/方法,如果符号引用验证失败,将抛出“java.lang.NoSuchFieldError”、“java.lang.NoSuchMethodError”等异常。
              确保解析动作正常进行。

3.准备

为【类变量】分配内存并设置类变量【初始值】,这些变量所使用的内存都分配在方法区。注意分配内存的对象是“类变量”而不是实例变量,而且为其分配的是“初始值”,一般数值类型的初始值都为0,char类型的初始值为’\u0000’(常量池中一个表示Nul的字符串),boolean类型初始值为false,引用类型初始值为null。 
但是加上final关键字比如public static final int value=123;在准备阶段会初始化value的值为123;

4.解析

解析是将常量池中【符号引用】替换为【直接引用】的过程。(ps:符号引用:类和接口的全限定名、字段、方法的名称和描述符eg:[Ljava/lang/String:String[])

符号引用是以一组符号来描述所引用的目标,符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。比如在com.danny.framework.LoggerFactory类引用了com.danny.framework.Logger,但在编译期间是不知道Logger类的内存地址的,所以只能先用com.danny.framework.Logger(假设是这个,实际上是由类似于CONSTANT_Class_info的常量来表示的)来表示Logger类的地址,这就是符号引用。

直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局有关,如果有了直接引用,那引用的目标一定在内存中存在。

解析的时候class已经被加载到方法区的内存中,因此要把符号引用转化为直接引用,也就是能直接找到该类实际内存地址的引用。

5.初始化

在准备阶段,已经为类变量赋了初始值,在初始化阶段,则根据程序员通过程序定制的主观计划去初始化类变量的和其他资源,也可以从另一个角度来理解:初始化阶段是执行类构造器<clinit>()方法的过程。

<clinit>()方法:由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的。

1. 静态语句块(static{})之前的变量可以访问,之后的变量只能赋值不能访问
2. 执行顺序:
继承的子类:子类引用父类的静态字段,子类不会初始化 

  • 父类静态变量
  • 父类静态代码块
  • 子类静态变量
  • 子类静态代码块
  • 父类普通变量
  • 父类普通代码块
  • 父类构造函数
  • 子类普通变量
  • 子类普通代码块
  • 子类构造函数

抽象的实现子类: 接口 - 抽象类 - 实现类

  • 接口静态变量
  • 抽象类静态变量
  • 抽象类静态代码块
  • 实现类静态变量
  • 实习类静态代码块
  • 抽象类普通变量
  • 抽象类普通代码块
  • 抽象类构造函数
  • 实现类普通变量
  • 实现类普通代码块
  • 实现类构造函数

2.2.类加载器

(1)类加载器的作用

  • 加载class:类加载的加载阶段的第一个步骤,就是通过类加载器来完成的,类加载器的主要任务就是“通过一个类的全限定名来获取描述此类的二进制字节流”,在这里,类加载器加载的二进制流并不一定要从class文件中获取,还可以从其他格式如zip文件中读取、从网络或数据库中读取、运行时动态生成、由其他文件生成(比如jsp生成class类文件)等。 
    从程序员的角度来看,类加载器动态加载class文件到虚拟机中,并生成一个java.lang.Class实例,每个实例都代表一个java类,可以根据该实例得到该类的信息,还可以通过newInstance()方法生成该类的一个对象。

  • 确定类的唯一性:类加载器除了有加载类的作用,还有一个举足轻重的作用,对于每一个类,都需要由加载它的加载器和这个类本身共同确立这个类在Java虚拟机中的唯一性。也就是说,两个相同的类,只有是在同一个加载器加载的情况下才“相等”,这里的“相等”是指代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括instanceof关键字对对象所属关系的判定结果。

(2)类加载器的分类

以开发人员的角度来看,类加载器分为如下几种:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)和自定义类加载器(User ClassLoader),其中启动类加载器属于JVM的一部分,其他类加载器都用java实现,并且最终都继承自java.lang.ClassLoader。

① 启动类加载器(Bootstrap ClassLoader)是由C/C++编译而来的,看不到源码,所以在java.lang.ClassLoader源码中看到的Bootstrap ClassLoader的定义是native的“private native Class findBootstrapClass(String name);”。启动类加载器主要负责加载JAVA_HOME\lib目录或者被-Xbootclasspath参数指定目录中的部分类,具体加载哪些类可以通过“System.getProperty(“sun.boot.class.path”)”来查看。

② 扩展类加载器(Extension ClassLoader)由sun.misc.Launcher.ExtClassLoader实现,负责加载JAVA_HOME\lib\ext目录或者被java.ext.dirs系统变量指定的路径中的所有类库,可以用通过“System.getProperty(“java.ext.dirs”)”来查看具体都加载哪些类。

③ 应用程序类加载器(Application ClassLoader)由sun.misc.Launcher.AppClassLoader实现,负责加载用户类路径(我们通常指定的classpath)上的类,如果程序中没有自定义类加载器,应用程序类加载器就是程序默认的类加载器。

④ 自定义类加载器(User ClassLoader),JVM提供的类加载器只能加载指定目录的类(jar和class),如果我们想从其他地方甚至网络上获取class文件,就需要自定义类加载器来实现,自定义类加载器主要都是通过继承ClassLoader或者它的子类来实现,但无论是通过继承ClassLoader还是它的子类,最终自定义类加载器的父加载器都是应用程序类加载器,因为不管调用哪个父类加载器,创建的对象都必须最终调用java.lang.ClassLoader.getSystemClassLoader()作为父加载器,getSystemClassLoader()方法的返回值是sun.misc.Launcher.AppClassLoader即应用程序类加载器。

(3)双亲委派模型
这里写图片描述

双亲委派模型的工作过程是,如果一个类加载器收到了类加载的请求,它首先不会加载类,而是把这个请求委派给它上一层的父加载器,每层都如此,所以最终请求会传到启动类加载器,然后从启动类加载器开始尝试加载类,如果加载不到(要加载的类不在当前类加载器的加载范围),就让它的子类尝试加载,每层都是如此。

那么双亲委派模型有什么好处呢?最大的好处就是它让Java中的类跟类加载器一样有了“优先级”。前面说到了对于每一个类,都需要由加载它的加载器和这个类本身共同确立这个类在Java虚拟机中的唯一性,比如java.lang.Object类(存放在JAVA_HOME\lib\rt.jar中),如果用户自己写了一个java.lang.Object类并且由自定义类加载器加载,那么在程序中是不是就是两个类?所以双亲委派模型对保证Java稳定运行至关重要。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值