[java] 虚拟机重点知识

Table of Contents

1.虚拟机内存结构详解

程序计数器

虚拟机栈

本地方法栈

方法区

2. 对象的内存布局?虚拟机栈中的引用如何和堆中的对象产生关联的?

句柄方式:

直接指针方式:

3. eden survivor区的比例,为什么是这个比例,eden survivor的工作过程

4. 可以视为GC root的对象都有哪几种?

5. 是否可以GC直接内存

6. 常用的JVM调优参数

7. dump文件分析

8. java有没有主动触发GC的方式?

9.GC算法总结

10. java内存模型与线程, happens before原则,内存模型与前面的内存区域的区别?

主内存与工作内存

与java内存区域的区别

happens-before原则


1.虚拟机内存结构详解

虚拟机管理的内存如图

我们主要关注的是运行是数据区,其中白色的几块为线程私有的,灰色的是线程共享的数据区,而GC针对的区域也是这两个区域:堆和方法区,且方法区的垃圾收集行为是可配的,也比较少出现。原因是程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭。

程序计数器

  1. 当前字节码执行的行号
  2. 线程私有
  3. 没有oom异常

虚拟机栈

  1. 线程私有
  2. 每个方法在执行时,会创建一个栈帧,即stack frame,用于存储方法的局部变量表,动态链接,方法出口等信息。这也从内存角度证明了局部变量的线程安全性。同时,还证明了在一个类编译出来的Class文件里面,是不会有这个类的方法的,只会包括这个类的成员变量,以及对象头信息等。
  3. 每个方法执行过程->对应一个栈帧的入栈和出栈过程
  4. 局部变量表,即存放方法的局部变量,有两种,一是基本数据类型,如int等,而是对象引用
  5. 当线程请求栈深度大于虚拟机允许的深度时->stackoverflow
  6. 虚拟机栈无法申请到足够内存时->oom

本地方法栈

  1. 与虚拟机栈几乎一样,只是虚拟机栈为java方法服务,而本地方法栈为native方法服务

  1. 是虚拟机管理的最大块内存,唯一目的存放对象实例。也叫GC堆。
  2. 所有线程共享
  3. 唯一目的存放对象实例,但现在已不再绝对,也叫GC堆
  4. 可分为新生代和老年代,默认比例为1:2。新生代细分可分为Eden空间,From Survivor空间,To Survivor空间。注意新生代和老年代只是针对堆的概念。
  5. 在物理上是不连续的
  6. 无法再分配实例时oom

方法区

  1. 线程共享
  2. 存储类信息,静态变量即类变量,常量,即时编译器编译之后的代码等。

    Person p = new Persoon("小明",18);
    p  是指针,存放在栈中。
    new Persoon("小明",18) 是对象 ,存放在堆中。
    Person 类的相关信息存放在方法区。
    引申 :对象的实例保存在堆上,对象的元数据(instantKlass)保存在方法区,对象的引用保存在栈上。

  3. 内存不够用时oom
  4. 也叫永久代,Permanent Generation
  5. 运行时常量池是方法区的一部分,用于存放在编译期生成的各种字面量和符号引用。

2. 对象的内存布局?虚拟机栈中的引用如何和堆中的对象产生关联的?

对象的内存布局分为三块区域:对象头header,实例数据Instance Data,对齐填充 Padding。如下图:

 

即对象的访问定位。两种方式主要有:

句柄方式:

引用中存储的是对象的句柄地址,而这些句柄存储在堆中的句柄池中,而池中的句柄存储了对象实例数据地址和对象类型数据的地址,通过对象实例数据地址可以在堆中找到对象实例数据,通过对象类型数据地址可以在方法区中找到类型数据.

优点:句柄方式最大的优点是当对象被移动的时候,只会改变句柄中对象实例数据地址,而引用(reference)本身中句柄地址不变。

直接指针方式:

引用中存储的是对象地址,通过对象地址在堆中可以找到对象实例数据,而对象实例数据中存储了类型数据的指针,通过这个指针可以到方法区中找到类型数据,即是哪个类的对象

优点: 减少了一次指针定位的时间开销,速度更快,HotSpot虚拟机正使用了这种方式。 

3. eden survivor区的比例,为什么是这个比例,eden survivor的工作过程

首先eden survivor是对新生代的内存划分方式,如下图:

eden区域栈8份,两个survivor区域各占1份。当对新生代进行minor gc时,会把Eden中和Survivor中的存活对象复制到另一块survivor区域中。因为新生代中98%的对象都是"朝生夕死"的,只有很少会存活下来,因此就设定了10%的空间来存放活下来的。

但是万一还是出现了不止10%的对象存活下来呢?岂不是放不下了?虚拟机考虑到了这个情况并有一个分配担保机制:在这种情况下让它们直接进入老年代即可。

4. 可以视为GC root的对象都有哪几种?

  • 虚拟机栈(栈帧中的本地变量表)中引用对象
  • 方法区中的类静态属性引用的对象
  • 方法区中常量引用的对象(final 的常量值)
  • 本地方法栈JNI(即一般说的Native方法)引用的对象

5. 是否可以GC直接内存

首先需要明确的是,直接内存是指什么。

直接内存,实际上就是系统物理内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

直接内存的分配不受java堆大小限制,但是必然会受到本机内存大小等的限制。

6. 常用的JVM调优参数

GC 命令行选项 描述
-Xms 设置Java堆大小的初始值/最小值。例如:-Xms512m (请注意这里没有”=”).
-Xmx 设置Java堆大小的最大值
-Xmn 设置年轻代对空间的初始值,最小值和最大值。请注意,年老代堆空间大小是依赖于年轻代堆空间大小的
-XX:PermSize=<n>[g|m|k] 设置持久代堆空间的初始值和最小值
-XX:MaxPermSize=<n>[g|m|k] 设置持久代堆空间的最大值

7. dump文件分析

老年代什么情况下会发生gc?

除直接调用System.gc外,触发Full GC执行的情况有如下四种。

1. 老年代空间不足。

老年代代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误: java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。

2. Permanet Generation空间满

PermanetGeneration中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息: java.lang.OutOfMemoryError: PermGen space 为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。

3. CMS GC时出现promotion failed和concurrent mode failure

对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。 promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。 应对措施为:增大survivorspace、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX:CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。

4. 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间

这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。 例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。 当新生代采用PSGC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。 除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java-Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。

场景分析:一台4核8g的服务器,每隔两小时就要出现一次老年代gc,现在有日志,怎么分析是哪里出了问题

 

8. java有没有主动触发GC的方式?

我们可以在代码里调用:

System.gc();

Runtime.getRuntime().gc();

java.lang.management.MemoryMXBean.gc()

但这些方法的作用只是告诉JVM尽快GC一次,不会立即执行GC。虚拟机的规范是通知虚拟机尽快执行,没有强制规定执行时间,因此按照这个规范,答案是没有。

9.GC算法总结

准确的说一共三种

1.标记清楚

2.标记整理

3.复制

4.分代收集,分代收集只是针对新生代和老年代采取不同的算法,但还是上面的这三种

现代虚拟机都采用的分代收集。

10. java内存模型与线程, happens before原则,内存模型与前面的内存区域的区别?

happens-before是JMM的核心。jmm即java memory model也就是java内存模型。java内存模型的用意是来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致饿内存访问效果。在jdk1.5之后成熟和完善了起来。

主内存与工作内存

  • JMM规定所有的变量存储在主内存(注意只是实例字段,静态字段,不包括局部变量,方法参数,因为后者是线程私有的不会被共享也就不会存在竞争问题。如果局部变量是一个reference类型,它引用的对象在java堆中被各个线程共享,但是reference这个引用本身在java栈的局部变量表中,它是线程私有的)
  • 每条线程有自己的工作内存(保存该线程用到的变量的主内存副本拷贝)
  • 线程对变量的所有操作 (读取,赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。问题:volatile关键字违反这一规则吗?答案:不违反,volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序规定,使得它看起来如果直接在主内存中读写访问一般,因此它并不是例外
  • 不同的线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存存完成

线程,主内存,工作内存三者的交互关系如图:

与java内存区域的区别

这里所说的主内存,工作内存与之前的java内存区域中的java堆,栈,方法区等并不是同一个层次的划分,两者基本上没有关系。如果一定要对应起来,则从定义上来看,主内存主要对应于java堆上的对象实例数据部分(对象除了实例数据部分还有对象头的信息),工作内存则对应与虚拟机栈中的部分区域。

happens-before原则

也叫先行发生原则,我们在写java并发代码时并没有感觉。下面是happens-before原则规则:

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

猜你喜欢

转载自blog.csdn.net/topdeveloperr/article/details/81303745
今日推荐