# 以下问题仅作为知识记录,有问题大家可以留言或私信一起讨论学习!
1. 什么情况下会发生栈内存溢出?
-
栈是线程私有的,栈的生命周期和线程是一样的,每一个方法执行的时候,都会创建一个栈帧,它包含了局部变量表,操作数栈,动态链接方法的出口等等。局部变量表又包含基本数据类型和对象的引用。
-
当线程请求超过虚拟机允许的最大深度的时候,就会报栈内存溢出。
比如:方法递归调用的时候就会触发栈内存溢出。
解决:
可以通过调整参数来调整JVM的栈的大小。
2.说说JVM的内存模型?
JVM将虚拟机分为5大区域,主要是程序计数器、虚拟机栈、本地方法栈、堆、方法区。
-
程序计数器是线程私有的,非常小的内存空间,当前线程的一些行号的指示器,用来记录当前虚拟机正在执行的一些线程指令地址,同时不会发生内存溢出的情况
-
虚拟机栈也是线程私有的,每一个方法在执行的时候都会创建一个栈帧,存储的是局部变量表、操作数、动态链接、方法返回等信息。
-
本地方法栈的线程私有的,保存的是native的方法的相关信息。当JVM创建一个线程,调用一个native的方法之后,JVM不会为该方法在虚拟机中创建栈帧的,而是简单的动态链接,并直接调用这个方法。
-
堆是所有线程共享的最大块的内存,几乎所有对象的实例和数组都在堆上分配内存,所以这个区域会经常发生一些垃圾回收的操作。
-
方法区存放的是已被加载的类信息(类名等),常量、静态变量、即时编译器编译之后的代码数据。在JDK1.8中就不存在方法区了,而是叫元数据区,元数据区被分为两个部分,第一部分是加载类的信息,第二部分是运行时的一些常量池;加载类的信息被保存在元数据区中,运行中的常量池被保存在堆中。
3.JVM中一次完整的GC是什么样子的?对象如何晋升到老年代?
堆分为新生代和老年代,新生代又分为Eden区和幸存者区(Survivor),幸存者区(Survivor)又分为Survivor0(to)区和Survivor1(from)区,Survivor0:Survivor1:Eden = 1:1:8 ; 当Eden区空间满的时候,就会触发一次Minor GC(轻量级的GC)来收集新生代的垃圾,存活的对象就会被分配到幸存者(Survivor)区,幸存者区之间是采用复制算法来进行复制对象的。对于大对象来说,需要大量连续的内存空间,会直接被分配到老年区。如果对象在Eden区出生,并且经过一次Minor GC之后,如果仍然存活,被分配到Survivor区的话,那么它的年龄会进行加一,按照计算年龄的方式,来确定对象存活的周期。此后每经过一次minor GC,并且存活下来,年龄就进行加一,当年龄达到15的时候,就会晋升到老年代,大对象除外,直接进入到老年代。当老年代满的时候,无法容纳更多对象,这个时候就会触发FULL GC,Major GC是发生在老年代的GC,用来清理老年代区的,经常会伴随着Minor GC进行垃圾回收。
4.聊一聊Java中的回收算法?
Java中有四种垃圾回收算法:
-
标记清除法
- 标记整理法
- 复制算法
- 分代收集算法
1.标记清除法
- 标记:利用可达性分析来遍历内存,把存活的对象和垃圾对象进行标记;
- 清除:再遍历一遍内存,把标记的垃圾对象进行回收。
**特点:**
1. 标记和清除两个过程都比较耗时,效率不高,
2. 标记和清除过后会产生大量的不连续的空间碎片,这样会导致程序运行的时候,需要分配大对象而找不到连续内存空间,不得不触发一次GC
**使用:**
CMS垃圾收集器
2.标记整理法
- 标记:利用可达性分析来遍历内存,把存活的对象和垃圾对象进行标记;

- 整理:将所有存活的对象向一端移动,边界以外的对象都会回收掉,把对象的存储空间进行整理。

**特点:**
适用于存活对象多,垃圾少这种情况,需要整理过程所消耗的时间成本比较高一些,不会产生空间碎片。
**使用:**
老年代Serial Old,Parallel Old
3.复制算法
将内存中按照容量大小相等的两块内存,像新生代中的Survivor区,每次只使用其中一块
当这一块使用完之后,还存活的对象就会转移到另一块,把它有序的进行整理,然后把使用过的内存空间进行移除。
特点:
不会产生碎片空间,但是需要浪费一块相同大小的内存空间。空间利用率降低。
使用
Serial、ParNew、Parallell
4.分代收集算法
根据对象的存活周期不同,把内存划分为几块。JAVA虚拟机里面就会分为新生代和老年代,新生代中如果大量对象被回收,和少量对象存活的话,它会采用复制算法,只需要付出少量对象的复制成本就可以完成垃圾收集。老年代因为对象的存活率比较高,没有额外的空间对他进行分配和担保,它会采用标记清除或者标记整理的算法来进行回收。
5.常用的调优参数
-Xms 和 -Xmx (-XX:InitialHeapSize 和 -XX:MaxHeapSize):指定JVM初始占用的堆内存和最大堆内存。
# JVM也是一个软件,也必须要获取本机的物理内存,然后JVM会负责管理向操作系统申请到的内存资源。JVM启动的时候会向操作系统申请 -Xms 设置的内存,JVM启动后运行一段时间,如果发现内存空间不足,会再次向操作系统申请内存。JVM能够获取到的最大堆内存是-Xmx设置的值。
-XX:NewSize 和 -Xmn(-XX:MaxNewSize):指定JVM启动时分配的新生代内存和新生代最大内存。
-XX:SurvivorRatio:设置新生代中1个Eden区与1个Survivor区的大小比值。
# 在hotspot虚拟机中,新生代 = 1个Eden + 2个Survivor。如果新生代内存是10M,SurvivorRatio=8,那么Eden区占8M,2个Survivor区各占1M。
-XX:NewRatio:指定老年代/新生代的堆内存比例。
# 在hotspot虚拟机中,堆内存 = 新生代 + 老年代。如果-XX:NewRatio=4 表示年轻代与年老代所占比值为1:4,年轻代占整个堆内存的1/5。
# 在设置了-XX:MaxNewSize(设置新生代的最大内存)的情况下,-XX:NewRatio的值会被忽略,
# 老年代的内存=堆内存 - 新生代内存。
# 老年代的最大内存 = 堆内存 - 新生代 最大内存。
-XX:OldSize:设置JVM启动分配的老年代内存大小,类似于新生代内存的初始大小-XX:NewSize。
-XX:PermSize 和 -XX:MaxPermSize:指定JVM中的永久代(方法区)的大小。
# 永久代不属于堆内存,堆内存只包含新生代和老年代。
-XX:MaxTenuringThreshold:对象从年轻代晋升到老年代经过GC次数的虽大阈值。
6.垃圾回收之新生代垃圾收集器
JVM的运行模式:Client模式和Server模式
1.Serial收集器
采用的是复制算法-单线程-Client模式
Serial收集器(-XX:+UseSerialGC,复制算法)
单线程收集,进行垃圾收集时,必须暂停所有工作线程
简单高效,Client模式下默认的年轻代收集器
2.ParNew收集器
采用的是复制算法-多线程-Client模式
ParNew收集器(-XX:+UseParNewGC,复制算法)
多线程收集,进行垃圾收集时,必须暂停所有工作线程
单核执行效率不如Serial,在多核下执行才有优势
3.Parallel Scavenge收集器
采用的是复制算法-多线程-Server模式
Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)
比起关注用户线程停顿时间,更关注系统的吞吐量
在多核下执行才有优势,Server模式下默认的年轻代收集器
7.垃圾回收之老年代垃圾收集器
1.Serial Old收集器
采用的是标记整理算法-单线程-Client模式
Serial Old收集器(-XX:+UseSerialOldGC,标记-整理算法)
单线程收集,进行垃圾收集时,必须暂停所有工作线程
简单高效,Client模式下默认的老年代收集器
2.Parallel Old收集器
采用的是标记整理算法-多线程
Parallel Old收集器(-XX:+UseParallelOldGC,标记-整理算法)
多线程,吞吐量优先
3.CMS收集器
采用的是标记清除算法-多线程
CMS收集器(-XX:+UseConcMarkSweepGC,标记-清除算法)
优点:
几乎可以和用户线程做到同时工作,本质上还是要STOP-THE WORLD,只是停顿时间短
如果在老年代中存活更多存活率高的对象,使用CMS收集器更合适。
收集流程:(一边掉垃圾一边打扫)
**初始标记:**stop-the-world,从垃圾回收的根对象开始,只是扫描和根对象有关联的对象并做标记。虽然暂停了所有的线程工作,但是停顿时间很短。
**并发标记:**并发追随标记,程序不会停顿
**并发预清理:**查找执行并发标记阶段从年轻代晋升到老年代的对象
**重新标记:**暂停虚拟机,扫描CMS堆中的剩余对象
**并发清理:**清理垃圾对象,程序不会停顿
**并发重置:**重置CMS收集器的数据结构
注意:
CMS 收集器是以低停顿为目标,而 Parallel Scavenge 收集器收集器是以吞吐量为目标,两个垃圾收集器是不同的架构,所以两个不能够并用,在多 CPU 环境中,CMS 收集器一般与 ParNew 收集器一起使用的场景比较多。
4.G1垃圾收集器
G1垃圾收集器(-XX:+UseG1GC , 复制+标记-整理算法)
G1 (Garbage-First)是一款面向服务端应用的垃圾收集器,主要使命是代替 CMS 收集器。G1 将 Java 堆划分为多个大小相等独立的区域 (Region)。
G1收集器详解
G1 还保留着新生代和老年代的概念,但是新生代和老年代不再是物理隔离了,它们都是一部分 Region (不需要连续)的集合。
运行效果图
详解
上面有一个特殊的区域,Humongous 区域。如果一个对象占用的空间超过了分区容量的 50% 以上,G1 收集器就认为这个是一个巨型对象。
这些巨型对象默认的是直接分配在老年代里面的,但是如果这个巨型对象是一个短期的对象,放在老年代里面就会对垃圾收集器造成影响,所以 G1 就划分了一个 Humongous 区域,专门存放巨型对象,如果一个区域放不了,就会寻找连续的区域存放。
在 Java 中,对象的引用不可能只是单个 Region 之间的对象引用,而是所有的 Region 对象之间的引用。这样的话,貌似需要全局扫描,才能够判断对象是否被引用。
G1 收集器使用了 Remembered Set 来避免全堆扫描。它是用来记录外部指向本 Region 的所有引用,每个 Region 维护一个 Remember Set。
G1收集器回收步骤
在不考虑上面维护 Remembered Set 的操作的时候,G1 收集器分为下面四步:
初始标记:Stop The World。标记一下 GC Roots 能直接关联到的对象。
并发标记:和业务线程一起执行 。从 GC Roots 开始对堆中的对象进行可达性分析,找出存活的对象。
最终标记:Stop The World。修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录。
筛选回收:和业务线程一起执行。对各个 Region 的回收价格和回收成本进行排序,根据用户期望的 GC 停顿时间来制定回收计划。
8.如何判断一个对象是否存活?
-
引用计数法
比较老的判断对象是否存活的一种方式,给每一个对象设置一个引用计数器,当有一个地方引用该对象的时候,计数器就会加一,引用失效就会减一,当引用计数器为0的时候,这个对象就是垃圾对象。
缺点:没有办法解决循环引用的问题。
-
可达性分析
从一个GCROOT(是一个集合),把对象进行标记,往下搜索,如果一个对象到GCROOT没有任何引用链的链接的时候,说明对象不可用。
可以作为GCROOT的对象:
1. 虚拟机栈中引用的对象; 2. 方法区中静态属性引用的变量; 3. 方法区常量池中引用的对象; 4. 本地方法栈引用的对象。
满足上述条件就可以被回收吗?不是的,还需要进行两次的标记,
第一次:判断当前对象是否有finalize方法,并且该方法没有被执行过,如果不存在则标记为垃圾对象,等待回收。如果有的话,则进行第二次标记。
第二次:当前对象放入一个Finalize-Queue的队列之中,并生成一个finalize线程去执行这个方法,但是虚拟机不保证这个方法一定被执行到,因为如果线程执行缓慢,或者是进入死锁,就会导致回收系统崩溃。如果执行过finalize方法之后,没有与GCROOT有直接或者间接的引用,就被标记为垃圾对象。
可达性分析:采用的是标记清除算法。
9.项目中怎么优化JVM的?(持续更新)
a.线上启动时内存过小问题
项目在线上进行启动的时候,报错java.lang.OutOfMemoryError: Java heap space ,
在JVM中如果98%的时间是用于GC且可用的 Heap size 不足2%的时候将抛出此异常信息。
JVM堆的设置是指java程序运行过程中JVM可以调配使用的内存空间的设置.
JVM在启动的时候会自动设置Heap size的值,其初始空间(即-Xms)是物理内存的1/64,最大空间(-Xmx)是物理内存的1/4。可以利用JVM提供的-Xmn -Xms -Xmx等选项可进行设置。
解决办法:
tomcat_home/bin下catalina.bat(win)或catalina.sh(linux)执行代码前加上:
set JAVA_OPTS=%JAVA_OPTS% -Xms128m -Xmx512m
10.class类文件如何加载到虚拟机?
答:一个java文件经过编译之后生成一个class文件,想要把class文件加载到虚拟机
第一步:需要先进行类的装载,通过双亲委派机制获得一个需要加载的类,先把类的信息(名称,创建时间等)、常量,静态变量及编译器即时编译之后的代码加载到jvm的方法区;然后把class类的信息加载到jvm的堆内存中。
第二步:需要进行链接,链接就是先进行验证class类的正确性。然后为类的静态变量分配内存,将其初始化为默认值,将class类中的符号引用转化为直接引用。
第三步:初始化操作,对类的静态变量,静态代码块执行初始化操作。
11.双亲委派机制
当一个类加载器在接到加载类的请求时,他首先不会自己去尝试加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有在父类加载器无法完成加载任务时,才自己去加载。
加载器自下而上有四种:Custom ClassLoader、App Classloader、Extension ClassLoader、Bootstrap ClassLoader.
12.方法区、堆、虚拟机栈、程序计数器、本地方法栈都是什么,各有什么特点?
方法区:
方法区是各个线程共享的内存区域,在虚拟机启动的时候就创建了。
存储内容:已被虚拟机加载的类信息,常量、静态变量、即时编译器编译后的代码等
堆:
堆也是被所有线程所共享,在虚拟机启动的时候就创建了。
存储内容:对象实例以及数组都在堆上分配。
虚拟机栈:
虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态。虚拟机栈是线程私有的,随着线程的创建而创建。
每一个被线程执行的方法,为该栈中的栈帧,即每一个方法对应着每一个栈帧。
调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。
程序计数器:
程序计数器占用的内存空间很小,由于java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时间的方式来实现的。在任意时刻,一个处理器只会执行一条线程中的指令。因此为了线程切换后能够恢复到正确的执行位置,每条线程都需要一个独立的程序计数器来记录正在执行的虚拟机字节码指令的地址。
如果正在执行的是Native方法,则这个计数器为空。
本地方法栈:
如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中进行执行。
13.Young区为什么还需要Survivor区?只有Eden区不行吗?
如果没有Survivor,Eden区每进行一次Minor GC,并且没有年龄限制的话,存活的对象就会被送到老年代。
这样一来,老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。
老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。
执行时间长有什么坏处?
频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。
可能你会说,那就对老年代的空间进行增加或者较少咯。
假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一旦发生Full GC,执行所需要的时间更长。
假如减少老年代空间,虽然Full GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。
所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代。
14.为什么需要两个Survivor区?
最大的好处就是解决了碎片化。
也就是说为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设 现在只有一个Survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循 环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的 存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。 永远有一个Survivor space是空的,另一个非空的Survivor space无碎片。
15.新生代中Eden:S1:S2为什么是8:1:1?
新生代中的可用内存:复制算法用来担保的内存为9:1
可用内存中Eden:S1区默认为8:1
即新生代中Eden:S1:S2 = 8:1:1
16.垃圾收集发生的时机
(1)当Eden区或者S区不够用了
(2)老年代空间不够用了
(3)方法区空间不够用了
(4)System.gc()
GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。 当然,我们可以手动进行垃圾回收,比如调用System.gc()方法通知JVM进行一次垃圾回收,但是具体什么时刻运行也无法控制。也就是说System.gc()只是通知要回收,什么时候回收由JVM决定。 但是不建议手动调用该方法,因为消耗的资源比较大。
17.JVM性能优化
18.内存泄漏与内存溢出的区别
内存泄漏:对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。
内存溢出:内存泄漏到一定的程度就会导致内存溢出,但是内存溢出也有可能是大对象导致的。
19.young gc会有stw吗?
不管什么 GC,都会有 stop-the-world,只是发生时间的长短。
20.major gc和full gc的区别
major gc指的是老年代的gc,而full gc等于young+old+metaspace(元空间)的gc。
21.G1与CMS的区别是什么
CMS 用于老年代的回收,而 G1 用于新生代和老年代的回收。
G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的产生。
22.什么是直接内存
直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。
23.不可达的对象一定要被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机 将这两种情况视为没有必要执行。被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
24.方法区中的无用类回收
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 无用的类 :
-
该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
-
加载该类的 ClassLoader 已经被回收。
-
该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
25.强引用、软引用、弱引用和虚引用
在JDK1.2以前的版本中,当一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及状态,程序才能使用它。这 就像在日常生活中,从商店购买了某样物品后,如果有用,就一直保留它,否则就把它扔到垃圾箱,由清洁工人收走。一般说来,如果物品已经被扔到垃圾箱,想再把它捡回来使用就不可能了。但有时候情况并不这么简单,你可能会遇到类似鸡肋一样的物品,食之无味,弃之可惜。这种物品现在已经无用了,保留它会占空间,但是立刻扔掉它也不划算,因 为也许将来还会派用场。对于这样的可有可无的物品,一种折衷的处理办法是:如果家里空间足够,就先把它保留在家里,如果家里空间不够,即使把家里所有的垃 圾清除,还是无法容纳那些必不可少的生活用品,那么再扔掉这些可有可无的物品。
从JDK1.2版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
1.强引用
以前我使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
2.软引用(SoftReference)
如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。
3.弱引用(WeakReference)
如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
4.虚引用(PhantomReference)
"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
特别注意,在实际程序设计中一般很少使用弱引用与虚引用,使用软用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
以下是软引用的代码:
import java.lang.ref.SoftReference;
public class Test {
public static void main(String[] args){
System.out.println("开始");
A a = new A();
SoftReference<A> sr = new SoftReference<A>(a);
a = null;
if(sr!=null){
a = sr.get();
}
else{
a = new A();
sr = new SoftReference<A>(a);
}
System.out.println("结束");
}
}
class A{
int[] a ;
public A(){
a = new int[100000000];
}
}
26.简述 Java 垃圾回收机制
在 Java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM 中,有一个垃圾回收线程,它是低 优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当 前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象, 并将它们添加到要回收的集合中进行回收。
27.垃圾回收的优点和原理。并考虑 2 种回收机制。
Java 语言中一个显著的特点就是引入了垃圾回收机制,使 C++ 程序员最头疼的内存管理的问题迎刃而解,它使得 Java 程序员在 编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制, Java 中的对象不再有“作用域”的概念,只有对象的引用才有" 作用域"。
垃圾回收可以有效的防止内存泄露,有效的使用可以使 用的内存。垃圾回收器通常是作为一个单独的低级别的线程运行, 不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的 对象进行清楚和回收,程序员不能实时的调用垃圾回收器对某个对 象或所有对象进行垃圾回收。 回收机制有分代复制垃圾回收和标记垃圾回收,增量垃圾回收。
28. 垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗? 有什么办法主动通知虚拟机进行垃圾回收?
对于 GC 来说,当程序员创建对象时,GC 就开始监控这个对象 的地址、大小以及使用情况。通常,GC 采用有向图的方式记录和 管理堆(heap)中的所有对象。通过这种方式确定哪些对象是” 可达的”,哪些对象是”不可达的”。当 GC 确定一些对象为“不 可达”时,GC 就有责任回收这些内存空间。可以。程序员可以手 动执行 System.gc(),通知 GC 运行,但是 Java 语言规范并不 保证 GC 一定会执行。
29.Java 中会存在内存泄漏吗,请简单描述。
所谓内存泄露就是指一个不再被程序使用的对象或变量一直被占 据在内存中。Java 中有垃圾回收机制,它可以保证一对象不再被 引用的时候,即对象变成了孤儿的时候,对象将自动被垃圾回收器 从内存中清除掉。由于 Java 使用有向图的方式进行垃圾回收管理, 可以消除引用循环的问题,例如有两个对象,相互引用,只要它们 和根进程不可达的,那么 GC 也是可以回收它们的,例如下面的 代码可以看到这种情况的内存回收:
package com.journaldev.examples;
import java.io.IOException;
public class GarbageTest {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
try {
gcTest();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("has exited gcTest!");
System.in.read();
System.in.read();
System.out.println("out begin gc!");
for (int i = 0; i < 100; i++) {
System.gc();
System.in.read();
System.in.read();
}
}
private static void gcTest() throws IOException {
System.in.read();
System.in.read();
Person p1 = new Person();
System.in.read();
System.in.read();
Person p2 = new Person();
p1.setMate(p2);
p2.setMate(p1);
System.out.println("before exit gctest!");
System.in.read();
System.in.read();
System.gc();
System.out.println("exit gctest!");
}
private static class Person {
byte[] data = new byte[20000000];
Person mate = null;
public void setMate(Person other) {
mate = other;
}
}
}
Java 中的内存泄露的情况:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是 Java 中内存泄露的发生场景,通俗地说,就是程序员可能创 建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是 java 中可能出现内存泄露的情况。
例如,缓存系统,我们加载了一个对象放在缓存中 (例如放在一个全局 map 对象中),然后一直不再使用它,这个对象一直被缓存引用,但却不再被使用。 检查 Java中的内存泄露,一定要让程序将各种分支情况都完整执行到程序结束,然后看某个对象是否被使用过,如果没有,则才能 判定这个对象属于内存泄露。
如果一个外部类的实例对象的方法返回了一个内部类的实例对象, 这个内部类对象被长期引用了,即使那个外部类实例对象不再被使 用,但由于内部类持久外部类的实例对象,这个外部类对象将不会 被垃圾回收,这也会造成内存泄露。
下面内容来自于网上(主要特点就是清空堆栈中的某个元素,并不 是彻底把它从数组中拿掉,而是把存储的总数减少,本人写得可以 比这个好,在拿掉某个元素时,顺便也让它从数组中消失,将那个 元素所在的位置的值设置为 null 即可):
public class Stack {
private Object[] elements = new Object[10];
private int size = 0;
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity() {
if (elements.length == size) {
Object[] oldElements = elements;
elements = new Object[2 * elements.length + 1];
System.arraycopy(oldElements, 0, elements, 0,
size);
}
}
}
上面的原理应该很简单,假如堆栈加了 10 个元素,然后全部弹 出来,虽然堆栈是空的,没有我们要的东西,但是这是个对象是无 法回收的,这个才符合了内存泄露的两个条件:无用,无法回收。 但是就是存在这样的东西也不一定会导致什么样的后果,如果这个 堆栈用的比较少,也就浪费了几个 K 内存而已,反正我们的内存都 上 G 了,哪里会有什么影响,再说这个东西很快就会被回收的, 有什么关系。下面看两个例子。
public class Bad {
public static Stack s = Stack();
static {
s.push(new Object());
s.pop(); //这里有一个对象发生内存泄露
s.push(new Object()); //上面的对象可以被回收了,等于是自愈了
}
}
因为是 static,就一直存在到程序退出,但是我们也可以看到它有 自愈功能,就是说如果你的 Stack 最多有 100 个对象,那么最 多也就只有 100 个对象无法被回收其实这个应该很容易理解, Stack 内部持有 100 个引用,最坏的情况就是他们都是无用的, 因为我们一旦放新的进取,以前的引用自然消失!
内存泄露的另外一种情况:
当一个对象被存储进 HashSet 集合中 以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否 则,对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希 值就不同了,在这种情况下,即使在 contains 方法使用该对象的 当前引用作为的参数去 HashSet 集合中检索对象,也将返回找不 到对象的结果,这也会导致无法从 HashSet 集合中单独删除当前 对象,造成内存泄露。
30.深拷贝和浅拷贝
简单来讲就是复制、克隆。
Person p=new Person(“张三”);
浅拷贝就是对对象中的数据成员进行简单赋值,如果存在动态成员或者指针就会报错。
深拷贝就是对对象中存在的动态成员或指针重新开辟内存空间。
31.System.gc() 和 Runtime.gc() 会做什么事情?
这两个方法用来提示 JVM 要进行垃圾回收。但是,立即开始还是 延迟进行垃圾回收是取决于 JVM 的
32.如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占 用的内存?
不会,在下一个垃圾回收周期中,这个对象将是可被回收的。
33.Java对象什么时候可以被垃圾回收?JVM的永久代中会发生垃圾回收么?
当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。
34.什么是类加载器,类加载器有哪些?
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
主要有一下四种类加载器:
• 启动类加载器(Bootstrap ClassLoader)用来加载 Java 核 心类库,无法被 Java 程序直接引用。
• 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类 加载器在此目录里面查找并加载 Java 类。
• 系统类加载器(system class loader):它根据 Java 应用 的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它。
• 用户自定义类加载器,通过继承 java.lang.ClassLoader 类 的方式实现。