前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM探范的字节码文件。javac编译器在将Java源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法解析、语法解析、语义解析以及生成字节码。
2.1.1 javac(IDEA默认使用的)
javac是一种能够将Java源码编译为字节码的前端编译器
2.1.2 ECJ编译器
在Java的前端编译器领域,除了javac之外,还有一种被大家经常用到的前端编译器,那就是内置在Eclipse中的EC(Eclipse Compiler for Java)编译器。和Javac的全量式编译不同,ECJ是一种增量式编译器。
(1) class:
外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
( 2) interface:接口
(3)[]:数组
(4) enum:枚举
(5) annotation:注解@interface
(6) primitive type:基本数据类型
( 7) void
i++的过程
bipush 10 将10放入操作数栈
istore_1 将栈中值放入局部变量表下标为1的位置
iload_1 将局部变量表中的值放入栈中
iinc 1 by 1 将局部变量表中的值加1
istore_1 将栈中的值放入局部变量表中
static void test2(){
Integer a = 128;
Integer b = 128;
log.info("输出的结果为{}",a == b);//false -128~127都会存入缓存中
}
public void test3(){
Integer a = 4;
int b = 4;
log.info("是否相等{}",a == b );//true Integer会自动进行拆箱(能拆不装)
}
进制在线转换
https://www.sojson.com/hexconvert.html
魔数
Class文件版本
常量池
访问标识(或标志)
类索引,父类索引,接口索引集合
字段表集合
方法表集合
属性表集合
常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一。
常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
字面量:1.文本字符串 2.声明为final的常量值
符号引用: 1.类和接口的全限定名 2.字段的名称和描述符 3.方法的名称和描述符
常量类型和结构
常量池中每一项常量都是一个表,JDK1.7之后共有14种不同的表结构数据。如下表格所示:
https://blog.csdn.net/qq_33521184/article/details/105622903?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163939550716780274184839%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=163939550716780274184839&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-105622903.first_rank_v2_pc_rank_v29&utm_term=java%E5%AD%97%E8%8A%82%E7%A0%81%E6%8C%87%E4%BB%A4&spm=1018.2226.3001.4187
3.1.1 Loading(装载)阶段
将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象。
步骤:
1.通过类的全名,获取类的二进制数据流。
2.解析类的二进制数据流为方法区内的数据结构(Java类模型)
3.创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口
类模型的位置
加载的类在JVM中创建相应的类结构,类结构会存储在方法区(JDK1.8之前:永久代;JDK1.8及之后元空间)。
Class实例的位置
类将.class文件加载至元空间后,会在堆中创建一个Java.lang.class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个class类型的对象。
3.1.2 Linking(连接)阶段
①验证
保证加载的字节码是合法、合理并符合规范的
为类的静态变量分配内存,并将其初始化为默认值。
需要注意的点:
1.这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配了,准备阶段会显式赋值。
2.注意这里不会为实例变量分配初始化,实例变量是会随着对象一起分配到Java堆中。
3.在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。
③解析
将符号引用转为直接引用,也就是得到类、字段、方法在内存中的指针或"偏移量。如果直接引用存在,那么可以肯定系统中存在该类、方法或者段。但只存在夺号引用,不能确定系统中一定存在该结构。
为类的静态变量赋予正确的初始值,以及加载静态代码块。执行类的初始化方法:()方法
():只有在给类的中的static的变量显式赋值或在静态代码块中赋值了。才会生成此方法。
():一定会出现在Class的method表中。
不会有()的类
public class Test {
//场景1:对于非静态的字段,不管是否进行了显式赋值,都不会生成()方法
public int num = 1;
//场景2:静态的字段,没有显式的赋值,不会生成()方法
public static int num1;
//场景3:比如对于声明为static final的基本数据类型的字段,不管是否进行了显式赋值,都不会生成()方法
public static final int num2 = 1;
}
()产生死锁的问题
class StaticA {
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("com.atguigu.java.StaticB");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticA init OK");
}
}
class StaticB {
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("com.atguigu.java.StaticA");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticB init OK");
}
}
public class StaticDeadLockMain extends Thread {
private char flag;
public StaticDeadLockMain(char flag) {
this.flag = flag;
this.setName("Thread" + flag);
}
@Override
public void run() {
try {
Class.forName("com.atguigu.java.Static" + flag);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(getName() + " over");
}
public static void main(String[] args) throws InterruptedException {
StaticDeadLockMain loadA = new StaticDeadLockMain('A');
loadA.start();
StaticDeadLockMain loadB = new StaticDeadLockMain('B');
loadB.start();
}
}
类的加载器只在loading阶段,只能影响到类加载的第一个阶段
显式加载:指的是在代码中通过调用ClassLoader加载class对象,如直接使用class.forName(name)或this.getClass().getClassLoader( ).loadClass()加载class对象。
隐式加载:则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。
1.这个类加载使用C/C++语言实现的,嵌套在VM内部。
2.它用来加载]ava的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)。用于提供VM自身需要的类。
3.并不继承自java.lang.ClassLoader,没有父加载器。
4.出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
5.加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
使用以下参数配置会打印被加载的类()
-XX:+TraceClassLoading
1.Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。继承于classLoader类
2.父类加载器为启动类加载器
3.从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
3.2.3系统类加载器
1.java语言编写,由sun.misc.Launcher$AppClassLoader实现继承于ClassLoader类
2.父类加载器为扩展类加载器
3.它负责加载环境变量classpath或系统属性 java.class.path指定路径下的类库应用程序中的类加载器默认是系统类加载器。
4.它是用户自定义类加载器的默认父加载器
5.通过classLoader的getSystemClassLoader()方法可以获取到该类加载器
3.2.4自定义类加载器
1.通过类加载器可以实现非常绝妙的插件机制,如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现。
2.自定义加载器能够实现应用隔离,例如Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器
3.所有用户自定义类加载器通常需要继承于抽象类java.lang.classLoader。
ClassLoader
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//查看当前类是否有被加载过 First, check if the class has already been loaded
Class> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {//给引导类加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class. 如果没有类加载器加载,就自己加载当前类
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {//是否需要解析,默认为false
resolveClass(c);
}
return c;
}
}
保护机制,即使我们重写了classLoader破坏双亲委派机制,引导类加载器也会加载java下的类
/* Determine protection domain, and check that:
- not define java.* class,
- signer of this class matches signers for the rest of the classes in
package.
*/
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
1.隔离加载类
2.修改类加载的方式
3.扩展加载源
4.防止源码泄漏
为了保证程序(在操作系统中理解为进程)能够连续地执行下去,CPU必须具有某些手段来确定下一条指令的地址。而程序计数器正是起到这种作用,所以通常又称为指令计数器。
在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
作用
1.在程序开始执行前,必须将它的起始地址,即程序的一条指令所在的内存单元地址送入PC,因此程序计数器(PC)的内容即是从内存提取的第一条指令的地址。当执行指令时,CPU将自动修改PC的内容,即每执行一条指令PC增加一个量,这个量等于指令所含的字节数,以便使其保持的总是将要执行的下一条指令的地址。
2.由于大多数指令都是按顺序来执行的,所以修改的过程通常只是简单的对PC加1。
3.当程序转移时,转移指令执行的最终结果就是要改变PC的值,此PC值就是转去的地址,以此实现转移。有些机器中也称PC为指令指针IP( Instruction Pointer)。
PC寄存器
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。执行引擎的字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
PC寄存器为什么会被设定为线程私有?
为了能够准确地记录各个线程正在执行的当前字节码指令地址,每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
为什么执行native方法是,是undefined
native 本地方法是大多是通过C实现,并未编译成需要执行的字节码指令,所以在计数器中当然是空(undefined)。
它是一种运算受限的线性表,是线程私有的,主管程序运行,生命周期和线程同步,线程结束,栈内存就释放了。不存在垃圾回收问题。默认1024k
在这个线程上正在执行的每个方法都各自对应一个栈帧〈Stack Frame) .枝帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
局部变量表也被称之为局部变量数组或本地变量表,局部变量表所需的容量大小是在编译期确定下来的
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型(8种)、对象引用(reference),以及returnAddress类型。
注意:非静态方法默认会有this存储在局部变量表中下标0的位置,double和long会占用两个slot(槽位)一个slot占用四个字节
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
在解释执行过程中,每当为Java方法分配栈桢时,Java虚拟机往往需要开辟一块额外的空间作为操作数栈,来存放计算的操作数以及返回结果。
具体来说便是:执行每一条指令之前,Java 虚拟机要求该指令的操作数已被压入操作数栈中。在执行指令时,Java 虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中。
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)
作用:将符号引用转换为调用方法的直接引用
只要在本万法的异常表中没有搜索到匹配的异常处理器,就会导致万法退出。简称异常完成出口。
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。
当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。
1.本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。·
2.甚至可以直接使用本地处理器中的寄存器
3.直接从本地内存的堆中分配任意数量的内存。
一个VM实例只存在一个堆内存,堆也是Java内存管理的核心区域。
Java 堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。
堆内存的大小是可以调节的。
堆,是GC ( Garbage collection,垃圾收集器)执行垃圾回收的重点区域。在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。
1.优先分配到Eden
2.大对象直接分配到老年代
· 尽量避免程序中出现过多的大对象
3长期存活的对象分配到老年代
4.动态对象年龄判断
如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年
龄大于或等于该年龄的对象可以直接进入老年代,无须等MaxTenuringThreshold中要求的年龄。
5.空间分配担保
-XX: HandlePromotionFailure
空间分配担保(jdk6之后默认开启)
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,
如果大于,则此次Ninor Gc是安全的
如果小于,则虚拟机会查看-XX:HandlePromotionFailure设置值是否允许担保失败
如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Ninor Gc,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。
TLAB
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。(默认栈Eden空间的1%)
一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
HotSpot虚拟机:jdk1.7之前方法区称之为永久代,jdk8开始,使用元空间(方法区的具体实现)取代了永久代
元空间与永久代最大的区别在于:元空间不在虚打机设置的内存中,而是使用本地内存。|
方法区溢出测试
public class OOMTest extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
OOMTest test = new OOMTest();
for (int i = 0; i < 10000; i++) {
//创建ClassWriter对象,用于生成类的二进制字节码
ClassWriter classWriter = new ClassWriter(0);
//指明版本号,修饰符,类名,包名,父类,接口
classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
//返回byte[]
byte[] code = classWriter.toByteArray();
//类的加载
test.defineClass("Class" + i, code, 0, code.length);//Class对象
j++;
}
} finally {
System.out.println(j);
}
}
}
虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。(即判断类元信息是否存在)。如果没有,那么在双亲委派模式下,使用当前类加载器以classLoader+包名+类名为Key进行查找对应的.class 文件。如果没有找到文件,则抛出classNotFoundException异常。·如果找到,则进行类加载,并生成对应的Class类对象。
首先计算对象占用空间大小,接着在堆中划分一块内存给新对象。
如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小。
指针碰撞(内存空间连续)
是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离。如果垃圾收集器选择的是Serial old、Par old这种基于压缩算法的,虚拟机采用这种分配方式
空闲列表(内存空间零散)
虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式称为“空闲列表(Free List)
在分配内存空间时,另外一个问题是及时保证new对象时候的线程安全性:创建对象是非常频繁的操作,虚拟机需要解决并发问题。虚拟机采用了两种方式解决并发问题:
内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保正了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)
放置原则:
占位符的作用
1.直接访问
2.句柄访问
JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被VM所识别的字节码指令、符号表,以及其他辅助信息。
那么,如果想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。
Java 语言的“编译期”其实是一段“不确定”的操作过程,因为它可能是指一个前端编译器(其实叫“编译器的前端”更准确一些)把.java文件转变成.class文件的过程; Sun 的javac Eclipse的JDT
也可能是指虚拟机的后端运行期编译器(JIT编译器,Just In TimeCompiler)把字节码转变成机器码的过程。HotSpot VM的C1 C2编译器
还可能是指使用静态提前编译器(AOT 编译器,Ahead Of TimeCompiler)直接把.java文件编译成本地机器代码的过程。缺点:破坏一次编译到处运行,降低了java链接过程的动态性
在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,但大多数情况下我们简称为C1编译器和C2编译器。开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器
在不同的编译器上有不同的优化策略,c1编译器上主要有方法内联,去虚拟化、冗余消除
C2的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在c2上有如下几种优化:
总结:
java是半编译半解释型,根据什么选择JIT编译器
一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为OSR(On StackReplacement)编译。
一个方法究竞要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准?必然需要一个明确的阈值,JIT编译器才会将这些“热点代码”编译为本地机器指令执行。这里主要依靠热点探测功能。
目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。
采用基于计数器的热点探测,HotSpot VM将会为每一个方法都建立2个不同类型的计数器,分别为
方法调用计数器(Invocation Counter)用于统计方法的调用次数
回边计数器(Back EdgeCounter)用于统计循环体执行的循环次数
这个计数器就用于统计方法被调用的次数,它的默认阈值在client模式下是 1500次,在 server模式下是 10000次。超过这个阈便,就会触发JIT编译。
当一个方法被调用时,会先检查该方法是否存在被JIT 编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值。如果已超过阙值,那么将会向即时编译器提交一个该方法的代码编译请求。
热度衰减
如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减(counter Decay),而这段时间就称为此方法统计的半衰周期(counter Half Life Time) 。
当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字书码文件中的内容“翻译”为对应平台的本地机器指令执行。
运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾
可达性分析算法中会出现内存泄漏吗
如果一个对象是垃圾但是没有被当做垃圾回收就是内存泄漏
在可达性算法中一个对象是垃圾但是被跟对象引用,导致不能被回收,就叫做内存泄漏
技巧:由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root 。
注意点:
使用可达性算法,只能某一个快照下去对对象去做判定,这点也是导致Gc进行时必须“stop The world”的一个重要原因
成员变量,也叫实例变量,不同于类变量(静态变量),前面讲到类变量是存储在方法区中,而成员变量是存储在堆内存的对象中的,和对象共存亡,所以是不能作为GC Roots的
核心思想:
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点:
缺点:
收集过程:
第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
优点:
缺点:
如果一个对象是垃圾但是没有被当做垃圾回收就是内存泄漏
内存泄漏的8种情况
HashSet<GCHash> objects = new HashSet();
GCHash a = new GCHash("张三", "18");
GCHash b = new GCHash("李四", "20");
objects.add(a);
objects.add(b);
a.setName("王五");
objects.remove(a);
log.info("集合的对象有{}",objects);
安全点:
程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始Gc,这些位置称为“安全点(Safepoint) ”
Safe Point的选择很重要,如果太少可能导致Gc等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择一些执行时间较长的指令作为Safe Point,如方法调用、循环跳转和异常跳转等。
如何在cc发生时,检查所有线程都跑到最近的安全点停顿下来呢?
安全区域(Safepoint)
Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint 。但是,程序“不执行”的时候呢?例如线程处于Sleep状态或
Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region)来解决。
运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)
现在JVM调优标准:在最大吞吐量优先的情祝下,降低停顿时间。
特点:
新生代与ParNew(默认)和Serial收集器搭配
初始标记(STW):暂时时间非常短,标记与GC Roots直接关联的对象。
并发标记(最耗时):从GC Roots开始遍历整个对象图的过程。不会停顿用户线程
重新标记:(STW):修复并发标记环节,因为用户线程的执行,导致数据的不一致性问题(关注垃圾从不可达到可达,不关注可达到不可达–浮动垃圾)
并发清理(最耗时)将垃圾清除
优点:
缺点:
并行与并发
分代收集
//设置老年代占用比例默认是 新生代:老年代1:2
-XX:NewRation=2
//设置新生代占用最大内存
-Xmn
//设置Eden区占用比例 默认 Eden区:From Survivor 区:To Survivor 区 8:1:1
-XX:SurvivorRatio
//true为使用 false为不使用 空间分配担保策略
-XX:HandlePromotionFailure
//设置新生代去老年代的次数默认是15
-XX:MaxTenuringThreshold=
’
//打印GC详细信息
-XX : +PrintGCDetails
//TLAB设置是否开启默认为true
-XX:+UseTLAB
//设置tlab空间大小
-XX:TLABwasteTargetPercent
//设置永久代初始大小
-XX:Metaspacesize=10m
//设置永久代最大值
-XX:MaxMetaspaceSize=10m
//控制开启编译器的server还是client模式
-XX:+RewriteFrequentPairs
//设置JIT调用阈值,超过当前阈值就会触发JIT编译
-XX: CompileThreshold
//是否关闭热度衰减
-XX:-UseCounterDecay
//热度衰减时长,单位为秒
-XX:CounterHalfLifeTime
------parallel gc相关参数设置
//指定垃圾回收器
-XX:+UseParallelGC
//指定老年代垃圾回收器
-XX:+UseParallelOldGC
//设置年轻代并行收集线程数
-XX:ParallerGCThreads
在默认情况下,当CPU 数量小于8个,ParallelGCThreads的值等于CPU 数量。
当CPU数量大于8个,Paralle1GCThreads的值等于3+[5* CPu_Count]/8]。
//设置最大停顿时间
-XX:MaxGCPauseMillis
//设置垃圾总占比时间,用户衡量吞吐量的大小,取值范围(0,99)
-XX:GCTiimeRatio
//设置自适应调节策略 默认开启
-XX:UseAdaptiveSizePolicy
-------G1垃圾回收器参数设置
//设置垃圾回收器
-XX:+UseG1GC
//设置Region大小默认是堆内存的1/2000,范围是1MB-32MB
-XX:G1HeapRegionSize
//设置期望最大GC停顿时间,默认是200ms
-XX:MaxGCPauseMillis
//设置STW时GC线程数,最多设置为8
-XX:ParallelGCThreads
//设置并发标记的线程数
-XX:ConcGCThread
//设置触发并发GC周期的java堆占用率阈值,超过就触发GC,默认是45
-XX:InitiatingHeapOccupancyyPercent
-----------------;