JVM相关
jvm 内存结构
运行时数据区: 堆:存放实例对象的。 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器生成代码的。 程序计数器:为了支持多线程。 虚拟机栈和本地方法栈:用于支持方法的运行
jvm 调优参数
什么是类加载?何时类加载?类加载流程?
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口
类加载时机:
- 创建类的实例,也就是new一个对象
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(Class.forName("com.lyj.load"))
- 初始化一个类的子类(会首先初始化子类的父类)
- JVM启动时标明的启动类,即文件名和类名相同的那个类
流程:加载-连接(验证-准备-解析)-初始化-使用-卸载
•加载:查找并加载类的二进制数据
加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:
1、通过一个类的全限定名来获取其定义的二进制字节流。 2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。 相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。 加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。 •连接 **验证:**确保被加载的类的正确性 验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作: 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。 符号引用验证:确保解析动作能正确执行。 验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。 准备:为类的静态变量分配内存,并将其初始化为默认值 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意: 1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。 2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。 假设一个类变量的定义为:public static int value = 3; 那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。 3、如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。 假设上面的类变量value被定义为: public static final int value = 3; 编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。回忆上一篇博文中对象被动引用的第2个例子,便是这种情况。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中 解析: 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。 符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。 综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。 •初始化 为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
- 定义静态变量时指定初始值。如 private static String x="123";
- 在静态代码块里为静态变量赋值。如 static{ x="123"; } 如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析,到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。
类的生命周期是加载、连接、初始化、使用和卸载。其中加载、连接、初始化属于类的加载过程,连接又分为验证、准备、解析三个阶段。所以,类的加载过程可以说是:加载、验证、准备、解析、初始化五个流程。加载就是查找并加载类的二进制数据,然后在堆里创建一个class类对象;验证主要为了确保这个类的正确性,包括一些文件格式、元数据、字节码、符号引用验证;准备,为类的静态变量分配内存,并将其初始化为默认值;解析,把类中的符号引用转换为直接引用;初始化,为类的静态变量赋予正确的初始值。
知道哪些类加载器。类加载器之间的关系?
站在Java虚拟机的角度来讲,只存在两种不同的类加载器:启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。 站在Java开发人员的角度来看,类加载器可以大致划分为以下三类: 启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.* 开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。 **扩展类加载器:**Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。 **应用程序类加载:**Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。 应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
1)在执行非置信代码之前,自动验证数字签名。 2)动态地创建符合用户特定需要的定制化构建类。 3)从特定的场所取得java class,例如数据库中和网络中。
从开发角度来看类加载器可以分为三类:根加载器(BootStrap)一般用本地代码实现,负责加载 JVM 基础核心类库(rt.jar);扩展加载器( ExtClassLoader)从 java.ext.dirs 系统属性所指定的目录中加载类库,它的父加载器是 Bootstrap,加载扩展的jar包;系统加载器( AppClassLoader) 又叫应用类加载器,其父类是 Extension。它是应用最广泛的类加载器,它从环境变量 classpath 或者系统属性 java.class.path 所指定的目录中加载类,是用户自定义加载器的默认父加载器:用户自定义类加载器 ( java.lang.ClassLoader 的子类)父类是AppClassLoader
类加载器的双亲委派了解么? 结合 Tomcat 说一下双亲委派(Tomcat 如何打破双亲委托机制?...)。
双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。
为什么需要双亲委派
双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
双亲委派机制能够保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。
Java 内存模型
Java Memory Model Java能摆脱硬件的束缚,可以“一次编写,到处运行”,这不仅是因为虚拟机的功劳,也是因为提供了相对安全的内存管理和访问机制,让Java程序在不同平台下都能达到一致的内存访问效果,这种可以屏蔽各种硬件和操作系统的内存访问差异,我们称是Java内存模型。
在并发编程中存在一个最重要的问题就是,线程之间是如何通信的。在Java并发中,线程通信采用共享内存模型机制的,在共享内存模型中,线程间通过读、写内存中公共的状态进行隐式通信。采用内存共享的优点是,数据的共享使线程间的数据不用传送,而是直接访问内存,也加快了程序的效率,当然有利也有弊,共享内存没有提供同步的机制,这使得我们在使用共享内存进行进程间通信时,往往要借助其他的手段来进行进程间的同步工作。
在Java运行程序中有一些内存是线程共享的,例如Java中几乎所有的对象都存储在堆内存中,还有一些静态的常量,这些数据我们统称为共享变量,共享变量存储在主内存中。还有一些变量是每个线程独有的,存在在本地内存中,例如局部变量,方法内定义的参数还有异常处理器参数,这些数据不会在线程之间共享,我们所以不会存在可见性问题,也不受JMM影响,本地内存是JMM抽象的概念,实际并不存在,实际情况与主内存之间还存在来高速缓存、写缓冲区等。
https://blog.csdn.net/weixin_38003389/article/details/110103073 https://mp.weixin.qq.com/s/yxuGChcIga7SUe0hNMagsg
JMM就是Java内存模型(java memory model)。因为不同的硬件和不同的操作系统下,我们访问内存一定会有一些差异差异,所以会造成相同的代码运行在不同的系统上会出现各种不同的问题。java内存模型(JMM)屏蔽掉了各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。 JMM围绕三个特征建立起来的:原子性,可见性,有序性
栈中存放什么数据,堆中呢?
虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
大对象放在哪个内存区域
大对象直接进入老年代,大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
//新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。 -XX:PretenureSizeThreshold=1000000
设置-XX:PretenureSizeThreshold
这个参数,如果要创建的对象大于这个参数的值,比如分配一个超大的字节数组,此时就直接把这个大对象放入到老年代,不会经过新生代。
这么做就可以避免大对象在新生代,屡次躲过GC,还得把他们来复制来复制去的,最后才进入老年代,这么大的对象来回复制,是很耗费时间的。
堆区如何分类
Java堆区可以划分为新生代和老年代,新生代又可以进一步划分为Eden区、Survivor 1区、Survivor 2区。 新生代:老年代=1:2 dedn:s1:s2=8:1:1
垃圾回收有哪些算法
- 标记-清除算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
- 复制算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。缺点是浪费空间,优点是回收速度快,没碎片。
- 标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,完成碎片整理。
- 分代收集算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法,我觉得它更像是一种思想,而不是算法。
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
GC 的全流程
- Minor GC/Young GC:针对新生代的垃圾收集;
- Major GC/Old GC:针对老年代的垃圾收集。
- Full GC:针对整个Java堆以及方法区的垃圾收集。
大部分情况,对象都会首先在 Eden 区域分配,当第一次触发Minor GC,Eden区存活的对象被转移到Survivor区的某一块区域,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),以后再次触发Minor GC的时候,Eden区的对象连同一块Survivor区的对象一起,被转移到了另一块Survivor区,这样循环,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。
老年代是存储长期存活的对象的,占满时就会触发我们最常听说的Full GC,期间会停止所有线程等待GC的完成。所以对于响应要求高的应用应该尽量去减少发生Full GC从而避免响应超时的问题。
GC 中老年代用什么回收方法?
老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
如何识别垃圾?对象已经死亡?
常用有两种方式: 引用计数法,这种难以解决对象之间的循环引用的问题。 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
可达性分析算法,主流的JVM采用的是这种方式。 这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。