Java~阅读《深入理解Java虚拟机》知识点总结

Posted Listen-Y

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java~阅读《深入理解Java虚拟机》知识点总结相关的知识,希望对你有一定的参考价值。

文章目录

前言

  • 对于一名Java程序员,非常有必要去了解JVM虚拟机,只有在了解其内部机制,其内存分布,其运行机制,才有可能在实际的项目中去做出合适的量级优化
  • 近段时间阅读了一下《深入理解Java虚拟机》这本书,有机会也建议学Java的同志们去学习一下,此篇文章仅作为我的学习笔记,有机会大家还是阅读原书收获会更大

第一部分(Java程序内部机制)

  • 我们都知道Java的所有程序在运行前都会进行加载, 而加载其实就是把存放于存储中的编译好的字节码文件加载到JVM内存运行环境中

1.1Java运行时一个类是什么时候被加载的?

  • 一个类在什么时候开始被加载, 并没有进行强制约束,交给了虚拟机自己去自由实现, 而虚拟机的本质原则就是按需加载, 即在需要使用这个类的时候就去加载这个类,比如:
  1. 初始化这个类的实例
  2. 反射这个类
  3. 使用这个类的类对象
  4. 使用这个类的类加载器
  5. 使用这个类的静态变量
  6. 使用这个类的内部类
  • -XX:+TraceClassLoading这个参数可以帮助我们打印在运行java程序的时候帮我们显示加载了什么类


1.2JVM一个类的加载过程?

  • 一个类从加载到jvm内存,到从jvm内存卸载,它的整个生命周期会经历7个阶段:
    1、加载(Loading)
    2、验证(Verification)
    3、准备(Preparation)
    4、解析(Resolution)
    5、初始化(Initialization)
    6、使用(Using)
    7、卸载(Unloading)
    其中验证、准备、解析三个阶段统称为连接(Linking);
  • 加载:classpath、jar包、网络、某个磁盘位置下的类的class二进制字节流读进来,在内存中生成一个代表这个类的java.lang.Class对象放入元空间,此阶段我们程序员可以干预,我们可以自定义类加载器来实现类的加载;
  • 验证:验证Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证虚拟机的安全;
  • 准备:类变量赋默认初始值,int为0,long为0L,boolean为false,引用类型为null;常量赋正式值;
  • 解析:把符号引用翻译为直接引用, 把一个对象的class文件读进来变成一个java.long.class对象,会有一个随机的符合引用,需要在元空间给他一个属于他的直接引用;
  • 初始化:当我们new一个类的对象,访问一个类的静态属性,修改一个类的静态属性,调用一个类的静态方法,用反射API对一个类进行调用,初始化当前类,其父类也会被初始化… 那么这些都会触发类的初始化;
  • 使用:使用这个类;
  • 卸载
  1. 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;
  2. 加载该类的ClassLoader已经被GC;
  3. 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法;
  4. 该类的内部类没有被使用

1.3一个类被初始化的过程的注意点

  • 卸载一个类的条件是很苛刻的,很少去卸载一个类,而且在最终卸载的时候还会有一步操作即使检查这个类有没有实现Finalize接口,如果此时有一个强引用给到这个对象, 那这个对象还是不会被回收
  • 类的初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码;
  • 进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,才真正初始化类变量和其他资源;

1.4继承时父子类的初始化顺序是怎样的?

父类–静态变量
父类–静态初始化块
子类–静态变量
子类–静态初始化块
父类–变量
父类–初始化块
父类–构造器
子类–变量
子类–初始化块
子类–构造器

1.5究竟什么是类加载器?

在类“加载”阶段,通过一个类的全限定名来获取描述该类的二进制字节流的这个动作的“代码”被称为“类加载器”(Class Loader),这个动作是可以自定义实现的;

  • 所以类加载器也是一段程序,一段代码,可能是java实现的,也可能是c/c++实现的,核心作用就是将class文件,读取到JRE环境的jvm内存中

1.6JVM有哪些类加载器?

站在Java虚拟机的角度来看,只存在两种不同的类加载器:
1、启动类加载器(Bootstrap ClassLoader),使用C++语言实现,是虚拟机自身的一部分;
2、其他所有的类加载器,由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader;
站在Java开发者的角度来看,自JDK 1.2开始,Java一直保持着三层类加载器架构;

1.7JVM中不同的类加载器加载哪些文件?

1、启动类加载器(Bootstrap ClassLoader):(根的类加载器)C++语言实现的
<JAVA_HOME>\\jre\\lib\\rt.jar,resources.jar、charsets.jar
被-Xbootclasspath参数所指定的路径中存放的类库;

2、扩展类加载器(Extension ClassLoader):
sun.misc.Launcher$ExtClassLoader,
<JAVA_HOME>\\jre\\lib\\ext,
被java.ext.dirs系统变量所指定的路径中所有的类库;

3、应用程序类加载器(Application ClassLoader):系统的类加载器
sun.misc.Launcher$AppClassLoader
加载用户类路径(ClassPath)上所有的类库;


通过这行代码就知道这个类是谁加载的,但是根的类加载器是c++实现的,我们打印不出来,所以显示null

1.8JVM三层类加载器之间的关系是继承吗?

我们先看几个图



显然,他们不是继承关系, 但是都继承了ClassLoader, 也就是说,我们自己的类加载器也是需要继承classLoader的

1.9你了解JVM类加载的双亲委派模型吗?


双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当上一层类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到这个类)时,爆出异常由下一层类加载器捕获,然后才会尝试自己去加载;

1.10JDK为什么要设计双亲委派模型,有什么好处?

1、确保安全,避免Java核心类库被修改;
2、避免重复加载;
3、保证类的唯一性;
如果你写一个jaa.lang.String的类去运行,发现会抛出如下异常;

1.11可以打破JVM双亲委派模型吗?如何打破JVM双亲委派模型?

可以!
想要打破这种模型,那么就自定义一个类加载器继承原有类加载器,重写其中的loadClass方法,使其不进行双亲委派即可;

1.12如何自定义自己的类加载器?

1、继承ClassLoader
2、覆盖findClass(String name)方法 或者 loadClass() 方法;
findClass(String name)方法 不会打破双亲委派;
loadClass() 方法 可以打破双亲委派;



1.13ClassLoader中的loadClass()、findClass()、defineClass()区别?

  • loadClass() 就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中;
  • findClass() 根据名称或位置加载.class字节码;
  • definclass() 把字节码转化为java.lang.Class;

1、当我们想要自定义一个类加载器的时候,并且想破坏双亲委派模型时,我们会重写loadClass()方法;

2、如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢?可以可以重写findClass方法(),findClass()方法是JDK1.2之后的ClassLoader新添加的一个方法,这个方法只抛出了一个异常,没有默认实现;
JDK1.2之后已不再提倡用户直接覆盖loadClass()方法,而是建议把自己的类加载逻辑实现到findClass()方法中;
所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass()中实现你自己的加载逻辑即可;

加载一个类采用Class.forName()和ClassLoader有什么区别?

  • class.forName去加载会进行初始化
  • 通过classLoader去加载不会进行初始化操作,然后通过返回的class进行.newInstance的时候才会进行初始化

1.14你了解Tomcat 的类加载机制吗?


可以看到,在原来的Java的类加载机制基础上,Tomcat新增了3个基础类加载器和每个Web应用的类加载器+JSP类加载器;

3个基础类加载器在 conf/catalina.properties 中进行配置:
common.loader=" c a t a l i n a . b a s e / l i b " , " catalina.base/lib"," catalina.base/lib","catalina.base/lib/.jar"," c a t a l i n a . h o m e / l i b " , " catalina.home/lib"," catalina.home/lib","catalina.home/lib/.jar"
server.loader=
shared.loader=

Tomcat自定义了WebAppClassLoader类加载器,打破了双亲委派的机制,即如果收到类加载的请求,首先会尝试自己去加载,如果找不到再交给父加载器去加载,目的就是为了优先加载Web应用自己定义的类, 我们知道ClassLoader默认的loadClass方法是以双亲委派的模型进行加载类的,那么Tomcat打破了这个规则,重写了loadClass方法,我们可以看到WebAppClassLoader类中重写了loadClass方法;

1.15为什么Tomcat要破坏双亲委派模型?

Tomcat是web容器,那么一个web容器可能需要部署多个应用程序;
1、部署在同一个Tomcat上的两个Web应用所使用的Java类库要相互隔离;
2、部署在同一个Tomcat上的两个Web应用所使用的Java类库要互相共享;
3、保证Tomcat服务器自身的安全不受部署的Web应用程序影响;
4、需要支持JSP页面的热部署和热加载;

  • 比如使用的spring的版本不一样,如果不隔离,可能会导致后面部署的项目不能正常跑起来, 如果相同版本相同对象不共享,会导致oom

1.16有没有听说过热加载和热部署,如何自己实现一个热加载?

热加载 是指可以在不重启服务的情况下让更改的代码生效,热加载可以显著的提升开发以及调试的效率,它是基于Java的类加载器实现的,但是由于热加载的不安全性,一般不会用于正式的生产环境

热部署 是指可以在不重启服务的情况下重新部署整个项目,比如Tomcat热部署就是在程序运行时,如果我们修改了War包中的内容,那么Tomcat就会删除之前的War包解压的文件夹,重新解压新的War包生成新的文件夹;
1、热加载是在运行时重新加载class,后台会启动一个线程不断检测你的class是否发生改变;
2、热部署是在运行时重新部署整个项目,耗时相对较高;
如何实现热加载呢?
在程序代码更改且重新编译后,让运行的进程可以实时获取到新编译后的class文件,然后重新进行加载;
1、实现自己的类加载器;
2、从自己的类加载器中加载要热加载的类;
3、不断轮训要热加载的类class文件是否有更新,如果有更新,重新加载;

第二部分(JVM内存相关)

  • 此部分我将讲述JVM内存相关知识和垃圾回收相关知识

2.1Java代码到底是如何运行起来的?

其实运行起来一个Java程序,都是通过C:*\\dev\\Java\\jdk1.8.0_251\\bin\\java 启动一个JVM虚拟机 (jvm进程,也就是一个jvm虚拟机),在虚拟机里面运行Mall.class字节码文件;

2.2来,画一下JVM整个运行时内存原理图?

2.3请介绍一下JVM的内存结构划分?

加载进来的.class字节码文件、代码执行创建的对象、代码执行调用方法,方法中有变量等数据需要一个地方存放,所以JVM划分出了几个区域,用于存放这些信息

在JDK1.8之前,元空间就是原来的方法区(永久代);

2.4JVM哪些区域是线程私有的,哪些区域是线程共享的?

1、堆、元空间(方法区)是线程共享的;
2、其他区域是线程私有的;

2.5从JVM角度剖析如下程序代码如何执行?



2.6JVM运行时数据区 程序计数器 的特点及作用?

1、程序计数器是一块较小的内存空间,几乎可以忽略;
2、是当前线程所执行的字节码的行号指示器;
3、Java多线程执行时,每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响;
4、该区域是“线程私有”的内存,每个线程独立存储;
5、该区域不存在OutOfMemoryError;
6、无GC回收;

2.7JVM运行时数据区 虚拟机栈的特点及作用?

1、线程私有;
2、方法执行会创建栈帧,存储局部变量表等信息;
3、方法执行入虚拟机栈,方法执行完出虚拟机栈;(先进后出)
4、栈深度大于虚拟机所允许StackOverflowError;
5、栈需扩展而无法申请空间OutOfMemoryError(比较少见);hotspot虚拟机没有;
6、栈里面运行方法,存放方法的局部变量名,变量名所指向的值(常量值、对象值等)都存放到堆上的;
7、栈一般都不设置大小,栈所占的空间其实很小,可以通过-Xss1M进行设置,如果不设置默认为1M;
8、随线程而生,随线程而灭;
9、该区域不会有GC回收;
如图紫色区为一个虚拟机栈里的栈帧:

  • 操作数栈就是做一些简单的基本运算,将基本运算进行一次压栈进行运算
  • 动态链接一般就是指向的元空间的内存地址,方便找到下一次调用的方法

2.8JVM运行时数据区 本地方法栈的特点及作用?

1、与虚拟机栈基本类似;
2、区别在于本地方法栈为Native方法服务;
3、HotSpot虚拟机将虚拟机栈和本地方法栈合并;
4、有StackOverflowError和OutOfMemoryError(比较少见);这个oom就是创建了太多的线程,没有空间去创建虚拟机栈
5、随线程而生,随线程而灭;
6、GC不会回收该区域;
程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭;

2.9JVM运行时数据区 Java堆的特点及作用?

1、线程共享的一块区域;
2、虚拟机启动时创建;
3、虚拟机所管理的内存中最大的一块区域;
4、存放所有实例对象或数组;
5、GC垃圾收集器的主要管理区域;
6、可分为新生代、老年代;

新生代和老年代比例是1:2,新生代的三个区域是8:1:1
7、新生代更细化可分为Eden、From Survivor、To Survivor,Eden:Survivor = 8:1:1
8、可通过-Xmx、-Xms调节堆大小;
9、无法再扩展java.lang.OutOfMemoryError: Java heap space
10、如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率;

  • 多个线程往堆中放对象的时候就会产生竞争,所以会有个分配缓冲区来降低竞争,提高对象分配时的效率

2.10JVM中对象如何在堆内存分配?

1、指针碰撞(Bump The Pointer):内存规整的情况下;

2、空闲列表(Free List):内存不规整的情况下;
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定;
因此,当使用Serial、ParNew等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;
而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存;

3、本地线程分配缓冲(Thread Local Allocation Buffer,TLAB):对象创建在虚拟机中频繁发生,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况;
那么解决方案有两种:
(1)同步锁定,JVM是采用CAS配上失败重试的方式保证更新操作的原子性;
(2)线程隔离,把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定,虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定;
-XX:TLABSize=512k 设置大小;

2.11JVM堆内存中的对象布局?

在 HotSpot 虚拟机中,一个对象的存储结构分为3块区域:对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding);

对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit,官方称为 ‘Mark Word’;

第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例,另外,如果是Java数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以;

实例数据(Instance Data):程序代码中所定义的各种成员变量类型的字段内容(包含父类继承下来的和子类中定义的);

对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍,HotSpot虚拟机,任何对象的大小都是8字节的整数倍;

2.12JVM什么情况下会发生堆内存溢出?

Java堆中用于储存对象,只要不断地创建对象,并且保持GC Roots到对象之间有可达路径
来避免垃圾回收机制清理这些对象,那么随着对象数量的增加,总容量达到最大堆的容量限制后就会产生内存溢出;

  • 比如运行下面代码:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=d:/dev/heapdump.hprof
  • 上面俩个参数表示发生oom的时候在指定路径下生成dump文件
  • 当堆溢出的时候,将内存快照文件导出到某个路径, 然后依据MAT就可以分析这个dump文件

  • 可以看到哪些数据类型占用内存多少,我们可以对某些业务做出优化

2.13JVM如何判断对象可以被回收?

在JVM堆里面存放着所有的Java对象,垃圾收集器在对堆进行回收前,首先要确定这些对象之中哪些还“存活”着,哪些已经“死去”;
Java通过 **可达性分析(Reachability Analysis)**算法 来判定对象是否存活的;
该算法的基本思路:通过一系列称为“GC Roots”的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连(也称为不可达),则证明此对象是不可能再被使用的对象,就可以被垃圾回收器回收;
对象object 5、object 6、object 7虽然有关联,但它们到GC Roots是不可达的,所以它们将会被判定为可回收的对象;

哪些对象可以作为GC Roots呢?
1、在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等所引用的对象;
2、方法区/元空间中的类静态属性引用的对象;
3、方法区/元空间中的常量引用的对象;
4、在本地方法栈中JNI(即通常所说的Native方法)引用的对象;
5、Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如
NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器;
6、所有被同步锁(synchronized关键字)持有的对象;
7、反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等;
8、其他可能临时性加入的对象;

2.14谈谈Java中不同的引用类型?

Java里有不同的引用类型,分别是强引用、软引用、弱引用 和 虚引用;
强引用:Object object = new Object();
软引用:SoftReference 内存充足时不回收,内存不足时则回收;MyBatis用的软引用做的缓存
弱引用:WeakReference 不管内存是否充足,只要GC一运行就会回收该引用对象;弱引用 用的很少,比如threadLocal的数据就是用的弱引用,或者我们缓存对象我们感官他不常用就可以使用这个弱引用
虚引用:PhantomReference这个其实暂时忽略也行,因为很少用,它形同虚设,就像没有引用一样,其作用就是该引用对象被GC回收时候触发一个系统通知,或者触发进一步的处理;

2.15JVM堆内存分代模型?

JVM堆内存的分代模型:年轻代、老年代;
大部分对象朝生夕死,少数对象长期存活;
新生代占1/3,老年代占2/3, 在新生代里Eden和s1和s2的比例是8:1:1

2.16请介绍一下JVM堆中新生代的垃圾回收过程?

JVM里垃圾回收针对的是 新生代,老年代,还有元空间/方法区(永久代)
不会针对方法的栈帧进行回收,方法一旦执行完毕,栈帧出栈,里面的局部变量直接就从内存里清理掉,也就是虚拟机栈不存在垃圾回收

代码里创建出来的对象,一般就是两种:
1、一种是短期存活的,分配在Java堆内存之后,迅速使用完就会被垃圾回收;
2、一种是长期存活的,需要一直生存在Java堆内存里,让程序后续不停的去使用;
第一种短期存活的对象,是在Java堆内存的新生代里分配;
第二种长期存活的对象,通过在新生代S0区和S1区来回被垃圾回收15次后,进入Java堆内存的老年代中,这里的15次,我们也称为对象的年龄,即对象的年龄为15岁;
java -XX:+PrintFlagsFinal 打印jvm默认参数值, 分代年龄默认值是15;

2.17JVM对象动态年龄判断是怎么回事?

虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold=15才能晋升老年代;
结论–>动态年龄判断:Survivor区的对象年龄从小到大进行累加,当累加到X年龄(某个年龄)时占用空间的总和大于50%(可以使用-XX:TargetSurvivorRatio=? 来设置保留多少空闲空间,默认值是50),那么比X年龄大的对象都会晋升到老年代;
1、Survivor区分布如下图:

1-3岁总和小于50%
2、此时新生代GC后,有6%的对象进入Survivor区,Survivor区分布如下图:

这时从1岁加到4岁时,总和51% 大于50%,但此时没有大于四岁的对象,即没有对象晋升

3、又经过一次新生代GC后,有40%的对象进入Survivor区,Survivor区分布如下图:

Survivor区的对象年龄从小到大进行累加,当累加到 3 年龄时的总和大于50%,那么比3大的都会晋升到老年代,即4岁的20%、5岁的20%晋升到老年代;

2.18什么是老年代空间分配担保机制

  • 就是将数据往老年代放的时候,空间够不够怎么保证。
    新生代Minor GC后剩余存活对象太多,无法放入Survivor区中,此时就必须将这些存活对象直接转移到老年代去,如果此时老年代空间也不够怎么办?
    1、执行任何一次Minor GC之前,JVM会先检查一下老年代可用内存空间,是否大于新生代所有对象的总大小,因为在极端情况下,可能新生代Minor GC之后,新生代所有对象都需要存活,那就会造成新生代所有对象全部要进入老年代;
    2、如果老年代的可用内存大于新生代所有对象总大小,此时就可以放心大胆的对新生代发起一次Minor GC,因为Minor GC之后即使所有对象都存活,Survivor区放不下了,也可以转移到老年代去;
    3、如果执行Minor GC之前,检测发现老年代的可用空间已经小于新生代的全部对象总大小,那么就会进行下一个判断,判断老年代的可用空间大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小,如果判断发现老年代的内存大小,大于之前每一次Minor GC后进入老年代的对象的平均大小,那么就是说可以冒险尝试一下Minor GC,但是此时真的可能有风险,那就是Minor GC过后,剩余的存活对象的大小,大于Survivor空间的大小,也大于老年代可用空间的大小,老年代都放不下这些存活对象了,此时就会触发一次“Full GC”;
    所以老年代空间分配担保机制的目的?也是为了避免频繁进行Full GC
    4、如果Full GC之后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致“OOM”内存溢出 ;

2.19什么情况下对象会进入老年代?

1、躲过15次GC之后进入老年代,可通过JVM参数“-XX:MaxTenuringThreshold”来设置年龄,默认为15岁;
2、动态对象年龄判断;
3、老年代空间担保机制;
4、大对象直接进入老年代;

大对象是指需要大量连续内存空间的Java对象,比如很长的字符串或者是很大的数组或者List集合,大对象在分配空间时,容易导致内存明明还有不少空间时就提前触发垃圾回收以获得足够的连续空间来存放它们,而当复制对象时,大对象又会引起高额的内存复制开销,为了避免新生代里出现那些大对象,然后屡次躲过GC而进行来回复制,此时JVM就直接把该大对象放入老年代,而不会经过新生代;

我们可以通过JVM参数“-XX:PretenureSizeThreshold”设置多大的对象直接进入老年代,该值为字节数,比如“1048576”字节就是1MB,该参数表示如果创建一个大于这个大小的对象,比如一个超大的数组或者List集合,此时就直接把该大对象放入老年代,而不会经过新生代;
-XX:PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效,其他新生代垃圾收集器不支持该参数,如果必须使用此参数进行调优,可考虑 ParNew+CMS的收集器组合;

2.20JVM运行时数据区 元空间的特点及作用?

1、在JDK1.8开始才出现元空间的概念,之前叫方法区/永久代;
2、元空间与Java堆类似,是线程共享的内存区域;
3、存储被加载的类信息、常量、静态变量、常量池、即时编译后的代码等数据;
4、元空间采用的是本地内存,本地内存有多少剩余空间,它就能扩展到多大空间,也可以设置元空间大小;

-XX:MetaspaceSize=20M -XX:MaxMetaspaceSize=20m

5、元空间很少有GC垃圾收集,一般该区域回收条件苛刻,能回收的信息比较少,所以GC很少来回收;
6、元空间内存不足时,将抛出OutOfMemoryError;

2.21JVM本机直接内存的特点及作用?

1、直接内存(Direct Memory)不属于JVM运行时数据区,是本机直接物理内存;
2、像在JDK 1.4中新加入了NIO(New Input/Output)类,一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的 DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据;
3、可能导致OutOfMemoryError异常出现; 使用Netty如果出现这个异常,就有可能是因为使用本地直接内存不够了

2.22JVM本机直接内存溢出问题?

直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,该参数表示设置新I / O(java.nio程序包)直接缓冲区分配的最大总大小(以字节为单位);默认情况下,大小设置为0,这意味着JVM自动为NIO直接缓冲区分配选择大小;

由直接内存导致的内存溢出,无法生成Heap Dump文件,如果程序中直接或间接使用了NIO技术,那就可以重点考虑检查一下直接内存方面的原因;

2.23说几个与JVM内存相关的核心参数?

-Xms Java堆内存的大小;
-Xmx Java堆内存的最大大小;
-Xmn Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小;
-XX:MetaspaceSize 元空间大小;
-XX:MaxMetaspaceSize 元空间最大大小;
-Xss 每个线程的栈内存大小;
-XX:SurvivorRatio=8 设置eden区 和survivor 区大小的比例,默认是8:1:1;
-XX:MaxTenuringThreshold=5 年龄阈值;
-XX:+UseConcMarkSweepGC 指定CMS垃圾收集器;
-XX:+UseG1GC 指定使用G1垃圾回收器

–查看默认的堆大小及默认的垃圾收集器

java -XX:+PrintCommandLineFlags -version

第三部分(垃圾回收)

  • 这也是Java非常突出的一个优势就是开发人员不需要管理内存,一般情况下不会有内存泄漏的发生,就是因为Java有原生的垃圾回收器, 有不需要的对象,GC会帮助我们进行回收

3.1堆为什么要分成年轻代和老年代?


因为年轻代和老年代不同的特点,需要采用不同的垃圾回收算法;
年轻代的对象,它的特点是创建之后很快就会被回收,所以需要用一种垃圾回收算法;
老年代的对象,它的特点是需要长期存活,所以需要另外一种垃圾回收算法 ;
所以需要分成两个区域来放不同的对象;

1、绝大多数对象都是朝生夕灭的;
如果一个区域中大多数对象都是朝生夕灭,那么把它们集中放在一起,每次回收时只关注如何保留少量存活对象,而不是去标记那些大量将要被回收的对象,就能以较低的代价回收到大量的空间;

2、熬过越多次垃圾收集的对象就越难以回收;
如果是需要长期存活的对象,那把它们集中放在一块,虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用;

3、JVM划分出新生代、老年代之后,垃圾收集器可以每次只回收其中某一个或者某些部分的区域 ,同时也有了“Minor GC”“Major GC”“Full GC”这样的回收类型的划分;
Minor GC/Young GC :新生代收集
Major GC/Old GC:老年代收集
Full GC:整堆收集,收集整个Java堆和元空间/方法区的垃圾收集;
Mixed GC:混合收集,收集整个新生代以及部分老年代的垃圾收集,目前只有G1收集器会有这种行为;

4、针对不同的区域对象存亡特征采用不同的垃圾收集算法:
(1)复制算法
(2)标记-清除算法
(3)标记-整理算法

3.2JVM堆的年轻代为什么要有两个Survivor区?

1、如果没有Survivor区会怎么样?
此时每触发一次Minor GC,就会把Eden区的对象复制到老年代,这样当老年代满了之后会触发Major Gc/Full GC(通常伴随着MinorGC),比较耗时,所以必须有Survivor区;

2、如果只有1个Survivor区会怎么样?
刚刚创建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中存活的对象就会被移动到Survivor区,下一次Eden满了的时候,此时进行Minor GC,Eden和Survivor各有一些存活对象,因为只有一个Survivor,所以Eden区第二次GC发现的存活对象也是放入唯一的一个Survivor区域中,但此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化问题,并且由于不连续的空间会导致再分配大对象的时候,由于没有连续的空间来分配,会导致提前垃圾回收;
如果将Survivor中的所有存活对象进行整理消除碎片,然后将所有的存活对象放入其中,这样做会降低效率;
如果把两个区域中的所有存活对象都复制转移到一个完全独立的空间中,也就是第二块Survivor中,这样就可以留出一块完全空着的Eden和Survivor了,下次GC的时候再重复这个流程,所以我们便要有两个Survivor区;

  1. 如果不分的话,每次GC的区域太大,导致STW时间过长
  2. 再就是为了避免触发fullGC或者oldGC,让其来回复制达到一定年龄再进去老年代,以此来让一个对象在年轻代的停留时间加长,避免尽快进入到年代而导致老年代空间不够从而触发fullGC或者oldGC
  3. 再就是可以合理使用复制算法,来将另外一个区域的所有对象都删除,从而没有使用整理算法也能避免内存碎片。

3.3Eden区与Survivor区的空间大小比值为什么默认是8:1:1?

一个eden区 ,新生代对象出生的地方;
两个survivor区,一个用来保存上次新生代GC存活下来的对象,还有一个空着,在新生代GC时把eden+survivor中存活对象复制到这个空的survivor中;
统计和经验表明,90%的对象朝生夕死存活时间极短,每次gc会有90%对象被回收,剩下的10%要预留一个survivor空间去保存;

3.4请介绍下JVM中的垃圾回收算法?

3.4.3标记-清除算法

标记-清除算法是最基础的收集算法,后续的很多垃圾回收算法是基于该算法而发展出来的,它分为‘ 标记 ’和‘ 清除 ’两个阶段;
1、标记
标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记出所有存活的对象,在标记完成后,统一回收所有未被标记的对象,标记过程就是对象是否属于垃圾的判定过程,基于可达性分析算法判断对象是否可以回收;
2、清除
标记后,对所有被标记的对象进行回收;

优点:基于最基础的可达性分析算法,实现简单,后续的收集算法都是基于这种思想实现的;
缺点
1、执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;
2、内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集;

3.4.4复制算法

复制算法是标记-复制算法的简称,将可用内存按容量分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块内存上,然后再把已使用过的内存空间一次清理掉;

优点:实现简单,效率高,解决了标记-清除算法导致的内存碎片问题;
缺点
1、代价太大,将可分配内存缩小了一半,空间浪费太多了;
2、对象存活率较高时就要进行较多的复制操作,效率将会降低;
一般虚拟机都会采用该算法来回收新生代,但是JVM对复制算法进行了改进,JVM并没有按照1:1的比例来划分新生代的内存空间,因为通过大量的统计和研究表明,90%以上的对象都是朝生夕死的,所以JVM把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor,发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间,HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有另外一个Survivor空间即10%的新生代会被“浪费”;
当然,90%的对

以上是关于Java~阅读《深入理解Java虚拟机》知识点总结的主要内容,如果未能解决你的问题,请参考以下文章

深入理解java虚拟机GC垃圾回收-虚拟机及垃圾收集器日志参数总结

深入理解Java虚拟机Java内存区域模型对象创建过程常见OOM

深入理解Java虚拟机(类文件结构+类加载机制+字节码执行引擎)

深入理解多线程—— Java虚拟机的锁优化技术

深入理解JAVA虚拟机之JVM性能篇---基础知识点

《深入理解Java虚拟机 - Jvm高级特性与最佳实践(第三版)》阅读笔记