JVM相关学习总结

****

基础概念

数据类型

引用类型

** **

Java内存模型

01.方法区

02.堆区 --------------垃圾回收

03.栈区

04.程序计数器

05.原生方法栈

** **

常见问题汇总

** **

Jvm运行原理

** **

基本垃圾回收算法

垃圾回收算法面临的问题

分代垃圾回收

 

Jvm调优常见配置汇总

类加载过程

类加载器

内存泄漏和内存溢出

**-

一、基础概念**

**01.

数据类型**

①  基本数据类型:byte,short,int,long,char,float,double,Boolean

②  引用数据类型:类类型,接口类型和数组

**02.

引用类型**

①  强引用:声明对象时虚拟机生成的引用,如果被强引用,则不会被垃圾回收。

强引用其实也就是我们平时 A a = new A()这个意思。

②  软引用:一般被作为缓存来使用,当内存紧张的时候,这种类型引用的空间会被回收;

软引用可用来实现内存敏感的高速缓。

③  弱引用:与软引用差不多,也是作为缓存使用,但是每次垃圾回收肯定会被回收;

      虚引用(PhantomReference)

 “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会 决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

-Java内存模型



jvm 五大区  每个区的存储 作用

    线程共享: java 堆 方法区

    线程私有: 虚拟机栈   本地方法栈   程序计数器

01.方法区(method area)

    被虚拟机加载的类信息,静态变量,常量,即时编译器编译后的代码等数据, 运行常量池是方法区的一部分,class文件除了有 类的版本,字段 ,方法,接口,等描述信息外,还有一项信息常量池保存编译期生成的字面量和符号引用。

字面量: int i = 1;把整数1赋值给int型变量i,整数1就是Java字面量, 同样,String s = "abc";中的abc也是字面量。 符号引用: 符号引用是一组符号,用来描述所引用的目标,符号是以任何形式存在的字面量。对于符号引用Java虚拟机并没有严格的限制。规定只需要使用的时候能够无歧义定位到目标就可以。 符号引用属于常量池中的内容,那么是不是说符号引用的目标已经加载到内存中了呢?答案是否定的,因为符号引用与虚拟机的内存布局无关,符号引用的目标并不一定已经加载到内存中了。

    运行时常量池具备动态性,在运行期也可能将新的常量放入池中,这种特性被开发人员利用得较多得是String类的intern()方法。

02.堆区(heap)

       

  在虚拟机启动时创建,唯一目的是存放对象的实例。

      java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象的分配在堆上也逐渐变得不是那么“绝对”了。

      java堆是垃圾收集器管理的主要区域,从内存回收的角度,现在收集器基本都采用分代收集算法,所以java堆中还可以细分为新生代和老年代。从内存分配的角度看,线程共享的java堆可能划分出多个线程私有的分配缓冲区(TLAB),不过无论怎么划分,都与存放的内容无关。

     java堆可以出于物理上不连续的内存空间中,只要逻辑上连续即可。

     在进行jvm调优时,关于堆的操作,java堆溢出,出现OutOfMemoryError异常,-Xmx 堆最大值 和 -Xms 堆最小值两个参数十分重要,出现异常时,分析时内存泄漏还是内存溢出。

 

        Java中堆是由所有的线程共享的一块内存区域。

03.Java栈区(Java stack)

     栈也叫栈内存,是 Java 程序的运行区,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束,该栈就 Over。

 描述的是java方法执行的内存模型,每个方法执行同时都会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链表,方法出口等信息,每个方法从调用到执行完成的过程,就对应一个栈帧在虚拟机中入栈到出栈的过程

    局部变量表存储的是编译期间可知的各种基本数据类型,对象引用和returnAddress类型,所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量的大小。

    异常: 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常    如果扩展时无法申请到足够的内存,将会抛出OutOfMemoryError异常。

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

    在建立多线程时会导致内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

04.程序计数器寄存器(pc register)

       是当前线程所执行的字节码的行号指示器,若线程正在执行一个java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址,若正在执行的是Native方法,计数器值为空。

    它是虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域。

05.原生方法栈(native method stack)

        原生方法栈与 Java 方法栈相类似,这里不再赘述

常见问题汇总

01. 堆和栈有什么区别

答:堆是存放对象的,但是对象内的临时变量是存在栈内存中,如例子中的 methodVar 是在运行期存放到栈中的。

栈是跟随线程的,有线程就有栈,堆是跟随 JVM 的,有 JVM 就有堆内存。

     ① 栈是运行时单位,而堆是存储的单位。

     ② 堆中存的是对象,栈中存的是基本数据类型和堆中对象的引用。

     ③ 由于程序运行在栈中进行,所以传递参数的时候,只存在传递基本数据类型和对象引用的问题,所以它都是进行传值调用。

     ④ Java中栈的大小通过 -Xss来设置。

 

02. 堆内存中到底存在着什么东西?

答:对象,包括对象变量以及对象方法。

 

03. 类变量和实例变量有什么区别?

答:静态变量是类变量,非静态变量是实例变量,直白的说,有 static 修饰的变量是静态变量,没有 static 修饰的变量是实例变量。静态变量存在方法区中,实例变量存在堆内存中。

 

04.Java 的方法(函数)到底是传值还是传址?

答:都不是,是以传值的方式传递地址,具体的说原生数据类型传递的值,引用类型传递的地址。对于原始数据类型,JVM 的处理方法是从 Method Area 或 Heap 中拷贝到 Stack ,然后运行 frame 中的方法,运行完毕后再把变量指拷贝回去。

 

05. 为什么会产生 OutOfMemory 产生?

答:一句话:Heap 内存中没有足够的可用内存了。这句话要好好理解,不是说 Heap 没有内存了,是说新申请内存的对象大于 Heap 空闲内存,比如现在 Heap 还空闲 1M ,但是新申请的内存需要 1.1M ,于是就会报 OutOfMemory 了,可能以后的对象申请的内存都只要 0.9M ,于是就只出现一次 OutOfMemory , GC 也正常了,看起来像偶发事件,就是这么回事。 但如果此时 GC 没有回收就会产生挂起情况,系统不响应了。

 

我产生的对象不多呀,为什么还会产生 OutOfMemory ?

答:你继承层次忒多了,Heap 中 产生的对象是先产生 父类,然后才产生子类,明白不?

       

06.OutOfMemory 错误分几种?

答:分两种,分别是“ OutOfMemoryError:java heap size ”和” OutOfMemoryError: PermGen

space ”,两种都是内存溢出, heap size 是说申请不到新的内存了,这个很常见,检查应用或调整堆内存大小。

“ PermGen space ”是因为永久存储区满了,这个也很常见,一般在热发布的环境中出现,是

因为每次发布应用系统都不重启,久而久之永久存储区中的死对象太多导致新对象无法申请内存,

一般重新启动一下即可。

 

07. 为什么会产生 StackOverflowError ?

答:因为一个线程把 Stack 内存全部耗尽了,一般是递归函数造成的。

         

08. 一个机器上可以看多个 JVM 吗? JVM 之间可以互访吗?

答:可以多个 JVM ,只要机器承受得了。 JVM 之间是不可以互访,你不能在 A-JVM 中访问 B-JVM 的 Heap 内存,这是不可能的。在以前老版本的 JVM 中,会出现 A-JVM Crack 后影响到 B-JVM ,现在版本非常少见。

     

09. 为什么 Java 要采用垃圾回收机制,而不采用 C/C++ 的显式内存管理?

答:为了简单,内存管理不是每个程序员都能折腾好的。

 

010. 为什么你没有详细介绍垃圾回收机制?

答:垃圾回收机制每个 JVM 都不同, JVM Specification 只是定义了要自动释放内存,也就是说它只定义了垃圾回收的抽象方法,具体怎么实现各个厂商都不同,算法各异,这东西实在没必要深入。

    

011.JVM 中到底哪些区域是共享的?哪些是私有的?

答:Heap 和 Method Area 是共享的,其他都是私有的,

**   **

-JVM运行原理

01.向操作系统申请空闲内存: jvm对操作系统说“给我64M空闲内存”,操作系统给jvm分配内存以后,jvm准备加载类文件。

02.分配内存: 给head,stack等分配内存。

03.检查文件 :检查分析class文件,若发现错误立即返回错误。

04.加载类;

05.执行引擎执行方法。

**-

基本垃圾回收算法**

**01.

按照基本回收策略分**

1)  引用计数法:

2)  复制:

3)  标记整理:

**02.

按系统线程分**

①  串行收集:使用单线程处理所有垃圾回收工作;

②  并行收集:使用多线程处理所有垃圾回收工作;

③  并发收集:前面 两个在进行垃圾会收的时候需要暂停整个运行环境,而只有垃圾回收线程在运行,并发收集不需要暂停。

 

**-

垃圾回收面临的问题**

**01.

如何区分垃圾:**

     引用计数法与可达性分析算法。

栈是真正进程开始执行的地方,一个栈是与一个进程相对应的,如果有多个线程的话,必须对这些线程对应的栈进行检查。

除了栈外还有系统运行时的寄存器,也是存储程序运行时的数据。

这样以栈和寄存器的引用为起点,我们就可以找到堆中的对象,又从这些对象找到堆中其他对象的引用,这种逐步扩展,最终以null 引用或者基本数据类型结束,这样就形成了一颗以 Java 栈中所对应的对象为根结点的对象树,如果有多个引用就会有多个对象树。在对象树上,都是当前所需要的对象,不能被垃圾回收。而其他剩余对象,则被视为无法被引用的对象,可以被当做垃圾回收。

**02.

如何处理碎片**

“复制方法”和“标记

整理”都可以。

**03.

如何解决同时存在的对象创建和对象回收问题**

垃圾回收线程是回收内存的,而程序运行线程则是消耗内存的,存在矛盾。如果采用先暂停,进行垃圾回收,然后开启,这样的问题是:当堆空间持续增大,垃圾回收时间也会增大,对应暂停时间也会增大。可以采用并发垃圾回收。

 

**-

分代垃圾回收**

**01.

分代垃圾回收**

虚拟机中共划分为三个代:( 年轻代,年老代---堆区)、持久代(在方法区,存放Java 类的类信息)

过程:

新生代分为三个区:Eden 区,两个 Survivor 区;

刚产生的对象在eden 区,这个区满了以后,还存活的对象将被复制到 survivor 区(两个中的一个),当这个 survivor 区也满了以后,此区的存活对象被复制到另外一个 survivor 区,当第二个 survivor 区对象也满了以后,将存活的对象复制到年老区。需要注意,两个 survivor 区是对称的,没有先后关系。

持久代和 Metaspace

    移除永久代的工作从JDK1.7就开始了,JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。在 JDK 1.8 中, 取而代之是一个叫做 Metaspace(元空间) 的东西。

    元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集

-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

**02.

触发垃圾回收的条件**

GC 的两种类型:  minorGC  和 Full GC;

** **

**minorGC ** :

当对象生成,并且在Eden 区中申请空间失败,就会出发,对 eden 区进行清理非存活对象,并且将存活对象放在 survivor 区中。

Full GC :

年老代被写满;

持久代被写满;

System.gc() 被显示调用;

上一次GC 之后 Head 的各域分配策略动态变化。

**03.

选择合适的垃圾回收算法**

串行收集器:使用小型应用;

并行收集器:后台处理,科学计算;

并发收集器:Web 服务器 / 应用服务器、电信交换、集成开发环境。

 

-常见配置汇总

    不能通过写Java代码来干预Java的垃圾回收。

    影响Java垃圾回收的参数主要有-Xms,-Xmx(适当设置,避免频繁的垃圾回收,垃圾回收一般是在内存不满足要求的时候进行的)

-类加载过程

对于初始化阶段,虚拟机规范严格规定了有且只有5 种情况必须立即对类进行初始化:

  1 、遇到 new  getstatic  putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化,

      最常见的java 代码场景是:使用 new 关键字实例化对象时,读取或者设置一个类的静态字段(被 final 修饰,已在编译期把结果放入常量池的静态字段除外)时,调用一个类的静态方法的时候

  2 、使用反射对类进行调用时,若类没有初始化,则需要先触发其初始化。

  3 、当初始化一个类,其父类还没有初始化,则需要先触发其父类的初始化

  4 、当虚拟机启动时,用户需要指定一个要执行的主类(包含 main 方法的那个类),虚拟机会先初始化这个主类

  5 ,当使用 JDK1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后解析结果 REF_getStatic REF_putStatic REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

-类加载器


**1.

什么是类加载器**

  * 把 .class 文件加载到 JVM 的方法区中,变成一个 Class 对象!

 

**2.

得到类加载器**

  * Class#getClassLoader()

 

**3.

类加载器的分类**

   它们都是片警!

  * 引导:类库!

  * 扩展:扩展 jar 包

  * 系统:应用下的 class ,包含开发人员写的类,和第三方的 jar 包! classpath 下的类!

 

   系统类加载器的上层领导:扩展

   扩展类加载器的上层领导:引导

   引导没上层,它是BOSS

 

  ======================================

 

**4.

类加载器的委托机制(双亲委派模型)**

  * 代码中出现了这么一行: new A();

    > 系统发现了自己加载的类,其中包含了 new A() ,这说明需要系统去加载 A 类

    > 系统会给自己的领导打电话:让扩展去自己的地盘去加载 A 类

    > 扩展会给自己的领导打电话:让引导去自己的地盘去加载 A 类

    > 引导自己真的去 rt.jar 中寻找 A 类

      * 如果找到了,那么加载之,然后返回 A 对应的 Class 对象给扩展,扩展也会它这个 Class 返回给系统,结束了!

      * 如果没找到:

        > 引导给扩展返回了一个 null ,扩展会自己去自己的地盘,去寻找 A 类

  * 如果找到了,那么加载之,然后返回 A 对应的 Class 对象给系统,结束了!

  * 如果没找到

    > 扩展返回一个 null 给系统了,系统去自己的地盘(应用程序下)加载 A 类

      * 如果找到了,那么加载之,然后返回这个 Class ,结束了!

      * 如果没找到,抛出异常 ClassNotFoundException

双亲委派模型

    Java 类加载器的作用就是在运行时加载类。

    Java 类加载器基于三个机制:委托性、可见性和单一性

    1.委托机制是指双亲委派模型。

    2.可见性原理是子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类加载器加载的类。

    3.单一性原理是指仅加载一个类一次,这是由委托机制确保子类加载器不会再次加载父类加载器加载过的类

 

**5.

类的解析过程**

 

class MyApp {// 被系统加载

     main() {

       A a = new A();// 也由系统加载

       String s = new String();// 也由系统加载!

     }

}

class String {// 引导

  private Integer i;// 直接引导加载

}

=====================

**6.

自定义类加载器**

* 继承 ClassLoader

* 重写 findClass()

内存泄漏和内存溢出

内存****泄露

内存泄露是指一个不再被使用的对象或变量还在内存中占有存储空间。

内存泄露例子:

1Vector v = new Vector(10); 2for(int i = 1;< 10;i++){ 3    Object o = new Object(); 4    v.add(o); 5} 6

    在上述循环中,不断有新的对象加到vector中,当退出循环后,o的作用域将会结束,但是由于v在使用这些对象,因此垃圾回收器无法将其回收,因此造成内存泄露。解决办法是把v置为null。

    造成内存泄露的原因:

    01)静态集合类,例如HashMap和Vector。(如上例)

    02)各种连接,例如数据库连接、网络连接以及IO连接。比如数据库连接,对于connection,statement,result在使用完之后要及时关闭连接。

    03)变量不合理的作用域。

内存泄露的解决方案:

    1、避免在循环中创建对象。

    2、尽早释放无用对象的引用。(最基本的建议)

    3、尽量少用静态变量,因为静态变量存放在永久代(方法区),永久代基本不

    参与垃圾回收。

    4、使用字符串处理,避免使用 String,应大量使用 StringBuffer,每一个 String

    对象都得独立占用内存一块区域。

    

2.内存溢出

**内存溢出:**指程序运行过程中无法申请到足够的内存而导致的一种错误。

内存溢出的几种情况(OOM 异常):

   ** OutOfMemoryError 异常:**

    除了程 序计数器外 ,虚拟机内 存的其他几 个运行时区 域都有发生OutOfMemoryError(OOM)异常的可能。

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

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

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

    2.堆 溢出

    一般的异常信息:java.lang.OutOfMemoryError:Java heap spaces。

    3.方法区溢出

    异常信息:java.lang.OutOfMemoryError:PermGen space。

    4.运行时常量池溢出

    异常信息:java.lang.OutOfMemoryError:PermGen space。

    

导致内存溢出的原因:

    1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;

    2.集合类中有对对象的引用,使用完后未清空,使得 JVM 不能回收;

    3.代码中存在死循环或循环产生过多重复的对象实体;

    4.启动参数内存值设定的过小。

内存溢出的解决方法:

    第一步,修改 JVM 启动参数,直接增加内存。(-Xms,-Xmx 参数一定不要忘记加。一般要将-Xms 和-Xmx 选项设置为相同,以避免在每次 GC 后调整堆的大小;建议堆的最大值设置为可用内存的最大值的 80%)。

    第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。

    第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。

    第四步,使用内存查看工具动态查看内存使用情况(Jconsole)。

代码交流 2021