1.java内存结构,以及每个结构的作用?
-
线程共享区
堆内存:所有的对象实例都要在堆上分配 方法区:是各个线程共享的内存区域, 它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
-
非线程共享区
Java虚拟机栈:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息 本地方法栈:虚拟机调用本地方法的区域 程序计数器:当前线程所执行的字节码的行号指示器
java1.8:
以后在JDK1.8中元空间区取代了永久代,永久代原本主要存放Class和Meta的信息。
而元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
因此,默认情况下,元空间的大小仅受本地内存限制。
2.什么是java垃圾回收机制?
java内存是有限的,需要不定时去回收不可达的对象,如果不进行垃圾回收,
内存迟早会被消耗空。
3.垃圾回收的过程,以堆内存结构去分析?
新生代(1/3) 老年代(2/3)
新生代分为:Eden伊甸园、from(s0)、to(s1)
(8/10) (1/10) (1/10)
1).判断哪些对象是垃圾
2).垃圾回收算法
4.如何判断对象是否存活?
-
引用计数法:
如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到环的存在。 首先需要声明,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存。 什么是引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时, 计数器值加1;当引用失效时,计数器值减1.任何时刻计数器值为0的对象就是不可能再被使用的。 那为什么主流的Java虚拟机里面都没有选用这种算法呢? 其中最主要的原因是它很难解决对象之间相互循环引用的问题
-
根搜索算法:
根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点, 从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain), 当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。 那么问题又来了,如何选取GCRoots对象呢?在Java语言中,可以作为GCRoots的对象包括下面几种: (1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。 (2). 方法区中的类静态属性引用的对象。 (3). 方法区中常量引用的对象。 (4). 本地方法栈中JNI(Native方法)引用的对象。 下面给出一个GCRoots的例子,如下图,为GCRoots的引用链。
5.有哪些垃圾回收算法?
- (1).标记-清除:
应用场景:
该算法一般应用于老年代,因为老年代的对象生命周期比较长。
该算法有两个阶段。
-
标记阶段:找到所有可访问的对象,做个标记
-
清除阶段:遍历堆,把未被标记的对象回收
优点:
是可以解决循环引用的问题
必要时才回收(内存不足时)
缺点:
标记和清除的效率不高,尤其是要扫描,尤其是要扫描的对象比较多的时候
会造成内存碎片(会导致明明有内存空间,但是由于不连续,申请稍微大一些的对象无法做到)
- (2).复制算法:
复制算法一般使用在新生代中,因为新生代中的对象一般都是朝生夕死,存活的对象并不多,
这样使用coping算法进行拷贝时效率高。
如果jvm使用了cpying算法,一开始就会将可用内存分为两块,from(s0)域和to(s1)域,
每次只是使用from域,to域则空闲着。当from域内存不够了,开始执行GC操作,这个时候,
会把from域存活的对象拷贝到to域,然后直接把from域进行清理
步骤:1.当Eden区满的时候会触发第一个young gc,把还活着的对象拷贝到From;
当Eden区再次触发young gc的时候,会扫描Eden区和From区域,对两个区域进行垃圾回收,
经过这次回收后还存活的对象,直接复制到To区域,并将Eden和From区域清空。
2.当后续Eden又发生young gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,
并将Eden和To区域清空。
3.可见部分对象会在From和To区域中复制来复制去,如此交换15次(由jvm参数MaxTenuringThreshold决定,
这个参数默认值是15),最终如果还是存活,就存入到老年代。
注意!!!:万一存活对象比较多,那么To域的内存可能不够存放,这个时候会借助老年代的空间。
优点:在存活对象不多的情况下,性能高,能解决内存碎片和标记清除算法中导致更新的问题。
缺点:会造成一部分内存浪费,因为有一部分内存是空的,不过可以根据实际情况,将内存块大小
比例适当调整;如果存活对象的数量比较大,coping的性能会变得很差。
- (3).标记-压缩算法:
标记清除算法和标记压缩算法非常相同,
但是标记压缩算法在标记清除算法之上解决内存碎片化
优点:解决内存碎片问题
缺点:压缩阶段,由于移动了可用对象,需要去更新引用。
- (4).分代算法:
当前商业虚拟机的GC都是采用的’分代收集算法’,这并不是什么新的思想,只是根据对象的
存活周期的不同将内存划分为几块儿。
少量对象存活,适合复制算法,一般用在新生代
大量对象存活,适用于标记整理算法/标记清除算法
6.Minor GC和MajorGC、Full GC区别
年轻代满时会触发Minor GC,这里的年轻代满是指Eden区满。
老年代满时会触发MajorGC,只有CMS收集器会有单独收集老年代的行为,其他收集器均无此行为。
FullGC是针对新生代,老年代和方法区(元空间)的垃圾收集。FullGC产生的条件:
(1)调用System.gc时,系统建议执行Full GC,但是不一定会执行 。
(2)老年代空间不足。
(3)方法区空间不足,类卸载(类卸载三个条件)。
(4)通过 Minor GC 后进入老年代的空间大于老年代的可用内存
(5)内存空间担保。
7.JVM会在永久代中发生垃圾回收吗?
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。
如果你仔细查看垃圾回收器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的
永久代大小对避免Full GC是非常重要的原因。(注意:java8中已经移除了永久代,新加了一个叫元数据区的
native内存区)
8.OutOfMemoryError异常如何解决?
- java堆溢出
错误原因:java.lang.OutOfMemoryError: Java heap space 堆内存溢出
解决办法:设置堆内存大小 // -Xms1m -Xmx10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
- 虚拟机栈溢出
错误原因:java.lang.StackOverflowError 栈内存溢出
栈溢出 产生于递归调用,循环遍历是不会的,但是循环方法里面产生递归调用,也会发生栈溢出。
解决办法:设置线程的最大深度调用
-Xss5m 设置最大调用深度
9.内存溢出和内存泄露的区别?
- 内存溢出(Out Of Memory) :就是申请内存时,JVM没有足够的内存空间。通俗说法就是去蹲坑发现坑位满了。
- 内存泄露 (Memory Leak):就是申请了内存,但是没有释放,导致内存空间浪费。通俗说法就是有人占着茅坑不拉屎。
10.有哪些常用的垃圾回收器?
serial串行收集器、ParNew收集器、parallel收集器、cms收集器、g1收集器
11.jvm调优思路?
初始堆值和最大堆内存内存越大,吞吐量就越高。
最好使用并行收集器,因为并行收集器速度比串行吞吐量高,速度快。
设置堆内存新生代的比例和老年代的比例最好为1:2或者1:3。
减少GC对老年代的回收。
12.类加载的过程?
一个类从加载到内存开始,一直到被卸载结束,它的整个生命周期包括加载,连接
加载—>验证—>准备—>解析—>初始化—>使用—>卸载
<------连接Linking---->
加载(Load):类加载过程的一个阶段,ClassLoader通过一个类的完全限定名查找此类字节码文件,
并利用字节码文件创建一个class对象
验证(Verify):目的在于确保class文件的字节流中包含符合当前虚拟机要求,不会危害虚拟机自身的安全,主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证。
验证一个Class的二进制内容是否合法
准备(Prepare):
在准备阶段,虚拟机会在方法区中为Class分配内存,并设置static成员变量的初始值为默认值.
注意这里仅仅会为static变量分配内存(static变量在方法区中),并且初始化static变量的值为其所属类型的默认值.如:int类型初始化为0,引用类型初始化为null.即使声明了这样一个static变量:
public static int a=123;
在准备阶段后,a在内存中的值仍然是0,赋值123这个操作会在中初始化阶段执行,因此在初始化阶段产生了对应的Class对象之后a的值才是123.
解析(Resolve):解析阶段,虚拟机会将常量池中的符号引用替换为直接引用,解析主要针对的是类,接口,方法,成员变量等符号引用. 在转换成直接引用后,会触发校验阶段的符号引用验证,验证转换之后的直接引用是否能找到对应的类,方法,成员变量等. 这里也可见类加载的各个阶段在实际过程中,可能是交错执行.
初始化(Initialize):初始化阶段是类加载过程的最后一步,这个阶段才开始的真正的执行用户定义的java程序.在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则需要为类变量(非final修饰的变量)和其他变量赋值,其实就是执行类的()方法.
初始化阶段即开始在内存中构造一个Class对象来表示该类,即执行类构造器()的过程。
13.双亲委派模式?
上图是上面所介绍的这几种类加载器的层次关系,称为类加载器的双亲委派模型.
该模型要求除了顶层的启动类加载器外,其他的类加载器都要有自己的父类加载器.
一言概之,双亲委派模型,其实就是一种类加载的层次关系.
工作过程:
如果一个类加载器收到了类加载的请求,它首先不会自己区尝试加载这个类,而是把这个委派给
加载器去完成,每一个层次的类加载器都是如此.
因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要的类)时,子类加载器才会尝试去加载.
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次
14.双亲委派模式优势
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常
15.线上如何去排查分析内存溢出的问题?
设置虚拟机参数生成堆内存dump日志文件—>使用分析工具去分析具体线程所占用的内存—>分析具体的数据结构代码—>分析代码—>修改紧急上线