1.1
为什么要学习JVM?
第一: 深入理解JVM可以帮助提高解决问题的能力,
第二: 有效防止内存泄漏(Memory leak),
第三: 优化线程锁的使用(Thread Lock:
). 锁分为:分布式锁 > 进程锁 > 线程锁 第四: 提高系统吞吐量(throughput). 系统吞吐量::系统的抗压能⼒,可以理解为系统每秒钟能处理的⽤户 请求数量 第五: 降低延迟(Delay).提高其性能(performance).
1.2 你了解哪些JVM产品? *****应用最官方的,最主要的Hotspot VM(sun公司研发,后再2010年由Oracle公司收购) ***JRockit VM(BEA公司研发,后在2008年由Oracle公司收购) ***J9 VM(IBM公司内部使用), jikesRVM(IBM) ***TaobaoJVM(AliJVM团队开发,基于OpenJDK开发了AlibabaJDK) ***ZuLu,Zing(Azul Systems) ***Dalvik
1.3 JVM的构成有哪几部分? A.类加载系统(ClassLoader System):负责加载类到内存 B.运行时数据区(Runtime Data Area):负责存储数据信息(对象,方法等) C.执行引擎(Execution Engine):负责解释执行字节码,执行GC操作等. D.本地库接口(Native Interface):负责融合不同的编程语言为JAVA所用.
2.1 你知道哪些类加载器? --启动类加载:BootstrapClassLoader(也叫引导类加载器,是ExtClassLoader的⽗类加载器,默认负责加载%JAVA_HOME%lib下jar包和class⽂件。这个类加载使用C/C++语言实现的,嵌套在JVM内部) --扩展类加载器:ExtClassLoader(Java语言编写,ExtClassLoader是AppClassLoader的⽗类加载器,负责加载%JAVA_HOME%/lib/ext ⽂件夹下 的jar包和class类。由sun.misc.Launcher$ExtClassLoader实现 ) --APPClassLoader(AppClassLoader是⾃定义加载器的⽗类,负责加载classpath下的类⽂件。系统默认类加载器,平常开发中所写的java⽂件以及引⼊的jar包都由此加载器加载,不仅仅是系统类加载器,还是线程上下⽂加载器)。 继承ClassLoader实现⾃定义类加载器。) --UserClassLoader(自定义类加载器继承ClassLoader)
2.2 什么是双亲委派类加载模型? 应该是类的一种默认任务委派加载模式 /.如果一个类收到了类加载请求,他并不会自己先去加载,而是把这个请求委托给父类的加载器去执行; /.如果父类加载器还存在父类加载器,则进一步向上委托,一次递归,请求最终将到达顶层的启动类加载器; /.如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载才会尝试自己去加载,这就是双亲委派模式. ***当父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常。
2.3 双亲委派方式加载类有什么优势,劣势? /.优点:避免一个类被重复的多次加载,实现了类加载器时的优先级层次关系,保护程序安全,防止核心API被随意篡改.这样更有利于java程序的稳定运行. /.缺点:父类的类加载器无法访问底层的类加载器所加载的类. 1.Java继承概念中,父类是不确定子类的,但子类可以确定其父类–多态特性的来源之一 2.父类是不可调用子类的方法的,但子类可以调用父类所有非private的方法-继承特性的特征之一 3.存在明显的代码漏洞,例如:因为Java继承的关系,所以类进行加载时,是先加载的父类,才去加载子类,如果恰巧这是父类的某个方法调用了子类的方法,而子类方法的某些常量因为子类还未加载没有实例化,就会直接导致程序崩溃
2.4 描述一些类加载时候的基本步骤是怎样的? 1.通过一个类的完全限定名(类全名)来获取其定义的二进制字节流. 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构. 3.在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区中这些数据的访问入口
2.5 什么情况下会触发类的加载? 隐式加载: 1.调用类的静态成员(比如类变量,静态方法) 2.创建类的实例对象(比如使用new关键字构建对象或反射构建对象) 3.构建子类实例对象(构建类的对象时首先会加载父类类型) 显式加载: 1.ClassLoader.loadClass(...) 2.Class.forName(...) 3.java虚拟机启动时被标明为启动类的类(包含Main方法)
2.6 类加载时静态代码块一定会执行吗? 不一定,类加载时是否会执行静态代码块取决于加载的方式. 默认是懒加载
2.7 如何理解类的主动加载和被动加载? 1.主动使用:会执行加载,连接,初始化静态域 2.被动使用:只执行加载,连接,不初始化静态域
2.8 为什么要自己定义类加载器,如何定义? 因为: /1.隔离加载类 在某个应用中需要使用中间件,这个中间件有自己的依赖的jar包,在同一个工程里面,如果引用多个框架的话,有可能会出现某些类的路径一样、类名也相同, /2.修改类的加载方式 在整个类的加载过程中,bootstrap引导类加载器是一定被使用的 /3.扩展加载源 加载的类可以在网络、本地物理磁盘、jar包去加载, /4.防止源码泄露 当有了字节码文件或者没有反编译的手段,java代码是很容易被编译和篡改 定义的方式: /1.继承UClassLoader,重写findClass,在findClass里获取类的字节码并调用ClassLoader中的defineClass方法来加载类,获取类对象. 如果要打破双亲委派机制,需要重写loadClass方法.
3.1 为何要学习字节码? 对于开发人员,了解字节码可以更准确,直观地理解java语言中更深层次的东西,通过字节码可以很直观的看到Volatile关键字如何在字节码上生效.另外,字节码增强技术再SpringAop,各种ORM框架,热部署中的应用屡见不鲜,深入理解其原理对于我们来说大有裨益。除此之外,由于JVM规范的存在,只要最终可以生成符合规范的字节码就可以在JVM上运行,因此这就给了各种运行在JVM上的语言(如Scala、Groovy、Kotlin)一种契机,可以扩展Java所没有的特性或者实现各种语法糖。理解字节码后再学习这些语言,可以“逆流而上”,从字节码视角看它的设计思路,学习起来也“易如反掌”。 ***主要是更能通过字节码的看到程序的运行过程,能更好的写出一些维护性好,可用性好的优质代码
3.2 如何解读字节码内容? 1.首先编写一个测试类,然后在测试类的类加载目录下输入javap -verbose 类名.class. 2.也可以安装一个idea插件 插件的名字为jclasslib。代码在编译后,我们可以在菜单栏”View”中选择”Show Bytecode With jclasslib”,可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息。
3.3 字节码内容由哪几部分构成? 整体结构: 一个class类文件的结构组成如下 / 1.magic(魔数)4个字节: 所有.class文件的前四个字节都是魔数(Magic Number),是class文件的标识。 魔数的固定值为:0xCAFEBABE。 魔数放在文件开头,JVM可以根据文件魔数判断.class文件的合法性。 /2.minor_version(次版本号)也叫小版本号 2+2字节 /3. major_version(主版本号)也叫大版本号 版本号为魔数之后的4个字节。例如,版本号“00 00 00 34”。前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)。例如,版本号“00 00 00 34”的次版本号转化为十进制为0,主版本号转化为十进制为52,在Oracle官网中查询序号52对应的主版本号为1.8,所以编译该文件的Java版本号为1.8.0。 /4.constant_pool_count(常量池计数器) 常量池计数器:版本号后面是常量池数量,用两个字节表示.常量池容量计数值从1开始. 索引值0用于表达不引用任何常量池。 2+N个字节 /5.constant_pool[constant_pool_count-1](常量池) 常量池(Constant Pool)整体上分为两部分,常量池计数器以及常量池数据区。 常量池中存储两类常量:字面量(Literal)与符号引用(Symbolic References)。字面量为代码中声明为Final的常量值,符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。 /6.access_flags(类的访问标志)2个字节 常量池结束之后的两个字节,描述该Class是类还是接口,以及是否被Public、Abstract、Final等修饰符修饰。JVM规范规定了多个访问标志(Access_Flag)。需要注意的是,JVM并没有穷举所有的访问标志,而是使用按位或操作来进行描述的,比如某个类的修饰符为Public Final,则对应的访问修饰符的值为ACC_PUBLIC | ACC_FINAL,即0x0001 | 0x0010=0x0011。 /7.this_class(当前类名索引值)2个字节 访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。 /8.super_class(父类名索引值) 父类索引:当前类名后的两个字节,描述父类的全限定名,同上,保存的也是常量池中的索引值。 /9.interfaces_count(接口计数)2+N个字节 父类名称后的两字节,描述了该类或父类实现了哪些接口,接口的数量以及所有接口名称的字符串常量的索引值。 /10.interfaces[interfaces_count](接口数组) 当前类所实现的全部接⼝ /11.fields_count(成员变量计数)2+N个字节 当前类所拥有的所有字段 /12.fields[fields_count](成员变量数组) /13.methods_count(方法计数)2+N个字节 当前类所拥有的所有⽅法(每个⽅法都会有⼀个属性叫Code,⾥⾯是该⽅法的指令集。⼀般来说每个Code还会带有2个属性,LineNumberTable和LocalVaribleTable来分别记录字节码对应源码的⾏号和⽅法的变量表) /14.methods[methods_count](方法数组) /15.attributes_count(属性计数)2+N个字节 所有属性(最常见的就是Source File) /16.attributes[attributes_count](属性数组)
3.4 什么是字节码增强? 字节码增强技术相当于是一把打开运行时JVM的钥匙,利用它可以对现有字节码进行修改或者动态生成新的字节码,进而对运行中的程序做修改,实现热部署。也可以跟踪JVM运行中程序的状态,进行性能诊断等。 此外,我们平时使用的动态代理、AOP也与字节码增强密切相关,它们实质上还是利用各种手段生成符合规范的字节码文件。 AOP(Aspect Oriented Programming):面向切面编程 通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
3.5 为什么要进行字节码增强 Java字节码增强主要是为了减少冗余代码,提⾼性能等。掌握字节码增强后可以高效地定位并快速修复一些棘手的问题(如线上性能问题、方法出现不可控的出入参需要紧急加日志等问题),也可以在开发中减少冗余代码,大大提高开发效率。
3.6 你了解哪些字节码增强技术? ASM技术: 对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为。ASM的应用场景有AOP(Cglib就是基于ASM)、热部署、修改其他jar包中的类等。当然,涉及到如此底层的步骤,实现起来也比较麻烦。 关键API: /1.ClassReader:用于读取已经编译好的.class文件。 /2.ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。 /3.ClassVisitor:ASM中对于字节码文件中不同的区域有不同的Visitor,比如用于访问方法的MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的AnnotationVisitor等。为了实现AOP,重点要使用的是MethodVisitor。 Javassist技术: Javassist是一个用于分析、编辑和创建Java字节码的类库,相比ASM在指令层次上操作字节码会更加简单直观。可以无须关注字节码刻板的结构,直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。 Javassist中最重要的是ClassPool、CtClass、CtMethod、CtField这四个类: /.1CtClass(compile-time class):编译时类信息,它是一个class文件在代码中的抽象表现形式,可以通过一个类的全限定名来获取一个CtClass对象,用来表示这个类文件。基于CtClass可以实现对类的操作,如在类中动态添加新字段、方法和构造函数以及改变类、父类和接口的方法。 /.2ClassPool:ClassPool对象是一个CtClass对象的容器。一个CtClass对象被构建后,它被记录在ClassPool中。从开发视角来看,ClassPool是一张保存CtClass信息的HashTable,key为类名,value为类名对应的CtClass对象。当我们需要对某个类进行修改时,就是通过pool.getCtClass(“className”)方法从pool中获取到相应的CtClass。 /.3CtField:对应类的属性,通过它可以给类创建新的属性,还可以修改已有属性的类型,访问修饰符等。 /.4CtMethod:对应类中的方法,通过它可以给类创建新的方法,还可以修改返回类型,访问修饰符等,甚至还可以修改方法体内容。
3.7 什么是热替换以及如何实现? 热替换: Java Agent是Java Instrumentation API的一部分,它提供了向现有已编译的Java类添加字节码的功能,相当于字节码插桩的入口。可以侵入运行在JVM上的应用程序,进而修改应用程序中各种类的字节码。 Java Agent 这个技术出现在 JDK1.5 之后,,我们平时用的很多工具,都是基于 Java Agent 实现的,例如常见的热部署 JRebel,各种线上诊断工具(Btrace, Greys),还有阿里开源的 Arthas。总之,在分布式链路追踪中为了获取服务之间调用链信息,采集器(也叫做探针)通常需要在方法的前后做埋点。而Java Agent技术就是实现埋点的一种方式。 Java Agent的启动方式和普通 Jar 包有所不同,对于普通的Jar包,通过指定类的 main 函数进行启动,但是 Java Agent 并不能单独启动,必须依附在一个 Java 应用程序运行。 我们可以使用 Agent 技术构建一个独立于应用程序的代理程序,用来协助监测、运行甚至替换其他 JVM 上的程序,使用它可以实现虚拟机级别的 AOP 功能。 如何实现: Instrument以及Javassist对指定业务对象进行功能增强, ---第一步: 创建业务service类,将此类作为字节码增强对象 ---第二步: 创建Transformer对象,用于对CycleService对象进行功能增强, ---第三步: 创建Agent对象,用于调用DefaultClassTransformer对象执行字节码增强,在Agent中可定义两个方法进行不同时间点进行增强: 1.premain方法,此方法在main方法执行之前执行。(方法声明固定写法) 2.agentmain方法,此方法在main方法启动后,也就时候程序运行时进行执行。(方法声明固定写法) ---第四步: 添加maven插件用于对项目进行打包 依赖中,Premain-Class和Agent-Class用于指定代理类,Can-Redefine-Classes 是否需要重新定义所有类,默认为false,可选。Can-Retransform-Classes 是否需要retransform,默认为false,可选。依赖添加后,先执行maven clean对原有类文件进行清除,然后执行maven package对项目进行打包。 ---第五步:创建CycleServiceTests类,对CycleService对象进行调用,也就是启动服务,假如需要在此服务启动之前进行增强,可以考虑在idea的vm参数中添加-javaagent:E:\TCGBIV\DEVCODES\CSDNCODES\cgb2202codes\01-java\target\01-java-1.0-SNAPSHOT.jar,这里的jar位 ---第六步:添加tools依赖 程序启动之后,通过VirtualMachine 的 attach api加载 Java Agent,这组 api 其实是 JVM 进程之间的的沟通桥梁,底层通过socket 进行通信,JVM A 可以发送一些指令给JVM B,B 收到指令之后,可以执行对应的逻辑,比如在命令行中经常使用的 jstack、jps 等,很多都是基于这种机制实现的。VirtualMachine 在jdk的lib下面的tools.jar中,如果不在classpath的话,要加进去。可通过添加依赖的方式进行tools的依赖配置。 ---第七步:创建AgentInstrumentTests类 ---第八步:分别运行CycleServiceTests、AgentInstrumentTests类进行测试。
#
4.1 JVM运行内存是如何划分的? 主要分为五个: */1.方法区: 跟Java堆一样,方法区是各个线程共享的内存区域,此区域是用来存储类的信息(类的名称、字段信息、方法信息)、静态变量、常量以及编译器编译后的代码。JVM规范中并不区分方法区和堆,只把方法区描述为堆的逻辑部分,但是它却有一个别名叫做非堆(Non-Heap),目的就是与Java堆区分开。根据垃圾回收机制中分代回收的思想,如果在HotSpot虚拟机上开发,可以把方法区称为“永久代”(只是可以这么理解,但实质是不一样的),垃圾回收机制在Java堆中划分一个部分称为永久代,用此区域来实现方法区,这样HotSpot的垃圾收集器就可以像管理Java堆一样管理这部分内存,而不必为方法区开发专门的内存管理器。 */2.堆: Java堆是java虚拟机所管理的内存中最大的一块,是被所有线程都共享的内存区域。存在的唯一目的就是存放对象实例,几乎所有的对象实例都在这里进行分配内存。不过目前随着技术的不断发展,也并不是所有的对象实例都在堆中分配内存,可能也存在栈上分配。由于所占空间大,又存放各种实例对象,因此java虚拟机的垃圾回收机制主要管理的就是此区域,详细的垃圾回收方法以后会提到。JVM规范中规定堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。并且可以通过-Xmx和-Xms来扩展堆的内存大小,如果在堆中没有足够的内存为实例分配,并且堆也无法在扩展时,就会报OutOfMemoryError异常。 运行时常量池是方法区的一个部分,class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期间生成的各种字面量和符号引用,这部分内容会在类加载后进入方法区的运行时常量池中。Java 虚拟机对 Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。 */3.虚拟机栈: 我们经常会把java内存粗糙的分为两个部分,堆和栈,Java虚拟机栈就是栈这一部分,或者说是虚拟机栈中局部变量表部分。跟程序计数器一样,虚拟机栈也是线程私有的,它的生命周期跟线程相同。每个方法在执行的同时都会创建一个栈帧(Stack Frame),每个栈帧对应一个被调用的方法,栈帧中用于存储局部变量表、操作数栈、动态链表、方法出口等信息。每一个方法从开始执行到结束就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 局部变量表:顾名思义,他就是用来存储方法中的局部变量(包括在方法中生命的非静态变量以及函数形参),对于基本数据类型,直接存值,对于引用类型的变量,存储指向该对象的引用。由于它只存放基本数据类型的变量、引用类型的地址和返回值的地址,这些类型所需空间大小已知且固定,所以当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全可以确定的,在方法运行期间也不会改变局部变量表的大小。 指向运行常量池的引用:在方法执行过程中难免会使用到类中定义的常量,因此栈帧中要存放一个指向运行时常量池的引用。 方法返回地址:当一个方法执行结束后,要返回到之前调用它的地方,因此在栈帧中需要保存一个方法返回地址。 */4.本地方法栈: 本地方法栈与虚拟机栈的功能非常的相似,区别不过是虚拟机栈为虚拟机执行java方法服务,而本地方法栈为虚拟机执行Native方法服务。有的虚拟机并不会区分本地方法栈和虚拟机栈,比如Sun HotSpot虚拟机直接将两个合二为一。 */5.程序计数器: 虽然在上图中程序计数器的面积很大,但实际上它是一块较小的内存空间,可以看做当前线程所执行字节码的行号指示器。字节码解释器在工作中时下一步该干啥、到哪了,就是通过它来确定的。大家都知道在多线程的情况下,CPU在执行线程时是通过轮流切换线程实现的,也就是说一个CPU处理器(假设是单核)都只会执行一条线程中的指令,因此为了线程切换后能恢复到正确的执行位置,每个线程都要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。很明显,程序计数器就是线程私有的。如果线程正在执行的是一个java方法,程序计数器记录的是正在执行的虚拟机字节码指令地址;如果执行的Native方法,程序计数器记录的值为空(Undefined),此内存区域是java中唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
4.2 JVM中的程序计数器用于做什么? 程序计数器(Program Counter Register)也称之为PC寄存器,是一块较小的内存空间,用来存储指向下一条指令的地址,也可以看作是当前线程执行的字节码的行号指示器。 在虚拟机的概念模型里,字节码解析器的工作是通过改变这个计数器的值来选取下一条 要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
4.3 JVM虚拟机栈的结构是怎样的? Java 虚拟机栈(Java Virtual Machine Stacks)描述的是 Java 方法执行时的内存模型,解决的是程序运行时数据的操作问题,即程序如何执行,或者说如何处理数据。而堆解决的是数据存储的问题,就是数据怎么放,放哪里。 Java 虚拟机栈是线程私有的,每个方法在被线程调用时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈的生命周期和线程一致,也就是线程结束了,该虚拟机栈也销毁了。 如果线程请求的栈深度大于虚拟机所允许的栈深度就会抛出 StackOverflowError 异常。如果虚拟机是可以动态扩展的,如果扩展时无法申请到足够的内存就会抛出 OutOfMemoryError 异常。
4.4 JVM虚拟机栈中局部变量表的作用是什么? 局部变量表也称之为局部变量数组或本地变量表,用于存放方法参数和方法内部定义的局部变量信息。在Java程序被编译为Class文件时,就已经确定了每个方法所需局部变量表的大小。 局部变量表以变量槽为最小单位,每个变量槽可以存放一个32位以内的数据类型,故每个变量槽都应该能存放 boolean、byte、char、short、int、float、refrence或returnAddress类型的数据,对于long、double两种,会占用两个变量槽。
4.5 JVM虚拟机栈中操作数栈的作用是什么? 每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last - In - First -Out)的 操作数栈(Operand Stack)。 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop).
4.6 JVM堆的构成是怎样的? JAVA堆内存在JVM中可分为年轻代和老年代。年轻代又分为Eden和两个Survivor区。其中,堆内存又是垃圾收集器(GC)管理的主要区域。
4.7 Java对象分配内存的过程是怎样的? 1.编译器通过逃逸分析(JDK8已默认开启),确定对象是在栈上分配还是在堆上分配。 2.如果是在堆上分配,则首先检测是否可在TLAB(Thread Local Allocation Buffer)上直接分配。 3.如果TLAB上无法直接分配则在Eden加锁区进行分配(线程共享区)。 4.如果Eden区无法存储对象,则执行Yong GC(Minor Collection)。 5.如果Yong GC之后Eden区仍然不足以存储对象,则直接分配在老年代。 6.新生代由Eden 区和两个幸存区构成(假定为s1,s2), 任意时刻至少有一个幸存区是 空的(empty),用于存放下次GC时未被收集的对象。 7.GC触发时Eden区所有”可达对象”会被复制到一个幸存区,假设为s1,当幸存区s1无法 存储这些对象时会直接复制到老年代。 8.GC再次触发时Eden区和s1幸存区中的”可达对象”会被复制到另一个幸存区s2,同时清 空eden区和s1幸存区。 9.GC再次触发时Eden区和s2幸存区中的”可达对象”会被复制到另一个幸存区s1,同时清 空eden区和s2幸存区.依次类推。 10.当多次GC过程完成后,幸存区中的对象存活时间达到了一定阀值(可以用参数 - XX:+MaxTenuringThreshold 来指定上限,默认15),会被看成是“年老”的对象然后直接移动到老年代。
4.8 JVM年轻代幸存区设置的比较小会有什么问题? 伊甸园区被回收时,对象要拷贝到幸存区,假如幸存区比较小,拷贝的对象比较大,对象就会直接存储到老年代,这样会增加老年代GC的频率。而分代回收的思想就会被弱化。
4.9 JVM年轻代伊甸园区设置的比例比较小会有什么问题? 伊甸园设置的比较小,会增加GC的频率,可能会导致STW的时间边长,影响系统性能。 STW: Stop-The-World: 是在垃圾回收算法执⾏过程当中,将JVM内存冻结丶应用程序停顿的⼀种状态。
4.10 JVM堆内存为什么要分成年轻代和老年代? 为了更好的实现垃圾回收。
4.11 如何理解JVM方法区以及它的构成是怎样的? 如何理解: 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。多个线程同时加载统一个类时,只能有一个线程能加载该类,其他线程只能等等待该线程加载完毕,然后直接使用该类,即类只能加载一次。方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError:Metaspace JVM方法区的构成: 方法区(Methed Area)是一种规范,用于存储已被虚拟机加载的类信息、常 量、静态变量、即时编译后的代码等数据。 1)类信息包括对每个加载的类型(类class、接口interface、枚举enum、注解annotation)以及属性和方法信息。 2)常量信息可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。
4.12 什么是逃逸分析以及可以解决什么问题? 逃逸分析: 逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。 逃逸分析可以解决什么问题: 使用逃逸分析,编译器可以对代码做如下优化: 1、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。 2、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。 3、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
4.13 何为内存溢出以及导致内存溢出的原因? 内存溢出: 内存中剩余的内存不足以分配给新的内存请求就会内存溢出。内存溢出可能直接导致系统崩溃。 内存溢出的原因: 内存泄漏是导致内存溢出的一种原因,但内存溢出不全是由内存泄漏引起的,还可能是: 1)创建的对象太大导致堆内存溢出 2)创建的对象太多导致堆内存溢出 3)方法出现了无限递归调用导致栈内存溢出 4)方法区内存空间不足导致内存溢出。
4.14 何为内存泄漏以及内存泄漏的原因是什么? 内存泄漏: 动态分配的内存空间,在使用完毕后未得到释放,结果导致一直占据该内存单元,直到程序结束。这个现象称之为内存泄漏。因此良好的代码规范,可以有效地避免这些错误。 内存泄漏带来的问题: 1)长时间运行,程序会变卡,性能严重下降。 2)OutOfMemoryError错误,系统直接挂掉。 内存泄漏的原因: 1)大量使用静态变量(静态变量与程序生命周期一样) 2)IO/连接资源用完没关闭(记得执行close操作) 3)内部类的使用方式存在问题(实力内部类或默认引用外部类对象) 4)缓存(Cache)应用不当(尽量不要使用强引用) 5)ThreadLocal应用不当(用完记得执行remove操作)
4.15 JAVA中的四大引用你知道多少? 1.强引用(不会被回收 正常编码使用): 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回 收掉被引用的对象。 2.软引用(内存不够了被GC,可作为缓存): 如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。 软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。 SoftReference的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。 3.弱引用(GC发生时 可作为缓存(WeakHashMap)): 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只 能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只 被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。 4.虚引用(任何时候 监控对象回收,记录日志): 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的 存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚 引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中 虚引用和弱引用的区别: 弱引用应该还可以通过引用来获取对象,但是虚引用根本获取不到对象 虚引用存在于每一个对象里面,不会对对象的存活造成任何影响,唯一用处就是:能在对象被GC时收到系统通知 若某个对象与虚引用关联,那么在任何时候都可能被JVM回收掉。虚引用不能单独使用,必须配合引用队列一起使用。
5.1 何为GC以及为何要GC? GC(Garbage collection)称之为垃圾回收,是对内存中的垃圾对象,采用一定的算法进行内存回收的一个动作. 因为如果内存只使用不回收的话,很快就会占满,导致应用后续运行出现问题。
5.2 你知道哪些GC算法? /1.引用计数法: 这个算法是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象的时候,计数器就加 1,与之相反,每当引用失效的时候就减 1。也就是以计数来判断对象是否为垃圾。当某个对象的引用计数器的值为0时,表示这个对象不会在被实用,JVM中的GC被触发时,可回收这个对象。 /2.可达性分析法: 这个算法的核心思路就是通过一系列的“GC Roots”对象作为起始点,从这些对象开始往下搜索,搜索所经过的路径称之为“引用链”。当一个对象到 GC Roots 没有任何引用链相连的时候,证明此对象是可以被回收的。否则,证明这个对象有用,不是垃圾。 /3.标记清除: 标记清除(Mark-Sweep)算法分为“标记”和“清除”阶段,它首先会标记出内存中所有不需要回收的对象,然后从内存中清除所有未标记的对象。 /4.标记复制: 标记复制(Mark-Copy)算法是将内存分为大小相同的两块,当这一块使用完了,就把当前存活的对象复制到另一块,然后一次性清空当前区块。 /5.标记整理: 标记整理清除(Mark-Sweep-Compact)算法结合了“标记-清除”和“复制”两个算法的优点。第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把存活对象“压缩”复制到堆的其中一块空间中,按顺序排放。第三阶段清理掉存活边界以外的全部内存空间。 /6.分代回收: 年轻代:存活时间较短(这样的对象比较多)。 老年代:存活时间较长(这样的对象比较少)。
5.3 JVM中有哪些垃圾回收器? /1.年轻代和老年代的串行收集器:(Serial GC): 内部只使用一个线程执行垃圾回收(不能充分利用CPU的多核特性),无法并行化 /2.年轻代和老年代的并行收集器:(Parallel GC): 可利用CPU的多核特性执行多线程下的并行化GC操作。 GC期间, 所有 CPU 内核都在并行清理垃圾, 所以暂停时间较短。 最大优势是可实现可控的吞吐量与停顿时间。 /3.年轻代的并行收集器(Parallel New)+老年代的并发收集器(CMS-Concurrent Mark and Sweep): 使用空闲列表(free-lists)管理内存空间的回收,不对老年代进行碎片 整理,减少用户线程暂停时间。在标记-清除阶段的大部分工作和用户线程一起并发执行。最大优点是可减少停顿时间(可提高服务的响应速度),最大缺陷是老年代的内存碎片。 /4.年轻代和老年代的G1收集器,负责回收年轻代和老年代: 可以像CMS 收集器一样能同时和应用线程一起并发的执行。 减少整理内存空间时的停顿时间。 要满足可预测的GC停顿时间需求。 不能牺牲太多的吞吐性能。
5.4 服务频繁fullGc,youngGc次数较少,可能原因? 原因一: 系统承载高并发请求,或者处理数据量过大,导致Young GC很贫乏,而且每次Young GC过后存活对象太多,内存分配不合理,Survivor区过小,导致对象频繁进入老年代,频繁触发Full GC。 原因二: 系统一次性加载过多数据进内存,搞出来很多大对象,导致频繁有大对象进入老年带,必然频繁触发Full GC。 原因三: 系统发生了内存泄漏,莫名其妙创建大量的对象,始终无法回收,一直占用在老年代里,必然频繁触发Full GC。 原因四: Metaspace(永久代)因为加载类过多触发Full GC。 原因五: 误调用System.gc()触发Full GC。