JVM总结

JVM

JAVA底层是C语言写的,但是Java,Javac,Jar包中的内容是java代码

JVM

\3. 对象创建方法,对象的内存分配,对象的访问定位。

\5. GC的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路?

\6. GC收集器有哪些?CMS收集器与G1收集器的特点。

\7. Minor GC与Full GC分别在什么时候发生?

\8. 几种常用的内存调试工具:jmap、jstack、jconsole。

\9. 类加载的五个过程:加载、验证、准备、解析、初始化。

\10. 双亲委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。

\11. 分派:静态分派与动态分派。

提示:

虚拟机产生的原因:

Java的核心就是Java虚拟机,因为所有的Java程序都在虚拟机上运行。Java程序的运行需要Java虚拟机、Java API和Java Class文件的配合。Java虚拟机实例负责运行一个Java程序。当启动一个Java程序时,一个虚拟机实例就诞生了。当程序结束,这个虚拟机实例也就消亡。

Java虚拟机由主机操作系统上的软件实现时,Java程序通过调用本地方法和主机进行交互。Java方法由Java语言编写,编译成字节码,存储在class文件中。本地方法由C/C++/汇编语言编写,编译成和处理器相关的机器代码,存储在动态链接库中,格式是各个平台专有。所以本地方法是联系Java程序和底层主机操作系统的连接方式。

  内置的安全机制的操作,也是Java虚拟机的特性:

  • 类型安全的引用转换

  • 结构化的内存访问

  • 自动垃圾收集

  • 数组边界检查

  • 空引用检查

JVM的体系结构:

说一下 JVM 的主要组成部分?及其作用?

  • 类加载器(ClassLoader)(用来装载.class文件)

  • 运行时数据区(Runtime Data Area)(方法区、堆、java栈、PC寄存器、本地方法栈)

  • 执行引擎(Execution Engine)(执行字节码,或者执行本地方法)

  • 本地库接口(Native Interface)

主要任务:装载class文件并且执行其中的字节码。

实现:使用类装载器(class loader)从程序和API中装载class文件,Java API中只有程序执行时需要的类才会被装载,字节码由执行引擎来执行。

组件的作用: 首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。

类装载器子系统负责查找并装载类型信息。Java程序并不是一个可执行文件,是由多个独立的类文件组成。这些类文件并非一次性全部装入内存,而是依据程序逐步载入

分类:

1.系统装载器:Java虚拟机实现的一部分

2.用户自定义装载器:后者则是Java程序的一部分

  • 启动类装载器(bootstrap class loader):JVM的根ClassLoader加载Java的核心API,是用原生代码来实现的(由C++实现),虚拟机自身的一部分,并不继承自java.lang.ClassLoader,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;。

  • 扩展类装载器(extensions class loader):加载Java扩展API(lib/ext中的类)加载 Java 的扩展库,负责加载\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

  • 应用程序类装载器(application class loader):加载Classpath目录下定义的class它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

  用户自定义装载器:继承 java.lang.ClassLoader类的方式实现自己的类装载器,以满足一些特殊的需求。(4)Custom ClassLoader

  • 属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据J2EE规范自行实现ClassLoader

  类装载器子系统涉及Java虚拟机的其它几个组成部分以及来自java.lang库的类。ClassLoader定义的方法为程序提供了访问类装载器机制的接口。此外,对于每一个被装载的类型,Java虚拟机都会为它创建一个java.lang.Class类的实例来代表该类型。和其它对象一样,用户自定义的类装载器以及Class类的实例放在内存中的堆区,而装载的类型信息则位于方法区。

类加载的执行过程?

加载,验证,准备,解析,初始化

  • 装载(查找并装载类型的二进制数据)定位和导入二进制class文件

  • 连接(执行验证:确保被导入类型的正确性;准备:为类变量分配内存,并将其初始化为默认值;解析:把类型中的符号引用转换为直接引用)验证被导入类的正确性,解析符号引用

  • 初始化(类变量初始化为正确初始值)为类变量分配并初始化内存

类装载分为以下 5 个步骤:

    • 加载:根据查找路径找到相应的 class 文件然后导入;

    • 验证:检查加载的 class 文件的正确性;

    • 准备:给类中的静态变量分配内存空间;

    • 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;

    • 初始化:对静态变量和静态代码块执行初始化工作。

加载 ——>连接(验证——>准备——>解析) ——>初始化

加载

类都必须加载到内存中才能运行起来,加载就是通过IO把字节码从硬盘迁移到内存中。

连接

  1. 验证:确保被加载的类的正确性

javac编译出来的类都是正确的,但是如果是通过其他途径生成的字节码呢?是不是正确的呢?就比如你自己建一个文本文件,然后重命名该文件为Test.class,然后让JVM来运行这个类,显然是错误的。

类文件的结构检查:确保类文件遵从java类文件的固定格式

语义检查:确保类本身符合Java语言的语法规定,比如验证final类型的类没有子类,被final修饰的方法不能被覆盖。

字节码验证:检验是否有合法的操作数。

二进制兼容性的验证:确保相互引用的类之间协调一致。比如在A类的xxx方法会调用B类的yyy方法,那么jvm在验证A类的时候会在方法去中检验是否存在B的yyy方法,如果不存在就会抛异常。

  1. 准备:为类的静态变量分配内存,并将其初始化为默认值

一定要是为静态变量分配内存,而不是实例变量,为什么强调静态变量,因为实例变量是生成实例的时候产生的,所以一般是在new一个对象的时候才对这个类进行实例化(前提是这个类已经被加载),而我们现在还没有加载完类,所以这个时候只能对静态变量分配内存空间(静态变量是属于这个类的而不属于某个对象)

  1. 解析:把类中的符号引用转换为直接引用

初始化:

为类的静态变量赋予正确的初始值,上面是赋予默认值,这里是赋予正确的初始值,什么是正确的初始值,就是用户给赋予的值。

双亲委托模式

  加载类时默认采用的是双亲委派机制。

原理:通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归。如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是java的类随着它的类加载器一起具备了一种带有优先级的层次关系。

  作用:

1)避免重复加载;

2)更安全。(防止了我们自己写的类对java核心代码的破坏) 如果不是双亲委派,那么用户在自己的classpath编写了一个java.lang.Object的类,那就无法保证Object的唯一性。所以使用双亲委派,即使自己编写了,但是永远都不会被加载运行。

破坏双亲委派机制

  双亲委派机制并不是一种强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。

  线程上下文类加载器,这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那么这个类加载器就是应用程序类加载器。像JDBC就是采用了这种方式。这种行为就是逆向使用了加载器,违背了双亲委派模型的一般性原则。

JVM中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。

loadClass的方法——>调用findLoadedClass来判断是否已经转入了内存——>找到它的加载器——>调用parent的loadClass,

什么是运行时包

要了解运行时包,我们先来设想一个问题,如果你自己定义了一个java.lang.A的类,能不能访问到java.lang.String类的friend成员?

不行,为什么?这就是运行时包在起作用,java的语法规定,包访问权限的成员能够被同一个包下的类访问,那是为什么不能够访问呢,这同样是为了防止病毒代码的破坏,java虚拟机只允许由同一个类装载器装载到同一包中的类型互相访问,而由同一类装载器装载,属于同一个包的,多个类型的集合就是我们所指的运行时包了。

3.将代码归入某类(保护域),该类确定了代码能够执行那些操作

除了1.屏蔽不同的命名空间,2.保护信任类库的边界外,类装载器的第三个重要的作用就是保护域,类装载器必须把代码放入到保护域中以限定这些代码运行时能够执行的操作的权限,这也如我上面讲的,像一个监狱一样,不让它在监狱意外的范围活动。

通过Class我们可以拿到对应的Classloader,那我们再来看一下Class这个对象如何拿到Classloader。

Class对象有一个getClassLoader的方法用于返回该类的类加载器,但有些实现可能使用null来标识引导类加载器(根类加载器)。也就是说当我们使用根加载器加载的对象使用此方法获取到的ClassLoader是null,为什么是这样呢?前面我们也已经说了,根类加载器是使用C++编写的,JVM不能够也不允许程序员获取该类,所以返回的是null,下面还有一句,如果此对象表示的是一个基本类型或void,则返回null,其实进一步的含义就是:Java中所有的基本数据类型都是由根加载器加载的!

运行时数据区

Java 虚拟机规范 5 个部分:

  • 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;

  • Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;

  • 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;

  • Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;

  • 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

 

1.程序计数器:记录程序运行状态,记录下一条程序,不会出现OOM,线程私有。如果线程正在执行的是Native方法,则计数器值为空;

2.栈:存储栈帧(局部变量区,操作数栈),每一个线程执行方法时都会创建栈帧,会出现OOM,线程私有

3.堆:用来存放所有new的对象,垃圾回收的地方,被分为新生代和老年代,线程公有

4.方法区:存放被加载的类信息,方法,静态属性,final属性,jvm利用永久代存放方法区(运行时常量池: 线程共享 ,是方法区的一部分, C lass 文件中存放编译期生成的各种字面量和符号引用,类加载后进入方法区的运行时常量池中。)

5.本地方法栈:用于支持native本地方法,存储每个本地方法的调用状态

 

运行时数据区包括:程序计数器、虚拟机栈、本地方法栈、Java堆、方法区以及方法区中的运行时常量池

 

2、虚拟机栈:为虚拟机执行 Java 方法(字节码)服务,每个方法在执行的时会创建一个栈帧用于存放局部变量表、操作数栈、动态链接和方法出口等信息,每个方法的调用直至执行完成对应于栈帧的入栈和出栈;

3、本地方法栈: 为虚拟机使用的 Native 方法服务,也是 线程私有

4、Java 堆: 在虚拟机启动时创建, 线程共享 ,唯一目的是存放对象实例,是垃圾收集器管理的主要区域——” GC 堆“,可以细分为新生代和老年代,新生代又可以细分为 Eden 空间、 From Survivor 空间和 To Survivor 空间;物理上可以不连续,但逻辑上连续,可以选择固定大小或者扩展;

5、方法区: 线程共享 ,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。被称为“永久代”,是因为 HotSpot 虚拟机的设计团队把 GC 分代收集扩展到方法区,即使用永久代来实现方法区,像 GC 管理 Java 堆一样管理方法区,从而省去专门为方法区编写内存管理代码,内存回收目标是针对常量池的回收和堆类型的卸载;

在JDK1.8中,Perm区(持久代)中所有的内容中字符串常量移动至堆内存,其他内容包括类元信息,字段,静态属性,方法,常量等移动至元空间。

程序计数器(Program Counter Register)

  程序计数器也叫PC寄存器。程序计数器既能持有一个本地指针,也能持有一个returnAddress。当线程执行某个Java方法时,程序计数器的值总是下一条被执行指令的地址。这里的地址可以是一个本地指针,也可以是方法字节码中相对该方法起始指令的偏移量。如果该线程正在执行一个本地方法,那么此时程序计数器的值是“undefined”。

特点:

  • 当前线程所执行的字节码的行号指示器

  • 当前线程私有

  • 不会出现OutOfMemoryError情况

A. 程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。

B. 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

C. 由于Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

D. 如果线程正在执行的是一个Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie 方法,这个计数器值则为空。此内存区域是唯一一个在Java 虚拟机规范中没有规定任何OOM 情况的区域。

Java堆(Heap)

  a. 对于大多数应用来说,Java 堆是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。

b. 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

c. 这一点在Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT 编译器的发展与逃逸分析技术的逐渐成熟,栈上分配,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

d. Java 堆是垃圾收集器管理的主要区域。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java 堆中还可以细分为:新生代和老年代;再细致一点的有Eden 空间、From Survivor 空间、To Survivor 空间等。不过,无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。

e. 根据Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

f. 如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。

运行时创建的所有类实例或数组(数组在Java虚拟机中是一个真正的对象)都放在同一个堆中。所有线程都将共享这个堆。

  • 被所有线程共享的一块内存区域,在虚拟机启动时创建

  • 用来存储对象实例

  • 可以通过-Xmx和-Xms控制堆的大小

  • OutOfMemoryError异常:当在堆中没有内存完成实例分配,且堆也无法再扩展时。

  java堆是垃圾收集器管理的主要区域。java堆还可以细分为:新生代(New/Young)、旧生代/年老代(Old/Tenured)。持久代(Permanent)在方法区,不属于Heap。

 

新生代:新建的对象都由新生代分配内存。常常又被划分为Eden区和Survivor区。Eden空间不足时会把存活的对象转移到Survivor。新生代的大小可由-Xmn控制,也可用-XX:SurvivorRatio控制Eden和Survivor的比例。

旧生代:存放经过多次垃圾回收仍然存活的对象。

持久代:存放静态文件,如今Java类、方法等。持久代在方法区,对垃圾回收没有显著影响。

java虚拟机栈(VM Stack)

  栈由许多栈帧组成,一个栈帧包含一个Java方法调用的状态。当线程调用一个Java方法时,虚拟机压入一个新的栈帧到该线程的Java栈中,当该方法返回时,这个栈帧就从Java栈中弹出。Java栈存储线程中Java方法调用的状态--包括局部变量、参数、返回值以及运算的中间结果等。Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。这样设计的原因是为了保持Java虚拟机的指令集尽量紧凑,同时也便于Java虚拟机在只有很少通用寄存器的平台上实现。另外,基于栈的体系结构,也有助于运行时某些虚拟机实现的动态编译器和即时编译器的代码优化。

  • 线程私有,生命周期与线程相同

  • 存储方法的局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息。

  • java方法执行的内存模型,每个方法执行的同时都会创建一个栈帧,每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  • StackOverflowError异常:当线程请求的栈深度大于虚拟机所允许的深度

  • OutOfMemoryError异常:如果栈的扩展时无法申请到足够的内存

  JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量、部分的返回结果以及Stack Frame。其他引用类型的对象在JVM栈上仅存放变量名和指向堆上对象实例的首地址

a. 与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期与线程相同。

b. 每个方法被执行的时候都会同时创建一个栈帧,用于存储局部变量表、操作栈、静态解析、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

\1. 局部标量表,用于存放 方法参数局部变量。变量槽是局部变量表的最小单位。

局部变量表存放了编译期可知的各种基本数据类型、对象引用和returnAddress 类型(指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

\2. 操作数栈,是一个后入先出栈。

\3. Class 文件中存放了大量的符号引用,这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析**0。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接**。

a. 在Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError 异常。

栈帧

  栈帧由局部变量区、操作数栈和帧数据区组成。当虚拟机调用一个Java方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并根据此分配栈帧内存,然后压入Java栈中。

局部变量区

  局部变量区被组织为以字长为单位、从0开始计数的数组。字节码指令通过从0开始的索引使用其中的数据。类型为int, float, reference和returnAddress的值在数组中占据一项,而类型为byte, short和char的值在存入数组前都被转换为int值,也占据一项。但类型为long和double的值在数组中却占据连续的两项。

操作数栈

  和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组。它通过标准的栈操作访问--压栈和出栈。由于程序计数器无法被程序指令直接访问,Java虚拟机的指令是从操作数栈中取得操作数,所以它的运行方式是基于栈而不是基于寄存器。虚拟机把操作数栈作为它的工作区,因为大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。

帧数据区

  除了局部变量区和操作数栈,Java栈帧还需要帧数据区来支持常量池解析、正常方法返回以及异常派发机制。每当虚拟机要执行某个需要用到常量池数据的指令时,它会通过帧数据区中指向常量池的指针来访问它。除了常量池的解析外,帧数据区还要帮助虚拟机处理Java方法的正常结束或异常中止。如果通过return正常结束,虚拟机必须恢复发起调用的方法的栈帧,包括设置程序计数器指向发起调用方法的下一个指令;如果方法有返回值,虚拟机需要将它压入到发起调用的方法的操作数栈。为了处理Java方法执行期间的异常退出情况,帧数据区还保存一个对此方法异常表的引用。

本地方法栈(Native Method Stack)

  任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的栈,虚拟机只是简单地动态连接并直接调用指定的本地方法。

其中方法区和堆由该虚拟机实例中所有线程共享。当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息,然后把这些类型信息放到方法区。当程序运行时,虚拟机会把所有该程序在运行时创建的对象放到堆中。

像其它运行时内存区一样,本地方法栈占用的内存区可以根据需要动态扩展或收缩。

  • 与虚拟机栈相似,主要为虚拟机使用到的Native方法服务,在HotSpot虚拟机中直接把本地方法栈与虚拟机栈二合一

本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native 方法服务。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期的符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

Java 虚拟机对Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中①。

运行时常量池相对于Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只能在编译期产生,也就是并非预置入Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String 类的intern() 方法。

既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError 异常

方法区

  被装载的类型信息存储在一个方法区的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件并将它传输到虚拟机中,接着虚拟机提取其中的类型信息,并将这些信息存储到方法区。方法区也可以被垃圾回收器收集,因为虚拟机允许通过用户定义的类装载器来动态扩展Java程序。

a. 方法区与Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量池、静态变量、即时编译器编译后的代码等数据。

b. 根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OOM异常。

  方法区中存放了以下信息:

  • 这个类型的全限定名(如全限定名java.lang.Object)

  • 这个类型的直接超类的全限定名

  • 这个类型是类类型还是接口类型

  • 这个类型的访问修饰符(public, abstract, final的某个子集)

  • 任何直接超接口的全限定名的有序列表

  • 该类型的常量池(一个有序集合,包括直接常量[string, integer和floating point常量]和对其它类型、字段和方法的符号引用)

  • 字段信息(字段名、类型、修饰符)

  • 方法信息(方法名、返回类型、参数数量和类型、修饰符)

  • 除了常量以外的所有类(静态)变量

  • 指向ClassLoader类的引用(每个类型被装载时,虚拟机必须跟踪它是由启动类装载器还是由用户自定义类装载器装载的)

  • 指向Class类的引用(对于每一个被装载的类型,虚拟机相应地为它创建一个java.lang.Class类的实例。比如你有一个到java.lang.Integer类的对象的引用,那么只需要调用Integer对象引用的getClass()方法,就可以得到表示java.lang.Integer类的Class对象)

  • 线程间共享

  • 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

  • OutOfMemoryError异常:当方法区无法满足内存的分配需求时

    • 方法区的一部分

    • 用于存放编译期生成的各种字面量与符号引用,如String类型常量就存放在常量池

    • OutOfMemoryError异常:当常量池无法再申请到内存时

永久代

1. 方法区(method area)只是JVM规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,具体放在哪里,不同的实现可以放在不同的地方。而永久代Hotspot虚拟机特有的概念,是方法区的一种实现,别的JVM都没有这个东西。

2.类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

3.在1.8中HotSpot 已经没有永久代这个区间了,取而代之是一个叫做元空间的东西。

4.元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

 

java是个动态语言,运行时可以拿到类的定义信息,还可以产生动态的类,所以需要一个区域专门放类的定义 ,在JDK 8之前 叫永久代区,永久代区的信息很难清理,因为不知道某个类是不是会再次被用到。而且最初sun 也只是设置了较小的区域给永久代,因为认为类在程序启动的时候都基本被确定了,不需要很大,但是因为现在的程序大量使用到动态特性,比如web程序中的jsp,就是会被动态编译,各种框架也会用到动态代理机制,所以产生了大量类放到了永久代,不小心就产生了PermGen out of memeory 错误。到了JDK 8,为了解决这个问题,取而代之的是 MetaSpace,其实MetaSpace只是默认下放大的限制,默认是无限。。。

 

直接内存(Direct Memory)

  • 直接内存并不是虚拟机运行的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁使用

  • NIO可以使用Native函数库直接分配堆外内存,堆中的DirectByteBuffer对象作为这块内存的引用进行操作

  • 大小不受Java堆大小的限制,受本机(服务器)内存限制

  • OutOfMemoryError异常:系统内存不足时

  总结:Java对象实例存放在堆中;常量存放在方法区的常量池;虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据放在方法区;以上区域是所有线程共享的。栈是线程私有的,存放该方法的局部变量表(基本类型、对象引用)、操作数栈、动态链接、方法出口等信息。

  一个Java程序对应一个JVM,一个方法(线程)对应一个Java栈。

执行引擎

  在Java虚拟机规范中,执行引擎的行为使用指令集定义。实现执行引擎的设计者将决定如何执行字节码,实现可以采取解释、即时编译或直接使用芯片上的指令执行,还可以是它们的混合。

  执行引擎可以理解成一个抽象的规范、一个具体的实现或一个正在运行的实例。抽象规范使用指令集规定了执行引擎的行为。具体实现可能使用多种不同的技术--包括软件方面、硬件方面或树种技术的结合。作为运行时实例的执行引擎就是一个线程。

  运行中Java程序的每一个线程都是一个独立的虚拟机执行引擎的实例。从线程生命周期的开始到结束,它要么在执行字节码,要么执行本地方法。

指令集

  方法的字节码流由Java虚拟机的指令序列构成。每一条指令包含一个单字节的操作码,后面跟随0个或多个操作数。操作码表示需要执行的操作;操作数向Java虚拟机提供执行操作码需要的额外信息。当虚拟机执行一条指令时,可能使用当前常量池中的项、当前帧的局部变量中的值或者位于当前帧操作数栈顶端的值。

  抽象的执行引擎每次执行一条字节码指令。Java虚拟机中运行的程序的每个线程(执行引擎实例)都执行这个操作。执行引擎取得操作码,如果操作码有操作数,就取得它的操作数。它执行操作码和跟随的操作数规定的动作,然后再取得下一个操作码。这个执行字节码的过程在线程完成前将一直持续,通过从它的初始方法返回,或者没有捕获抛出的异常都可以标志着线程的完成。

本地方法接口

  Java本地接口,也叫JNI(Java Native Interface),是为可移植性准备的。本地方法接口允许本地方法完成以下工作:

  • 传递或返回数据

  • 操作实例变量

  • 操作类变量或调用类方法

  • 操作数组

  • 对堆的对象加锁

  • 装载新的类

  • 抛出异常

  • 捕获本地方法调用Java方法抛出的异常

  • 捕获虚拟机抛出的异步异常

  • 指示垃圾收集器某个对象不再需要

创建一个对象的过程:

首先栈中的main函数执行Person = new Person("wqh",24);这个简单的语句会涉及到如下几个步骤:

1、由于是要创建Person类对象,java虚拟机(JVM)先去找Person.class文件,如果有的话,将其加载到内存。

2、将类型信息(包括静态变量,方法等)加载进方法区。

3、执行该类中static代码块。

4、到这时才进行堆内存空间的开辟,并为对象分配首地址。

5、在堆内存中建立对象的成员属性,并对其进行初始化(先进行默认初始化再进行显示初始化)。

6、进行构造代码块的初始化,由此看出构造代码块初始化的优先级要高于对象构造函数的初始化。

7、对象的构造函数进行初始化。

8、将堆内存中的地址(引用)赋给栈内存中的p变量。

GC

5. JVM垃圾回收(GC)

分类:

  • 新生代——>minor GC;

  • 旧生代——>Full GC;

young gc和full gc触发条件

young gc :eden空间不足

full gc :显示调用System.GC、旧生代空间不足、Permanet Generation空间满、CMS GC时出现promotion failed和concurrent mode failure、 RMI等的定时触发、YGC时的悲观策略、dump live的内存信息时

  • 调用System.gc()的GC为Full GC。

    后台线程gc执行的,自动运行无需显示调用。

    java.lang.System.gc():该方法也只会提醒系统进行垃圾回收,但系统不一定会回应,可能会不予理睬。

垃圾回收机制:将内存中不再被引用的对象进行回收,GC中用于回收的方法称为收集器。

垃圾回收器线程——>系统级的线程,GC是完全自动的,不能被强制执行。最多只能用System.gc()来建议执行垃圾回收器回收内存,但是具体的回收时间,是不可知的。

垃圾:当对象的引用变量被赋值为null,可能被当成垃圾。

GC需要消耗一些资源和时间,Java在对对象的生命周期特征进行分析后,按照新生代、旧生代的方式来对对象进行收集,以尽可能的缩短GC对应用造成的暂停。

垃圾标准:

(1)对象赋予了空值,且之后再未调用(obj = null;)

(2)对象赋予了新值,即重新分配了内存空间(obj = new Obj();)

内存泄漏:1)对象是可达的;2)对象是无用的。

解决办法:

  保不需要的对象不可达,将对象字段设置为null的方式,或从容器collection中移除对象。局部变量不再使用时无需显示设置为null,因为对局部变量的引用会随着方法的退出而自动清除。

内存泄露的原因:1)全局集合;2)缓存;3)ClassLoader

两种对象存活算法

    • 引用计数器:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;并且开销较大,频繁且大量的引用变化,带来大量的额外运算;

    • 可达性分析:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。

可作为GC Roots的对象包括以下几种:

1、虚拟机栈(栈帧中的本地变量表)中引用的对象;

2、方法区中类静态属性引用的对象;

3、方法区中常量引用的对象;

4、本地方法栈中JNI(即一般说的Native方法(本地方法))引用的对象。

而最简单的Java栈就是Java程序执行的main函数。

第一次标记

在可达性分析后发现到GC Roots没有任何引用链相连时,被第一次标记;

并且进行一次筛选:此对象是否必要执行finalize()方法;

(A)、没有必要执行

没有必要执行的情况:

(1)、对象没有覆盖finalize()方法;

(2)、finalize()方法已经被JVM调用过;

这两种情况就可以认为对象已死,可以回收;

(B)、有必要执行

对有必要执行finalize()方法的对象,被放入F-Queue队列中;

稍后在JVM自动建立、低优先级的Finalizer线程(可能多个线程)中触发这个方法;

2、第二次标记

GC将对F-Queue队列中的对象进行第二次小规模标记;

finalize()方法是对象逃脱死亡的最后一次机会:

(A)、如果对象在其finalize()方法中重新与引用链上任何一个对象建立关联,第二次标记时会将其移出"即将回收"的集合;

(B)、如果对象没有,也可以认为对象已死,可以回收了;

一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;

回收算法:

1.标记清除(Mark-Sweep)

标记+清除两个阶段,在标记阶段,垃圾收集器跟踪从根对象的引用,在追踪的过程中对遇到的对象打一个标记,最终未被标记的对象就是垃圾对象,在清除阶段,回收垃圾对象占用的内存。可以在对象本身添加跟踪标记,也可以用一个独立的位图来设置标记。标记清除法是基础的收集 算法,其他算法大多时针对这个算法缺点的改进。

存在效率和碎片问题。

2.复制算法(Copying)

将内存划分为大小相等的两个区域,每次只使用其中的一个区域,当这个区域的内存用完了,就将可触及的对象直接复制到新的区域并连续存放以 消除内存碎片,当可触及对象复制完后,清除旧内存区域,修改引用的值。这种算法明显缺点是浪费内存,故实际使用中常将新生代划分成8:1:1三个区。

3.标记整理(Mark-Compact)

标记整理算法中标记的过程同标记清理一样,但整理部分不是直接清除掉垃圾对象,而是将活动对象统一移动一内存的一端,然后清除边界外的内 存区域,这样就避免了内存碎片。也不会浪费内存,不需要其他内存进行担保分代收集。

4.分代收集

基于对对象生命周期分析后得出的垃圾回收算法。现在的垃圾回收器(从J2SE1.2开始)都是使用此算法的。

回收策略:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。

新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:

  • 把 Eden + From Survivor 存活的对象放入 To Survivor 区;

  • 清空 Eden 和 From Survivor 分区;

  • From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。

每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。

老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程池连接、数据库连接、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。

分代:

堆中的共划分为三个代:年轻代、年老点和持久代。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

如图表示默认内存比例

uploading.4e448015.gif转存失败重新上传取消

堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块Survivor 区域是空闲着的。

因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC、FullGC ( 或称为 Major GC )。

Java 引用类型

  • 强引用:发生 gc 的时候不会被回收。

  • 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。

  • 弱引用:有用但不是必须的对象,在下一次GC时会被回收。

  • 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。

垃圾回收器

Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS(ConCurrent Mark Sweep)、G1(Garbage First)

收集器算法特点适应代
Serial复制单线程串行(stop the world)简单高效新生代
Serial Old标记-整理单线程老年代
ParNew复制Serial 的多线程版本,stop the world新生代
Parallel Scavenge复制多线程(吞吐量优先)高效的利用CPU时间新生代
Parallel Old标记-整理 老年代
CMS标记-清除多线程并发,最短停顿时间为目标老年代
G1 JDK 9 以后的默认整堆

六,CMS收集器

1,初始标记:该步会stop the world,但耗时非常短,标记GC Root直接关联的对象

2,并发标记:耗时较长,用户线程可同时运行,标记至GC Root有可达路径的对象

3,重新标记:该步会stop the world,但耗时非常短。由于步骤2中用户线程会同步运行,此时主要修正因步骤2中用户线程同步运行产生的对象标记变动

4,并发清除:耗时较长,用户线程可同时运行

在耗时很长的并发标记阶段和并发清除阶段用户线程和收集线程都可同时工作,故而总体上来说,CMS收集器的内存回收是与用户线程一起并发执行的

并发清除时,用户线程是可以同时运行的,此时用户线程会产生新的垃圾,这部分垃圾在标记过程之后产生,本次GC已经不能进行标记后清除,只能留到下次GC时处理,被称为浮动垃圾

由于CMS的收集线程执行时,用户线程也是会同时执行的,导致CMS收集器无法像其它老年代收集器那样在老年代内存几乎耗尽时再进行GC,必须为用户线程预留部分内存

七,G1收集器

JDK从1.7版本开始提供

G1收集器将整个堆内存划分为2048个大小相同的独立区域块,每个区域块的大小根据堆的实际大小而定,整体被控制在1M-32M之间,G1收集器跟踪区域中的垃圾堆积情况并在后台维护一个优先级列表,每次根据设置的垃圾回收时间回收优先级最高的区域,这样可以避免整个新生代或整个老年代的垃圾回收,使得stop the world的时间更短、更可控,同时在有限的时间内可以获得最高的回收效率。

在注重吞吐量或CPU资源敏感的场合,可以优先考虑Parallel Scavenge收集器 + Parallel Old收集器

204. 详细介绍一下 CMS 垃圾回收器?

CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。

CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。

cms回收器的标记过程,内存碎片问题

cms回收器的标记过程:

1.STW initial mark:第一次暂停,初始化标记,从root标记old space存活对象(the set of objects reachable from roots (application code))

2.Concurrent marking:运行时标记,从上一步得到的集合出发,遍历old space,标记存活对象 (all live objects that are transitively reachable from previous set)

3.Concurrent precleaning:并发的标记前一阶段被修改的对象(card table)

4.STW remark:第二次暂停,检查,标记,检查脏页的对象,标记前一阶段被修改的对象 (revisiting any objects that were modified during the concurrent marking phase)

内存碎片问题:

CMS基于“标记-清除”算法,进行垃圾回收后会存在内存碎片,当申请大的连续内存时可能内存不足,此时需要进行一次Full GC,可以通过参数指定进行Full GC后或进行多少次Full GC后进行一次内存压缩来整理内存碎片。

jdk11中的ZGC垃圾收集器

在JDK 11当中,加入了实验性质的ZGC。它的回收耗时平均不到2毫秒。它是一款低停顿高并发的收集器。

ZGC几乎在所有地方并发执行的,除了初始标记的是STW的。ZGC 是一个并发、基于区域(region)、增量式压缩的收集器。Stop-The-World 阶段只会在根对象扫描(root scanning)阶段发生,这样的话 GC 暂停时间并不会随着堆和存活对象的数量而增加。所以停顿时间几乎就耗费在初始标记上,这部分的实际是非常少的。那么的呢?

其他阶段可以并发执行的原因:新增了两项技术:一个是着色指针Colored Pointer,另一个是读屏障Load Barrier。

ZGC 的设计目标: TB 级别的堆内存管理; 最大 GC Pause 不高于 10ms; 最大的吞吐率(Throughput)损耗不高于 15%; 关键点:GC Pause 不会随着 堆大小的增加 而增大。

着色指针Colored Pointer:

ZGC利用指针的64位中的几位表示Finalizable、Remapped、Marked1、Marked0(ZGC仅支持64位平台),以标记该指向内存的存储状态。相当于在对象的指针上标注了对象的信息。注意,这里的指针相当于Java术语当中的引用。

在这个被指向的内存发生变化的时候(内存在Compact被移动时),颜色就会发生变化。

在G1的时候就说到过,Compact阶段是需要STW,否则会影响用户线程执行。那么怎么解决这个问题呢?

读屏障Load Barrier:

由于着色指针的存在,在程序运行时访问对象的时候,可以轻易知道对象在内存的存储状态(通过指针访问对象),若请求读的内存在被着色了,那么则会触发读屏障。读屏障会更新指针再返回结果,此过程有一定的耗费,从而达到与用户线程并发的效果。

把这两项技术联合下理解,引用R大(RednaxelaFX)的话

与标记对象的传统算法相比,ZGC在指针上做标记,在访问指针时加入Load Barrier(读屏障),比如当对象正被GC移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是,永远只有单个对象读取时有概率被减速,而不存在为了保持应用与GC一致而粗暴整体的Stop The World。

ZGC虽然目前还在JDK 11还在实验阶段,但由于算法与思想是一个非常大的提升,相信在未来不久会成为主流的GC收集器使用。

永久代中会发生垃圾回收么?

不会,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。这时永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。请参考下Java8:从永久代到元数据区 /22、为什么young区中的E:S:S是8:1:1呢

为什么Eden区一般比Survivor区大很多:

新生代GC算法采用的是复制清除,而不是标记清除。而Java中大部分的对象是朝生夕死的,也就是说在一次young gc后,大部分的对象都不存在,根据经验只有10%的对象最后会进入Survivor。

将survivor区设得较小,相应的Eden区就可以获得更大的内存,减少GC次数。

-xmx和-xms是用来做什么的呢,如果设置一样,有什么好处

最大和最小堆空间。如果设置一样,JVM不会自动调整堆大小,因此也不会引入调整时的损耗。

 

12.JVM垃圾收集在ParNew+CMS条件下,哪些情况下会让JVM认为产生了一次FULL GC?

JVM认为在老年代或者永久区发生的gc行为就是Full GC,在ParNew+CMS条件下,发生Full GC的原因通常为:

a) 永久区达到一定比例。

b) 老年代达到一定比例。

c) 悲观策略。

d) System.gc(), jmap -dump:live, jmap -histo:live 等主动触发的。

JVM生命周期

  • 启动。启动一个Java程序时,一个JVM实例就产生了,任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点。

  • 运行。main()作为该程序初始线程的起点,任何其他线程均由该线程启动。

  • 消亡。当程序中的所有非守护线程都终止时,JVM才退出;若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出。

  一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。程序开始执行时他才运行,程序结束时他就停止。你在同一台机器上运行三个程序,就会有三个运行中的Java虚拟机。 Java虚拟机总是开始于一个main()方法,这个方法必须是公有、返回void、直接受一个字符串数组。在程序执行时,你必须给Java虚拟机指明这个包换main()方法的类名。main()方法是程序的起点,他被执行的线程初始化为程序的初始线程。程序中其他的线程都由他来启动。

  Java中的线程分为两种:守护线程 (daemon)和普通线程(non-daemon)。守护线程是Java虚拟机自己使用的线程,比如负责垃圾收集的线程就是一个守护线程。当然,你也可以把自己的程序设置为守护线程。包含main()方法的初始线程不是守护线程。

  只要Java虚拟机中还有普通的线程在执行,Java虚拟机就不会停止。如果有足够的权限,你可以调用exit()方法终止程序。

Java中编译和运行的区别。

编译:编译时是调用检查你的源程序是否有语法错误,如果没有就将其翻译成字节码文件。即.class文件。

Java编译一个类时,如果这个类所依赖的类还没有被编译,编译器就会先编译这个被依赖的类,然后引用,否则直接引用。如果java编译器在指定目录下找不到该类所其依赖的类的.class文件或者.java源文件的话,编译器话报“cant find symbol”的错误。

编译后的字节码文件格式主要分为两部分:常量池和方法字节码。常量池记录的是一些类名,成员变量名等等 以及 符号引用(方法引用,成员变量引用等等);方法字节码放的是类中各个方法的字节码。

运行:运行时是java虚拟机解释执行字节码文件。java类运行的过程大概可分为两个过程:1、类的加载 2、类的执行。

需要说明的是:JVM主要在程序第一次主动使用类的时候,才会去加载该类。也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。

Java代码的编译和执行包括了三个重要机制:

(1)Java源码编译机制(.java源代码文件 -> .class字节码文件)

(2)类加载机制(ClassLoader)

(3)类执行机制(JVM执行引擎)

4.1 Java源码编译机制

  Java源代码是不能被机器识别的,需要先经过编译器编译成JVM可以执行的.class字节码文件,再由解释器解释运行。即:Java源文件(.java) -- Java编译器 --> Java字节码文件 (.class) -- Java解释器 --> 执行。流程图如下:

  字节码文件(.class)是平台无关的。

  Java中字符只以一种形式存在:Unicode。字符转换发生在JVM和OS交界处(Reader/Writer)。

  最后生成的class文件由以下部分组成:

  • 结构信息。包括class文件格式版本号及各部分的数量与大小的信息

  • 元数据。对应于Java源码中声明与常量的信息。包含类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池

  • 方法信息。对应Java源码中语句和表达式对应的信息。包含字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息

4.3 类执行机制

Java字节码的执行是由JVM执行引擎来完成,流程图如下所示:

  JVM是基于栈的体系结构来执行class字节码的。线程创建后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。

主要的执行技术:解释,即时编译,自适应优化、芯片级直接执行

  • 解释属于第一代JVM,

  • 即时编译JIT属于第二代JVM,

  • 自适应优化(目前Sun的HotspotJVM采用这种技术)则吸取第一代JVM和第二代JVM的经验,采用两者结合的方式

  开始对所有的代码都采取解释执行的方式,并监视代码执行情况。对那些经常调用的方法启动一个后台线程,将其编译为本地代码,并进行优化。若方法不再频繁使用,则取消编译过的代码,仍对其进行解释执行

内存调优

调优目的:减少GC的频率尤其是Full GC的次数

主要手段:主要通过配置JVM的参数合理分配堆内存各部分的比例,和GC策略来实现。

导致Full GC的几种情况和调优策略:

  • 旧生代空间不足 调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象

  • 持久代(Pemanet Generation)空间不足 增大Perm Gen空间,避免太多静态对象

  • 统计得到的GC后晋升到旧生代的平均大小大于旧生代剩余空间 控制好新生代和旧生代的比例

  • System.gc()被显示调用 垃圾回收不要手动触发,尽量依靠JVM自身的机制

堆内存比例不良设置会导致什么后果:

1)新生代设置过小

一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入旧生代,占据了旧生代剩余空间,诱发Full GC

2)新生代设置过大

一是新生代设置过大会导致旧生代过小(堆总量一定),从而诱发Full GC;二是新生代GC耗时大幅度增加

一般说来新生代占整个堆1/3比较合适

3)Survivor设置过小

导致对象从eden直接到达旧生代,降低了在新生代的存活时间

4)Survivor设置过大

导致eden过小,增加了GC频率

另外,通过-XX:MaxTenuringThreshold=n来控制新生代存活时间,尽量让对象在新生代被回收

 

JVM提供两种较为简单的GC策略的设置方式:

1)吞吐量优先

JVM以吞吐量为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,来达到吞吐量指标。这个值可由-XX:GCTimeRatio=n来设置

2)暂停时间优先

JVM以暂停时间为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,尽量保证每次GC造成的应用停止时间都在指定的数值范围内完成。这个值可由-XX:MaxGCPauseRatio=n来设置

JVM常见配置

  1. 堆设置

    • -Xms:初始堆大小

    • -Xmx:最大堆大小

    • -XX:NewSize=n:设置年轻代大小

    • -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4

    • -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5

    • -XX:MaxPermSize=n:设置持久代大小

  2. 收集器设置

    • -XX:+UseSerialGC:设置串行收集器

    • -XX:+UseParallelGC:设置并行收集器

    • -XX:+UseParalledlOldGC:设置并行年老代收集器

    • -XX:+UseConcMarkSweepGC:设置并发收集器

  3. 垃圾回收统计信息

    • -XX:+PrintGC

    • -XX:+PrintGCDetails

    • -XX:+PrintGCTimeStamps

    • -Xloggc:filename

  4. 并行收集器设置

    • -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。

    • -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间

    • -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

  5. 并发收集器设置

    • -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。

    • -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

堆和栈

栈是一种具有后进先出性质的数据结构,也就是说后存放的先取,先存放的后取。

堆是一种经过排序的树形数据结构,每个结点都有一个值。通常我们所说的堆的数据结构,是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。由于堆的这个特性,常用来实现优先队列,堆的存取是随意的。

为什么要划分堆和栈

1、从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。

2、堆与栈的分离,使得堆中的内容可以被多个栈共享。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。

3、栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。

4、体现了Java面向对象这一核心特点(也可以继续说一些自己的理解)

对象等级晋升

(一)Minor GC

Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。

当对象**在Eden区和From Survivor区,(Survivor区“To”是空的),在经过一次 Minor GC 后,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是15岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 )会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域,经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。**

晋升老年代的条件:

a. 对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。

b. survivor空间中相同年龄所有对象大小总和大于servivor空间的一半,年龄大于等于该年龄的对象可直接进入老年代。

c. 长期存活对象,年龄到达15岁,进入老年代

(二)**Full GC**

Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除或标记-整理算法

老年代里面的对象几乎都是在 Survivor 区域中熬过来的, Full GC 发生的次数不

会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。

另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。

JVM调优

调优方法

调优手段主要是通过控制堆内存的各个部分的比例,

1)新生代设置过小

一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入旧生代,占据了旧生代剩余空间,诱发Full GC

2)新生代设置过大

一是新生代设置过大会导致旧生代过小(堆总量一定),从而诱发Full GC;二是新生代GC耗时大幅度增加

一般说来新生代占整个堆1/3比较合适

3)Survivor设置过小

导致对象从eden直接到达旧生代,降低了在新生代的存活时间

4)Survivor设置过大

导致eden过小,增加了GC频率

堆设置

-Xms:初始堆大小

-Xmx:最大堆大小

-XX:NewSize=n:设置年轻代大小

-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4

-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5

-XX:MaxPermSize=n:设置持久代大小

 

JVM参数列表

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

-Xmx3550m:最大堆内存为3550M。

-Xms3550m:初始堆内存为3550m。

此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。

-XXn2g:设置年轻代大小为2G。

整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

-Xss128k:设置每个线程的堆栈大小。

JDK5.0以后每个线程堆栈大小为1M,在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 3000~5000左右。

-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5

-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。

设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6

-XX:MaxPermSize=16m:设置持久代大小为16m。

-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。

如果设置为0的话,则年轻代对象不经过Survivor区,直 接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象 再年轻代的存活时间,增加在年轻代即被回收的概论。

 

收集器设置

-XX:+UseSerialGC:设置串行收集器

-XX:+UseParallelGC:设置并行收集器

-XX:+UseParalledlOldGC:设置并行年老代收集器

-XX:+UseConcMarkSweepGC:设置并发收集器

垃圾回收统计信息

-XX:+PrintGC

-XX:+PrintGCDetails

-XX:+PrintGCTimeStamps

-Xloggc:filename

并行收集器设置

-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。

-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间

-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

并发收集器设置

-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。

-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

垃圾收集器

 

minor GC 和 Full GC触发条件 以及不同

Minor GC触发条件:当Eden区满时,触发Minor GC。

Full GC触发条件:

(1)调用System.gc时,系统建议执行Full GC,但是不必然执行

(2)老年代空间不足

(3)方法去空间不足

(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存

(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

内存泄露+内存溢出

内存泄漏:指你的应用程序使用完资源后,没有及时释放,导致应用内存中持有了不需要的资源,这是一个状态的描述

内存泄漏是申请的空间没有及时释放或者干脆丢了指针没法释放.不是泄漏,是漏不出来;只是泄漏的内存远小于可分配的内存时影响不大,多了就玩完.

用完没有释放,造成可用内存越来越少

一般说内存泄漏是指分配了一块内存,用它存了一些东西,然后不再用它了,但并没有把这块内存释放掉。实际上只要程序退出了,不管泄漏不泄露,所有的内存一样会释放。

内存溢出:指你的应用内存已经不能满足正常使用了,堆栈已用内存已经达到系统设置的最大值,进而导致系统崩溃,是一个结果的描述

内存溢出就是申请的内存超过了可用内存,内存不够用了,比如申请了10m的内存,但是一共只有5m,申请不了,就溢出了. 导致覆盖了其他正常数据

java哪种情况会发生内存泄露

\1. 静态集合出现内存泄露

像hashmap、vector之内的静态变量集合生命周期和应用程序一样长,他们所引用的所有有对象都不会被释放

\2. hash运算的集合属性修改,remove不起作用

例如hashset、hashmap等集合根据哈希值存储元素,若存储的元素对象象属性修改,他的hash值改变,使用remove不能定位到它,也就无法删除,内存泄漏

\3. 各种连接

例如数据库连接、io连接,socket 数据库连接,需要显示close,否则造成连接对象无法释放,内存泄漏

\4. 单例模式持有一个对象引用

单例模式一般被认为是与程序的生命周期一样长,若持有另一个对象引用。会造成对象无法释放

栈溢出、堆溢出

首先分析:为什么会出现栈溢出:

\1. 就是请求的栈深度>jvm'所允许的栈深度。

\2. Jvm在扩展栈深度时无法获取到足够的内存。

下来就是:为什么会出现堆溢出:

因为:给对象分配内存时,没有足够的空间供使用了

 

如何确定内存泄露的位置 、内存泄漏如何解决

用jstat,jstack,jmap各种工具分析 (1.确定频繁full GC现象,找出进程唯一ID,用JPS 2.Jstat查看Full GC频次 3.jmap分析堆文件)

java OOM异常,如何排除和解决的?

OOM异常的四种类型:

  一:StackOverflowError :通常因为递归函数引起(死递归,递归太深)。-Xss 128k 一般够用。

 二:out Of memory: PermGen Space:通常是动态类大多,比如web 服务器自动更新部署时引起。-Xmx 256M,一般够用。JDK 8 没有PermGen Space,相对应是MetaSpace

三:OutOfMemoryError:unable to create native thread : 线程数太多(查看下线程数)或者给虚拟机内存过大( -Xmx 值小点)

四:out Of memory :heap space : 没有及时释放对象,主要查下各类集合引用的对象。这类问题最难查,可借且 jvisualvm 程序,仔细分析 OutOfMemoryError异常

查看是否有内存泄漏可以使用 内存影响分析工具对堆的存储快照进行分析.

java 内存模型(简称JMM)

JMM决定一个线程对共享变量的写入何时对另一个线程可见

线程之间的共享内存在主内存中,每个线程还有一个私有的本地内存,本地内存中存储了用于读写的共享变量的副本。

 

从上图看,A线程与B线程通信需要经过两个步骤

\1. 首先线程A把本地更新的共享变量刷新到主内存中

\2. 然后,线程B从主内存中更新已被线程A更新过的共享变量

 

JVM中的线程都有自己的线程栈,线程栈包含当前线程的方法调用信息,也称调用栈。一个线程只能读取自己的线程栈,也就是说线程中的本地变量对其他线程不可见

支撑Java内存模型的基础原理

1) 指令重排序

在执行程序时,编译器和cpu为了提高效率会对指令进行重排序,通过插入Memory Barrier禁止重排序,为上一层提供可见性保证

处理器指令重排、内存屏障,问处理器一般会怎么指令重排,是问重排序的三种:编译器优化的重排序、指令级并行的重排序和内存系统的重排序;

1、编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

2、指令级并行的重排序:如果不存l在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

3、内存系统的重排序:处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

2) 内存屏障(Memory Barrier )

内存屏障,是一条cpu指令:

a. 保证特定操作的顺序:

编译器和cpu能够指令重排,保证最终结果,而插入一条Memory Barrier就是告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排

b. 影响某些数据的内存可见性

强制刷出cpu缓存,因此任何cpu线程都能够读取这些数据最新版本。

volatile基于Memory Barrier实现的。

3) happens-before

从jdk5开始是基于happens-before的概念来阐述操作之间的内存可见性。

在JMM中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,这个的两个操作既可以在同一个线程,也可以在不同的两个线程中。

与程序员密切相关的happens-before规则如下:

1、程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。

2、监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作。

3、volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。

4、传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。

注意:两个操作之间具有happens-before关系,并不意味前一个操作必须要在后一个操作之前执行!仅仅要求前一个操作的执行结果,对于后一个操作是可见的,且前一个操作按顺序排在后一个操作之前。

 

java,exe是java虚拟机

javadoc.exe用来制作java文档

jdb.exe是java的调试器

javaprof,exe是剖析工具

初始化执行代码顺序

(1)在初次new一个Child类对象时,发现其有父类,则先加载Parent类,再加载Child类。

(2)加载Parent类:

初始化Parent类的static属性,赋默认值;

执行Parent类的static初始化块;

(3)加载Child类:

初始化Child类的static属性,赋默认值;

执行Child类的static初始化块;

(4)创建Parent类对象:

初始化Parent类的非static属性,赋默认值;

执行Parent类的instance初始化块;

执行Parent类的构造方法;

(5)创建Child类对象:

初始化Child类的非static属性,赋默认值;

执行Child类的instance初始化块;

执行Child类的构造方法;

后面再创建Child类对象时,就按照顺序执行(4)(5)两步。

创建对象之前会加载类,静态初始化块会随着类的加载而加载,对整个类进行初始化,而且子类会先执行顶层父类的静态初始化快后才是子类静态初始化快,创建对象时先执父类行初始化代码快然后执行,构造函数,然后执行子类的初始化代码快之后才是子类构造函数

jvm 的主要组成部分

1、JVM的组成:

JVM 由类加载器子系统、运行时数据区、执行引擎以及本地方法接口组成。

用new创建的对象在堆区

函数中的临时变量在栈去

java中的字符串在字符串常量区

2、JVM的运行原理:

JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种基于下层的操作系统和硬件平台并利用软件方法来实现的抽象的计算机,可以在上面执行java的字节码程序。java编译器只需面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译器,编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。

 

堆栈的区别?

栈(stack)。位于通用RAM中,但通过它的“堆栈指针”可以从处理器哪里获得支持。堆栈指针若向下移动,则分配新的内存;若向上移动,则释放那些 内存。这是一种快速有效的分配存储方法,仅次于寄存器。创建程序时候,JAVA编译器必须知道存储在堆栈内所有数据的确切大小和生命周期,因为它必须生成 相应的代码,以便上下移动堆栈指针。这一约束限制了程序的灵活性,所以虽然某些JAVA数据存储在堆栈中——特别是对象引用,但是JAVA对象不存储其 中。

堆(heap)。一种通用性的内存池(也存在于RAM中),用于存放所以的JAVA对象。堆不同于堆栈的好处是:编译器不需要知道要从堆里分配多少存储区 域,也不必知道存储的数据在堆里存活多长时间。因此,在堆里分配存储有很大的灵活性。当你需要创建一个对象的时候,只需要new写一行简单的代码,当执行 这行代码时,会自动在堆里进行存储分配。当然,为这种灵活性必须要付出相应的代码。用堆进行存储分配比用堆栈进行存储存储需要更多的时间。

Java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等 指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时 动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。 栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类 型的变量(,int, short, long, byte, float, double, boolean, char)和对象句柄。 栈有一个很重要的特殊性,就是存在栈中的数据可以共享(与两个对象的引用同时指向一个对象的这种共享是不同的)。

堆为什么分两个Survivor 区

S区即Survivor区,位于年轻代。年轻代分三个区。一个Eden 区,两个Survivor 区。

大部分对象在Eden 区中生成。当Eden 区满时,还存活的对象将被复制到Survivor 区(两个中的一个),当这个Survivor 区满时,此区的存活对象将被复制到另外一个Survivor 区,当这个Survivor 去也满了的时候,从第一个Survivor 区复制过来的并且此时还存活的对象,将被复制年老区(Tenured)。需要注意,Survivor 的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden 复制过来对象,和从前一个Survivor 复制过来的对象,而复制到年老区的只有从第一个Survivor 去过来的对象。而且,Survivor 区总有一个是空的。

JVM垃圾收集复制算法中,Eden:Form:To,为什么是8:1:1,而不是1:1:1?

一般情况下,新生代中的对象大多生命周期很短,也就是说当进行垃圾收集时,大部分对象都是垃圾,只有一小部分对象会存活下来,所以只要保 留一小部分内存保存存活下来的对象就行了。在新生代中一般将内存划分为三个部分:一个较大的Eden空间和两个较小的Survior空间(一样大小 ),每次使用Eden和一个Survior的内存,进行垃圾收集时将Eden和使用的Survior中的存活的对象复制到另一个Survior空间中,然后清除这两个 空间的内存,下次使用Eden和另一个Survior,HotSpot中默认将这三个空间的比例划分为8:1:1,这样被浪费掉的空间就只有总内存的1/10了。

OOM原因

导致OutOfMemoryError异常的常见原因有以下几种:

1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;

2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;

3. 代码中存在死循环或循环产生过多重复的对象实体;

4. 使用的第三方软件中的BUG;

5. 启动参数内存值设定的过小;

内存分配策略

1)对象优先在Eden分配;

2)大对象直接进入老年代;

3)长期存活的对象将进入老年代;

4)动态对象年龄判定

线上环境JVM参数Xms Xmx是如何设置的, 如果大小是一至,为什么这样设置?

参考答案:

Xmx设置JVM 的最大可用内存,Xms 设置JVM实际使用内存,一般Xmx和Xms相同,这是因为当Xmx内存空间不够用时,将进行扩容导致Full GC。将Xmx和Xms设置成相同的值,避免因Xms偏小导致频繁重新分配内存,影响应用使用。

如何定位一个CPU Load过高的Java线程

1、jps -v列出所有的java进程 , top找出cpu占用过高的对应的java 进程pid

2、使用top -H -p PID 命令查看对应进程里的哪个线程占用CPU过高,取该线程pid

3、将线程的pid 转成16进制

4、jstack [进程pid]|grep -A 100 [线程pid的16进制] dump出jvm该线程的后100行,或者整个输出到文件 jstack -l pid > xxxfile 参考文献:Crm线上机器发布时load过高案例分析阶段总结以及监控工具介绍

3.1 cpu load过高一般有以下原因

(1) 代码中有死循环

(2) 线程池误用,线程持续增加,线程切换消耗cpu资源

3.2 如何定位

(1) top定位负载过高的进程

(2) top -H 定位该进程中消耗cpu过高的线程

(3) 根据线程id,结合 jstack查看线程栈信息,进而定位到代码段

JmapDump的文件大小和MAT分析工具里显示的大小不一致一般是什么原因导致的?

JmapDump的文件一般比MAT工具大。创建索引期间,MAT会删除部分垃圾收集算法遗留下的不可达的object,因为回收这些较小的object代价 较大,一般这些object占比不超过4%。另外不能正确的写JmapDump的文件。尤其在较老的jvm(1.4,1.5)并且使用jmap命令获取JmapDump 的文件的时候。

1、JVM工具

jps——查看java进程pid;

jinfo——查看java进程启动参数;

jstack——查看java进程线程栈信息;

jstat——统计java进程的内存占用和回收情况;

jmap——导出JVM堆进行分析,强制产生Full GC;

2、其他工具

jmeter——用于压测模拟线上业务请求;

MAT——用于分析JVM堆是否有内存泄露;

GDB——用于分析内存指定区域存放的具体信息;

gperftools——谷歌的内存问题排查工具

如何在Jmap未响应的情况下Dump出内存?

加-F参数

JVM的一些健康指标和经验值,如何配置最优?

参考答案: 这个是比较开发性试题,偏社招,考察面试者对系统的掌控力,一般都会从垃圾回收的角度来解释,比如用jstat或者gc日志来看ygc的单次时间和 频繁程度,full gc的单次时间和频繁程度;ygc的经验时间100ms以下,3秒一次;full gc 1秒以下 1小时一次,每次回收的比率70%等等,也会用jstack和jmap看系统是否有太多的线程和不必要的内存等等。关于如何才能让配置最优,有一些理 论支撑,比如高吞吐和延迟低的垃圾收集器选择,比如高并发对象存活时间不长,可以适当加大yong区;但是有经验的面试者会调整一些参数测试 来印证自己的想法。

如何查看StringPool大小?

通过java -XX:+PrintStringTableStatistics命令查看,Number of buckets显示的就是StringPool的默认大小,在jdk7 u40版本以前,它的默认大小是1009,之后便调整为60013。

解题思路:JVM基础知识 考察点:StringPool 分类:JVM GC {校招,社招} 难度分级:P4,P5

JmapDump的文件中是否包括StringPool?

参考答案:

StringPool在jdk6中是在永久区,dump heap时,无法输出。在jdk7中,stringpool移到heap中,可以输出。

Java8的内存分代改进

Java7、Java8的堆内存有啥变化?

jdk的类加载机制和类似tomcat类web容器的类加载机制的不同

参考答案:http://blog.csdn.net/codolio/article/details/5027423

java的类加载器体系结构和双亲委托机制,应用场景举例

参考答案:

http://blog.csdn.net/lovingprince/article/details/4317069

http://www.cnblogs.com/whgw/archive/2011/09/29/2194997.html)

 

错误:

java.lang.OutOfMemoryError: PermGen space

查了一下为"永久代"内存大小不足,“永久代”的解释应该为JVM中的方法区,主要用于存储类信息,常量,静态变量,即时编译器编译后代码等。本错误仅限于Hotspot虚拟机,本区进行垃圾回收很少,不够直接加大简单粗暴。

java.lang.OutOfMemoryError: Requested array size exceeds VM limit

直接翻译报错信息:数组过长导致堆内存溢出,加大堆内存或者减少数组长度。

java.lang.OutOfMemoryError: Java heap space

堆内存不足,直接增大堆内存。

加载:

在继承中代码的执行顺序为:1.父类静态对象,父类静态代码块

2.子类静态对象,子类静态代码块

3.父类非静态对象,父类非静态代码块

4.父类构造函数

5.子类非静态对象,子类非静态代码块

6.子类构造函数

  • 考虑 Java 中构造器、初始化块、静态初始化块的执行顺序。

  • 静态初始化块 > 初始化块 > 构造器

  • 父类 > 子类

综合下来顺序就是:

  • 父类静态初始化块

  • 子类静态初始化块

  • 父类初始化块

  • 父类构造器

  • 子类初始化块

  • 子类构造器

需要注意静态初始化块是在类第一次加载的时候就会进行初始化。

类的加载顺序

(1) 父类静态对象和静态代码块

(2) 子类静态对象和静态代码块

(3) 父类非静态对象和非静态代码块

(4) 父类构造函数

(5) 子类 非静态对象和非静态代码块

(6) 子类构造函数

其中:类中静态块按照声明顺序执行,并且(1)和(2)不需要调用new类实例的时候就执行了(意思就是在类加载到方法区的时候执行的)

类的加载顺序

(1) 父类静态代码块(包括静态初始化块,静态属性,但不包括静态方法)

(2) 子类静态代码块(包括静态初始化块,静态属性,但不包括静态方法 )

(3) 父类非静态代码块( 包括非静态初始化块,非静态属性 )

(4) 父类构造函数

(5) 子类非静态代码块 ( 包括非静态初始化块,非静态属性 )

(6) 子类构造函数

先父类静态―>子类静态―>父类非静态―>父类构造函数―>子类非静态―>子类构造函数

FullGC 是老年代内存空间不足的时候,才会触发的,老年代一般是生命周期较长的对象或者大对象,频繁的 FullGC 不会可能会影响程序性能(因为内存回收需要消耗CPU等资源),但是并不会直接导致内存泄漏。

JVM奔溃的可能是内存溢出引起的,也可能是其他导致 JVM崩溃的操作,例如设置了错误的JVM参数等。

内存异常,最常见的 就是 StackOverFlow 了把,内存溢出,其实内存泄漏的最终结果就是内存溢出。所以,基本上C是对的答案。

年老代溢出原因有 循环上万次的字符串处理、创建上千万个对象、在一段代码内申请上百M甚至上G的内存,既A B D选项

持久代溢出原因 动态加载了大量Java类而导致溢出

196. 说一下堆栈的区别?

  • 功能方面:堆是用来存放对象的,栈是用来执行程序的。

  • 共享性:堆是线程共享的,栈是线程私有的。

  • 空间大小:堆大小远远大于栈。

207. 说一下 JVM 调优的工具?

JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。

    • jconsole:用于对 JVM 中的内存、线程和类等进行监控;

    • jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

208. 常用的 JVM 调优的参数都有哪些?

  • -Xms2g:初始化推大小为 2g;

  • -Xmx2g:堆最大内存为 2g;

  • -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;

  • -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;

  • –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;

  • -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;

  • -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;

  • -XX:+PrintGC:开启打印 gc 信息;

  • -XX:+PrintGCDetails:打印 gc 详细信息。

 

 

内存泄漏如何解决 用jstat,jstack,jmap各种工具分析

(1.确定频繁full GC现象,找出进程唯一ID,用JPS 2.Jstat查看Full GC频次 3.jmap分析堆文件)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值