《深入理解JAVA虚拟机》学习笔记

2023-12-31 06:11:26
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的引用链。
    

img

img

5.有哪些垃圾回收算法?
  • (1).标记-清除:

应用场景:

该算法一般应用于老年代,因为老年代的对象生命周期比较长。

该算法有两个阶段。

  1. 标记阶段:找到所有可访问的对象,做个标记

  2. 清除阶段:遍历堆,把未被标记的对象回收

优点:

是可以解决循环引用的问题

必要时才回收(内存不足时)

缺点:

标记和清除的效率不高,尤其是要扫描,尤其是要扫描的对象比较多的时候

会造成内存碎片(会导致明明有内存空间,但是由于不连续,申请稍微大一些的对象无法做到)

  • (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---->

?

img

加载(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.双亲委派模式?

img

img

上图是上面所介绍的这几种类加载器的层次关系,称为类加载器的双亲委派模型.

该模型要求除了顶层的启动类加载器外,其他的类加载器都要有自己的父类加载器.

一言概之,双亲委派模型,其实就是一种类加载的层次关系.

工作过程:

如果一个类加载器收到了类加载的请求,它首先不会自己区尝试加载这个类,而是把这个委派给

加载器去完成,每一个层次的类加载器都是如此.

因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要的类)时,子类加载器才会尝试去加载.

采用双亲委派模式的是好处是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日志文件—>使用分析工具去分析具体线程所占用的内存—>分析具体的数据结构代码—>分析代码—>修改紧急上线

文章来源:https://blog.csdn.net/m0_46419294/article/details/135244748
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。