JVM虚拟机内存问题和垃圾回收问题

1.JVM是什么?JRE是什么?JDK是什么?三者有什么关系?

答:JVM:Java虚拟机

      JRE:Java Runtime Environment,Java 运行环境。如果只想运行一个开发好的java程序,只需要安装一个JRE就行。

      JDK:Java Development Kit,Java开发工具包。提供给Java开发人员所用,用来开发java程序的。

三者关系:JDK包括JRE,JRE包括JVM。安装一个JDK,万事搞定!


2.JVM中,类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括哪几个阶段?

答:7个阶段,分别是:加载、验证、准备、解析、初始化、使用和卸载。


3. java有多少个类加载器?分别的作用是什么?一个Class文件是怎么被加载到JVM里的,描述一下加载流程。

答:java有三个类加载器,分别为:根类加载器,扩展类加载器,系统类加载器。

根类加载器负责java核心类的加载,扩展类加载器负责扩展jar包的加载;系统类加载器负责自定义类的加载。

类加载的全过程包括加载验证准备解析初始化5个阶段。其中,验证、准备、解析三个部分统称为连接。

加载阶段。虚拟机利用类加载器将Class文件加载到内存中,准确的讲,是加载到内存中的方法区,并为这个Class文件生成一个Class文件对象(类的字节码对象),作为方法区中这个Class文件的访问入口。(注意,Class文件存放在方法区,但是Class文件对象并不一定存放在堆中,还可能在方法区中)

验证阶段。这一阶段的目的是为了确保Class文件的中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备阶段。这一阶段是正式在方法区内为类变量分配内存,并设置初始值。

这里有两个特别容易混淆的概念需要强调一下。首先,这个时候进行内存分配的类变量指的仅是静态变量,而不是成员变量(实例变量),成员变量将会在对象实例化时随着对象一起分配在堆内存中。其次,这里的初始值指的是数据类型的零值(假设有一个类的静态变量为static int value = 123,那变量value在准备阶段过后的初始值为0而不是123)。

解析阶段。这一阶段是将类文件中的符号引用替换为直接引用。

符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在java中,一个java类将会被编译成一个Class文件。在编译时,java类并不知道所引用的类实际地址,因此只能使用符号引用来替代。比如People类引用了Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个符号,当然,实际不是)来表示Language类的地址。

直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。直接引用就是类的实际内存地址,如果有了直接引用,那引用的目标必定已经被加载入内存中了。

初始化阶段。执行类中定义的java代码,初始化类变量和其它资源(假设有一个类的静态变量为static int value = 123,准备阶段过后,value初始值为0,而初始化阶段,就是执行java代码,将123赋给valuevalue的初始值为123)。


4.JavaC/C++,有一点不同之处在于:Java程序员不需要考虑内存管理和垃圾回收,因为这些操作都是由Java虚拟机自动完成的,既然都已经实现“自动化”了,那为什么还要学习JVM的内存机制和垃圾回收机制呢?(为什么要学JVM?)

答:原因很简单,当出现各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,如果你对垃圾收集算法、内存分配机制等一窍不通,那你就无法去排查和解决这些问题。


5.简介一下JVM的内存区域(运行时数据区)。

答:为了提高程序的效率,JVM中的内存区域具体划分了如下5个内存空间:

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

(通俗解释)虚拟机栈是用来管理线程运行时方法的。一个运行时方法对应一个栈帧,虚拟机栈通过栈帧来管理正在运行的方法,栈帧随着方法的调用而入栈,随着方法的结束而出栈。一个栈帧又被划分了多个区域,如局部变量表、操作数栈、动态链接、方法出口等区域,每个区域用来管理运行时方法里的不同数据,比方局部变量表里存放的是方法里的局部变量,方法出口里面存放的是方法的返回值信息等。

注意:栈内存存在内存溢出问题,一个线程对应一个虚拟机栈,是线程安全的。

Java:存放的是所有new出来的东西。包括数组和对象实例(成员变量及类中方法在方法区中的内存地址)。

注意:并不是所有的对象都会放在堆内存中;堆内存是垃圾回收器管理的主要区域;堆内存存在内存溢出问题;java堆是线程共享的运行时数据区,是线程不安全的。

方法区:类信息(被虚拟机加载的Class文件),常量(自定义常量和字符串常量,放在运行时常量池中),静态变量等。

注意:方法区中有一部分叫运行时常量池,该常量池中存放着常量以及字符串常量;方法区也会被垃圾回收器管理;方法区存在内存溢出问题;方法区是线程共享的运行时数据区,是线程不安全的。

本地方法栈:(官方解释)本地方法栈与栈所发挥的作用非常相似,它们之间的区别不过是栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。

(通俗解释)本地方法栈的内存机制与虚拟机栈类似。Java中有两种方法,一种是自己用java代码写的方法,一种是JDK自带的本地方法,有native关键字修饰,如Date类里面的currentTimeMillis()等,本地方法栈是用来管理运行时的本地方法的。

注意:本地方法栈存在内存溢出问题;一个线程对应一个本地方法栈,是线程安全的。

程序计数器:(官方解释)程序计数器是一块较小的内存空间,是“线程私有”的内存空间。由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

(通俗解释)线程A执行到一半,CPU执行权被线程B抢走了,当线程B执行完后,线程A抢到CPU执行权,需要从断开的位置继续向下执行,如果来确定断开的位置在哪呢?这就需要程序计数器了,一个线程对应一个程序计数器,程序计数器是用来专门管理线程的内存空间。

注意:程序计数器主要用来管理线程,一个线程对应一个程序计数器,是线程安全的;程序计数器不存在内存溢出问题。

 

6.方法递归调用时,虚拟机栈中是只有一个栈帧,还是有多个栈帧?

答:多个栈帧。经过测试,当无线递归调用时,会发生内存溢出异常,所有可得出结论,递归调用的方法,在虚拟机栈中会创建多个栈帧。

 

7.虚拟机栈中,一个栈帧对应一个运行时方法,栈帧与栈帧之间大小相等吗?

答:不一定相等。因为栈帧中的局部变量表里存放着方法的局部变量,每个方法所包含的局部变量大小不一定相等,所以局部变量表不一定相等,所以栈帧不一定相等。

 

8.堆内存是如何为这些对象分配空间的呢?

答:堆内存可分为两部分,分别是:新生代和老年代。假如堆内存大小为20M,那么新生代10M,老年代10M

新生代又分为3部分,一个Eden和两个Survivor。其中,EdenSurvivorSurvivor = 8M1M1M,每次新生代中可用内存空间为整个新生代容量的90%80%+10%),即1Eden1Survivor,只有10%的内存会被浪费。当有对象进来时,会优先在新生代中的Eden中进行分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC(新生代的垃圾回收)。

新生代中是通过复制回收算法来实现垃圾回收操作的,思路如下:当回收时,将EdenSurvivor中还存活着的对象一次性地复制到另外一块空Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。当新生代中存活的对象大于空Survivor的最大容量时,Survivor空间不够用,需要依赖其他内存(这里指老年代)进行分配担保,也就是说要把超出Survivor最大容量的对象存放到老年代中。

有些对象在分配的时候会直接进入老年代。

1. 大对象直接进入老年代。虚拟机可设置一个参数,假如参数值为3M,当一个对象的大小超过3M时,这个对象就不会往新生代中存放,而是直接放到老年代中。

2. 长期存活的对象直接进入老年代。既然虚拟机采用了分代收集的思想来管理内存,那么堆内存在为对象分配内存时,就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机为每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),被视为长期存活的对象,就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数进行设置。

当老年代中的内存不够用时,会触发Major GC(老年代的垃圾回收),老年代进行垃圾回收采用的是标记-清除算法或者标记-整理算法。

 

9.什么情况下,对象会存放在新生代?

答:对象会优先存放在新生代的Eden空间中,如果Eden没有足够的连续空间存放该对象,会将该对象存放在新生代其中1Survivor空间中,如果Eden1Survivor空间中均没有足够大的连续空间存放该对象,那么新生代会进行一次新生代的垃圾回收(Minor GC),将Eden和其中1Survior中存活的对象复制到另一个Survivor空间中(如果另一个Survivor没有足够大的连续空间来接收这些对象,将会把超出部分的对象利用分配担保机制存放到老生代中),然后清空Eden和其中1Survior的内存空间,再将该对象存放到Eden中。

 

10.什么情况下,对象会被存放在老年代?

答:1.当新生代中没有足够大的连续空间来存放这个对象时,会通过分配担保机制将该对象存放在老年代中。

2.大对象直接存放在老年代。

3.长期存活的对象将存放在老年代。

 

11.JVM内存模型指的是什么?

答:内存模型是用来解决多线程间通信问题的。我们可把内存模型分为两部分:主内存和工作内存。


12.JVM运行时数据区(内存区域)和JVM内存模型有什么区别?

答:大家千万不要把运行时数据区和内存模型搞混了。

运行时数据区分为5部分:虚拟栈、java堆、方法区、本地方法栈、程序计数器。

内存模型分为2个部分:主内存和工作内存。

运行时数据区和内存模型不是一回事,这是两个层次的东西,没有必然联系。如果非要把两者联系起来,那么,主内存中存放的是多线程的共享数据,应该属于java堆或者方法区。工作内存是每个线程独立拥有的内存空间,是线程安全的,应该属于虚拟机栈或者本地方法栈部分。


13.在语言层面上,创建对象(例如克隆、反序列化)通常仅仅是一个new关键字而已,而在虚拟机中,对象(文中讨论的对象限于普通Java对象,不包括数组和Class对象等)的创建又是怎样的一个过程呢?

答:虚拟机遇到一条new指令时,首先检查相应的类文件是否已被加载到内存中,如果没有,就必须先执行相应的类加载过程。

在类加载完毕后,虚拟机将会在堆内存中为新生对象分配内存空间。

内存分配完毕后,虚拟机需要将分配到的内存空间都初始化为零值。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

接下来,虚拟机要对对象进行必要的设置(具体做了哪些设置,不需要知道)。

在上面的工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象的创建才刚刚开始,因为这时所有的字段都还为零。所以,执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。


14.你知道哪些垃圾回收算法,垃圾回收的时候怎么判断一个对象是否可以被回收?

:常见的垃圾回收算法有标记-清除算法复制算法标记-整理算法分代收集算法

标记-清除算法:是最基础的垃圾回收算法。如其名一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。之所以说是最基础的垃圾回收算法,是因为后面几个都是基于它的思路并改其不足而得到的。

缺点:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。

复制算法:为了解决效率问题,一种称为“复制”的算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,代价未免太高了一点。

现在的商业虚拟机都采用这种收集算法来回收新生代。但并不是按1:1的比例来划分内存空间。我们发现,新生代中的对象98%都是“朝生夕死”的,所以不需要按1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将EdenSurvivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认EdenSurvivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%80%+10%),只有10%的内存会被浪费。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。

内存的分配担保是指,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

缺点:复制回收算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

标记-整理算法:根据老年代的特点,有人提出了另外一种“标记—整理”算法。标记过程仍然与“标记—清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法:当代商业虚拟机的垃圾收集算法都采用“分代收集”算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆内存划分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清除”算法或者“标记—整理”算法来进行回收。

 

15.Java中,垃圾回收的时候怎么判断一个对象是否可以被回收?

答:有人会说,用引用计数算法来实现,思路如下:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

这种回答是错误的,引用计数算法实现简单,判定效率也很高,也有一些比较著名的应用案例,例如微软的COM技术、Python语言和在游戏脚本领域被广泛应用的Squirrel中都使用了引用计数算法进行内存管理。但是java虚拟机中没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

JavaC#)等主流商用程序语言都是使用可达性分析算法来判定对象是否存活的。这个算法的基本思路如下:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即GC Roots到这个对象不可达)时,则证明此对象是不可用的。

Java语言中,可作为GC Roots的对象包括下面几种:

1. 虚拟机栈中引用的对象。

2. 方法区中类静态属性引用的变量。

3. 方法区中常量引用的对象

4. 本地方法栈中JNI(即一般中的Native方法)引用的对象。

 

16.Java中,如果垃圾回收器判断一个对象是可以被回收的,是不是就会立即对其进行回收呢?

答:不是。即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程,也就是说要通过可达性分析算法对其进行两次判断,如果两次判断的结果都是可回收,那么就会回收。

 



 

 

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。



猜你喜欢

转载自blog.csdn.net/lz1170063911/article/details/80261901