jvm知识整理
内存结构:
、
1、程序计数器
又称“指令计数器”。中央处理机中的一种寄存器。用来保存计算机执行程序时的指令地址。能根据当前执行指令的要求,将下一条要执行的指令地址存放在程序计数器中,保证计算机有条不紊地工作。此区域占用内存较小,不会有内存溢出的情况。
2、什么是栈?
- 每个线程运行时所需要的内存叫虚拟机栈,栈是先进后出
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
可以在debug时进行查看栈的信息。栈内存每次都会自动回收掉,所以不需要垃圾回收来处理。
方法内的局部变量是否线程安全?
- 如果方法内局部变量没有逃离方法的作用范围,它就是线程安全的。
- 如果局部变量引入了对象或者逃离了方法的作用范围(比如入参或返回),就要考虑线程安全。
栈帧溢出情况:
- 栈帧过多
- 栈帧过大
本地方法栈:java中只提供定义,由非java语言提供实现。
3、什么是堆?
- 是线程共享的,要考虑线程安全的问题
- 有垃圾回收机制
4、栈和堆区别
- 从存储角度来看,堆内存主要用于存储实例对象和对应class文件,栈内存用于存储基本变量和对象的引用。
- 从存储速度来看,栈存取速度快,堆存取比较慢,因为要在运行时动态分配内存,存取速度较慢。
- 从线程角度来看,每个线程都有自己的要给JAVA栈,所有线程共享一个堆。在JVM中,内存被分为线程栈区和堆区。
- 从GC来看,栈区GC比较频繁,堆区GC不频繁。
5、永久代和元空间的区别
- 永久代(1.8之前)使用的是jvm内存存储,而元空间(1.8及之后)使用的是本地内存存储
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
6、运行时常量池和字符串常量池和Class常量池
运行时常量池:
所谓的运行时常量池其实就是将编译后的类信息放入运行时的一个区域中,用来动态获取类信息。
运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
字符串常量池:
本质是一个HashSet<String>,这是一个纯运行时的结构,而且是惰性维护的。注意它只存储String对象的引用,而不存储String对象的内容,根据这个引用可以得到具体的String对象。
Class常量池
主要存放两大类常量:字面量和符号引用。加载Class文件时,Class文件中String对象会进入字符串常量池(这里的进入是指 放入字符串的引用,字符串本身还是在堆中),别的大都会进入运行时常量池。
字面量:
字面量就是指由字母、数字等构成的字符串或者数值。
字面量只可以右值出现,所谓右值是指等号右边的值。
7、StringTable特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理时StringBuilder
- 字符串常量拼接的原理时编译器优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池,串池1.8位于堆空间,1.6位于永久代
8、可达性分析
可达性分析算法的基本思路就是通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可以用的。
GC Roots引用对象:
1、在虚拟机栈中引用的对象
2、在方法区中静态引用指向的对象
3、方法区中常量引用指向的对象
4、Native方法中JNI引用的对象
5、活着的Thread
9、强、软、弱、虚引用
强引用:
普通引用,只要引用存在,垃圾回收器永远不会回收
Object obj = new Object();
//可直接通过obj取得对应的对象 如obj.equels(new Object());
而这样 obj对象对后面new Object的一个强引用,只有当obj这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。
软引用:
非必须引用,内存溢出之前进行回收,可以通过以下代码实现
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有时候会返回null
这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null;
软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
弱引用:
第二次垃圾回收时回收,可以通过如下代码实现
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有时候会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。
弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。
虚引用:
垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回是否从内存中已经删除
虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。
虚引用主要用于检测对象是否已经从内存中删除。
虚引用场景:
有一个DirectBuffer对象,其本身存储于堆内存中,但是会关联一大片堆外内存。
由于堆外内存不会被GC自动回收,因此DB对象创建时会关联一个Cleaner。
当DirectBuffer不再使用,它的虚引用,也就是Cleaner对象(Cleaner继承了虚引用),会被ThreadHandler处理,它的clean()方法会回收直接内存。
10、垃圾回收算法
1、复制算法
复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一半的内存
2、标记清除算法
是JVM垃圾回收算法中最古老的一个,该算法共分成两个阶段,第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,清除未被标记的对象。该算法的缺点是需要暂停整个应用,并且在回收以后未使用的空间是不连续,即内存碎片,会影响到存储。
3、标记整理算法
此算法结合了标记-清楚算法和复制算法的优点,也分为两个阶段,第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题,按顺序排放,同时解决了复制算法所需内存空间过大的问题。
4、分代垃圾回收
98%的对象都是‘朝生夕死’,所以并不需要1:1的划分内存,即我们现在常用的分代收集算法(一次完整的GC流程):
根据对象的存活周期将内存划分为两块,分别为新生代和老年代,然后对各代采用不同的回收算法,在新生代中大部分是‘朝生夕死’的对象,继续将新生代8:2划分为伊甸园区和survival区,其中survival区1:1分成s0和s1两块,采用之前说的复制算法,减少内存碎片的产生。
新生代满了会进行一次minor GC(会引发stop the world) ,minor GC 存活的对象转移到幸存区,survival区满了就会将survival区进行回收,存活的survival区对象复制到另外一块survival区中,并且survival区对象每存活一轮年龄+1当到达阈值年龄就会前往老年代(大对象直接进入老年代)。当老年代空间不足时,会尝试触发minor GC,如果空间仍不足,则会触发full GC/major GC。STW时间更长。
11、为什么要分为Eden和Survivor?为什么要设置两个Survivor区?
如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)
12、常用jvm参数有哪些?
1、-Xms
- 初始大小内存,默认为物理内存 1/64,等价于 -XX:InitialHeapSize
2、-Xmx
- 最大分配内存,默认为物理内存的 1/4,等价于 -XX:MaxHeapSize
3、-Xss
- 设置单个线程栈的大小,一般默认为 512-1024k,等价于 -XX:ThreadStackSize
4、-Xmn
- 设置年轻代的大小
整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小
持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
5、-XX:MetaspaceSize
- 设置元空间大小
元空间的本质和永久代类似,都是对 JVM 规范中的方法区的实现。
元空间与永久代之间最大区别:元空间并不在虚拟机中,而是使用本地内存
因此默认情况下,元空间的大小仅受本地内存限制,元空间默认比较小,我们可以调大一点
6、-XX:+PrintGCDetails
- 输出详细GC收集日志信息
7、-XX:SurvivorRatio
- 设置新生代中 eden 和 S0/S1 空间比例,默认 -XX:SurvivorRatio=8,Eden : S0 : S1 = 8 : 1 : 1
8、-XX:NewRatio
- 配置年轻代和老年代在堆结构的占比,默认 -XX:NewRatio=2 新生代占1,老年代占2,年轻代占整个堆的 1/3
9、-XX:MaxTenuringThreshold
- 设置垃圾最大年龄,晋升阈值
13、三色标记法
白色节点:尚未被标记的对象
黑色节点:已经被标记,且其引用关系已经被处理
灰色节点:已经被标记,但引用关系尚未被处理
14、浮动垃圾
状况:GC 线程遍历到 E(E是灰色),一个业务线程执行了 D.E = null,此时 E 应该被回收的。但是 GC 线程已经认为 E 是灰色了会继续遍历,导致 E 没有被回收。
![]()
漏标
![]()
GC 线程遍历到 E(灰色了)。业务线程执行了 E-->G 断开,D-->G 链接的操作。GC 线程发现 E 无法到达 G,而D因为是黑色不会再遍历标记了。最终导致漏标 G。
漏标的必备两个条件:灰到白断开,黑到白建立。
Object G = E.G; // 第一步 :读Object E.G = null; // 第二步:写Object D.G = G; // 第三步:写
漏标解决方法:将对象 G 存储到特定集合中,等并发标记遍历完毕后再对集合中对象进行重新标记。
CMS:写屏障+增量更新方式
当这个黑色对象增加了对白色对象的引用之后,将它的这个引用,记录下来,在最后标记的时候,再以这个黑色对象为根,对它的引用进行重新扫描.
可以简单理解为,当一个黑色对象增加了对白色对象的引用,那么这个黑色对象就被变灰
这样有一个缺点,就是会重新扫描这个黑色对象的所有引用,比较浪费时间
G1:原始快照方式
当这个灰色对象取消对白色对象的引用之前,将这个引用记录下来,在最后标记的时候,再以这个引用指向的白色对象为根,对它的引用进行扫描
可以简单理解为,当一个灰色对象取消了对白色对象的引用,那么这个白色对象被变灰
这样做的缺点就是,这个白色对象有可能并没有黑色对象去引用它,但是它还是被变灰了,就会导致它和它的引用,本来应该被垃圾回收掉,但是此次GC存活了下来,就是所谓的浮动垃圾.其实这样是比较可以忍受的,只是让它多存活了一次GC而已,浪费一点点空间,但是会比增量更新更省时间
15、CMS和G1
CMS:
采用标记清除算法,在收集垃圾阶段,与工作线程并发执行。
1、初始标记:stop the word, 标记一些和GC Roots相连的对象,速度很快。
2、并发标记:可达性性分析,找到存活对象,期间与工作线程并发执行。
3、重新标记:stop the word, 因为并发标记阶段,工作线程的执行会导致一些对象的标记更改,这个阶段,就是修正这些标记变化的对象。时间比初始标记长,但远低于并发标记。
4、并发清除:清除垃圾对象,与工作线程并发执行,所以没有办法清除一些浮动垃圾。
缺点:
1、依赖CPU
2、无法清除浮动垃圾
3、标记清除会产生大量的内存碎片,所以不适合产生大量对象的新生代区域。
优点:并发执行,低停顿。
G1:
打破了分代模型,虽然还是有新生代,老年代的概念,但内存被划分为一个个区域,新生代,老年代被打散在不同的地方。由于每个区域的收集时间可预测,用户可以指定收集操作时间,GC总能选择一组区域作为收集目标,做到实时收集垃圾和只收集部分区域垃圾。这也是他最大的两个特点,可预测和收集部分区域。

1、初始标记: 停顿线程,标记一些GC Roots连接的对象。
2、并发标记:可达性分析,找到存活对象,与工作线程并发执行。
3、重新标记:线程停止,因为并发标记阶段,工作线程的执行导致一些对象的标记改变,这个阶段就是修正这些兑现标记。
4、并发清除:清除垃圾对象,与工作线程并发执行,所以没有办法清除一些浮动垃圾。
优点:
1、可以充分利用多核CPU。
2、采用标记——整理的清除算法,不会产生内存碎片。
3、对每个Region的收集价值和成本进行排序,根据用户指定的停顿时间回收,因为并发回收和只收集部分垃圾区域,导致时间很短。
16、卡表
有个场景,老年代的对象可能引用新生代的对象(跨代引用),那标记存活对象的时候,需要扫描老年代中的所有对象。因为该对象拥有对新生代对象的引用,那么这个引用也会被称为GC Roots。那不是得又做全堆扫描?成本太高了吧。
HotSpot给出的解决方案是一项叫做卡表(Card Table)的技术。该技术将整个堆划分为一个个大小为512字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位。这个标识位代表对应的卡是否可能存有指向新生代对象的引用。如果可能存在,那么我们就认为这张卡是脏的。
在进行Minor GC的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到Minor GC的GC Roots里。当完成所有脏卡的扫描之后,Java虚拟机便会将所有脏卡的标识位清零。
想要保证每个可能有指向新生代对象引用的卡都被标记为脏卡,那么Java虚拟机需要截获每个引用型实例变量的写操作,并作出对应的写标识位操作。
卡表能用于减少老年代的全堆空间扫描,这能很大的提升GC效率。
17、什么是类加载器?
将对应类的.class文件中的二进制流加载到内存空间。
18、类加载器的分类及作用?
启动类加载器:
负责加载$JAVA_HOME/jre/lib/下核心API或者-Xbootclasspath选项指定的jar包,因为核心Java API的class文件是用于启动Java虚拟机的class文件,所以启动类加载器的名字因此而得.
扩展类加载器:
负责加载$JAVA_HOME/jre/lib/ext/*.jar或者-Djava.ext.dirs 指定目录下的jar包,一般都是加载扩展的api
系统类加载器:
负责加载classpath中的类,也可以通过-classpath选项进行指定
用户自定义类加载器:
可以根据用户的需要定制自己的类加载过程,在运行期进行指定类的动态实时加载。创建用户自定义的类加载器,其中一个重要原因就是能够以定制的方式把类型的全限定名转换成一个Java class文件格式的字节数组。
19、什么是双亲(上级)委派模型?
如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
双亲委派模型的好处:
如果没有双亲委派,那么用户是不是可以自己定义一个java.lang.Object的同名类,java.lang.String的同名类,并把它放到ClassPath中,那么类之间的比较结果及类的唯一性将无法保证,因此,为什么需要双亲委派模型?防止内存中出现多份同样的字节码。
其次是考虑到安全因素。假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
如何打破双亲委派:重写loadClass()方法。

浙公网安备 33010602011771号