Java高级特性与JVM

一、java基础知识(必须会的基础)

1. 语言基础

1)关键字:static, volatile, final , transient
2)高阶语法:内部类(高频考点)、泛型。

1.1 static:属于类而非实例

static变量不会被GC回收,也就意味着有内存泄露的风险。
附:融云的static内存缓存,如果不加控制,可能会由于内存缓存太多而导致频繁的full GC。

static会将所引用的属性、方法、内部类,与类直接产生引用关系,而不是与类的实例。

为什么一个没有被static修饰的内部类,必须要这么声明:

OuterClass.InnerClass innerClass = new OuterClass().new InnerClass();

这是因为,没有用static修饰内部类InnerClass,所以必须new一个外部类OuterClass的实例,才能在此基础上new出新的内部类的实例。如果使用了static修饰内部类,那么就不用new OuterClass()了,而仅需要这样:

OuterClass.InnerClass innerClass = new OuterClass.InnerClass();

static表示“全局”或者“静态”,用于修饰成员变量或方法,也可形成static代码块。

static修饰的成员变量方法,为类的所有实例所共享。只要这个类被加载,JVM就可以根据类名,在运行时数据区的Method Area内存区域内找到他们。(注意:方法区存的时成员变量或方法,而不是对象)

1.1.1 static成员变量

static变量在内存中只有一份拷贝,即JVM只为静态变量分配一次内存,在加载类的过程中完成内存分配,可直接用类名访问。而普通成员变量会根据实例数目在内存中有多个拷贝。

1.1.2 static方法

为了方便调用,通常把工具方法声明为static。
static方法必须有方法体,不能是abstract方法
static方法有下面几个限制:
1)仅能调用static方法,不能调用普通方法
2)仅能访问static数据,不能访问普通数据
3)不能访问this或super

1.1.3 static代码块

在类加载时,如果有多个代码块,static代码块会按顺序加载,且static代码块只执行一次。

1.1.4 static和final一起修饰

通常,可将缓存对象设置为static final的

1.2 volatile:可见性(主存和工作内存)、不能保证原子性(如i++)

用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最新值。即用volatile修饰的变量,一旦写入完成,任何访问这个字段的线程都会得到最新的值。

volatile无法实现原子性,也就是不能用它来做计数器。

1.2.1 常考:为什么volatile用作计数器会有问题呢?

每一个线程都有一个线程栈,线程栈中保存了线程运行时的变量值。当线程访问一个对象的成员变量的时候:
Step1(read and load, 从主存复制到当前工作内存). 通过对象的引用找到对应在堆内存中的变量的值,然后copy到线程栈的内存中。
Step2(use and assign, 执行代码,改变共享变量值). 在线程栈中修改该变量
Step3. (store and write, ) 在修改完之后,线程退出前,自动把线程栈中的变量副本回写到对象的堆内存中。这样,堆中的对象就变化了。

**对于volatile修饰的变量,JVM只保证Step1即从主存储加载到线程工作内存的值是最新的。**例如,某个时刻thread1, thread2, 获取主内存中的count值都是5,那么,在他们经过step6之后,回写时,值都是6,所以计数器会不准确。

通常用volatile + CAS(compare and swap)来做计数器。

1.2.2 为什么使用volatile?
  1. volatile使用简单,且读操作开销低(几乎和非volatile一样,但volatile的写操作开销很高,但比锁的开销低)
    2)volatile不会像锁一样造成阻塞。如果读操作次数远超过写操作,那么与锁相比,volatile变量会比锁减少很多同步的开销性能。
1.2.3 volatile的应用场景
1.2.3.1 场景1. 将volatile作为状态标志使用。
volatile boolean shutdownRequested;
... 
public void shutdown() { shutdownRequested = true; }
 
public void doWork() {
    while (!shutdownRequested) {
        // do stuff
    }
}

当shutdown与doWork在不同的线程中时,通过使用volatile关键字,可以保证不同线程之间对shutdownRequested的可见性。

1.2.3.2 场景2. 单例

单例的双重校验锁。

public class Singleton {
    private static volatile Singleton singleton;
    
    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
1.2.3.3 场景3. AtomicInteger: 基于volatile实现的支持原子性的计数器
public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

这样,即实现了线程安全的计数器。

测试线程安全的计数器的源码:

public class AtomicIntegerTest {
      
       private volatile AtomicInteger counter = new AtomicInteger(0);
 
       public static void main(String[] args) {
              AtomicIntegerTest t = new AtomicIntegerTest();
              t.test();
       }
      
       public void test() {
              for (int i = 0; i < 100000; i++) {
                     new Thread(new Runnable() {
                            @Override
                            public void run() {
                                   counter.incrementAndGet();
                            }
                     }).start();
              }
             
              System.out.println("counter:" + counter);
       }
}

输出结果为:counter:100000

总结:
volatile修饰的成员变量:
1) 在每次被线程访问时,都强迫从主内存中重读该成员变量的值;
2) 当成员变量发生变化时,强迫线程将变化的值回写到主内存。
这样,在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。

1.3 final

final可用于修饰成员变量、局部变量、方法、类。
一旦将引用声明为final, 你将不能改变这个引用的指向了。

1.3.1 final变量

变量不可改变。对于final和static一起修饰的变量叫做常量。final变量是只读的。

备注:成员常量会被放到方法区中的常量池。但即使只用static修饰的成员变量,同样也会放到方法区。

例子:

public static final String NAME = “John”;
NAME = new String(“Jack”); // 此时会编译报错。

final变量存在Stack内存区,但相比普通变量,final变量会被JVM优化。

1.2.3.2 final方法:不能被Override的方法

final方法比普通方法更快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。

一个final方法被调用时会转为内嵌调用,不会使用常规的压栈方式,使得运行效率较高。

类中的所有private方法都隐式地指定为是final,所以在继承关系中不存在覆盖问题。

1.2.3.3 final类:不能被继承,对象是只读的,多线程下线程安全。

用final修饰的类叫不可变类。

String是典型的例子,不可变类的好处是,其对象是只读的,这样在多线程环境下线程安全,不必额外的同步开销。(但前提是final类的成员变量也是final的)。

final类的好处是速度快,因为final类中的方法也都是隐式final的,即不可被override, 这样可在编译期间加载,而不必在运行期间动态加载

final类的好处:
1.不涉及继承和覆盖。
2.其地址引用和装载在编译时完成。
3.在运行时不要求JVM执行因覆盖而产生的动态地址引用而花费时间和空间。
4.与继承链上的一般对象相比,垃圾回收器在收回final对象所占据的地址空间时也相对简单快捷。

1.2.3.4 使用final关键字的好处:

1.final变量提高了性能。JVM和Java应用都会缓存final变量 — 这里缓存的仅是final的成员变量,局部变量存到stack中,不会缓存,但JVM会对其进行优化。
2.final成员变量在多线程场景下线程安全,而不需要加锁等做同步的开销。
3.对于final修饰的方法、类,JVM会对其进行自动优化。
4. 对于final的局部变量,存在Stack; 对于final成员变量或者static final成员变量,存Method Area中的常量池。

1.2.3.5 关于final的知识点:

1.final变量必须在声明的时候初始化,或者在构造器中初始化,否则会报编译错误。final局部变量必须在声明时赋值。
2.匿名类中的所有变量必须是final变量。(如new Thread(new Runnable(){…}).start();中的…中的变量)
3.接口中的所有变量自动都是final的。
4.final方法在编译阶段绑定,称为静态绑定(static binding)
5.将类、方法、变量声明为final, 都能提升性能。因为JVM就有机会进行评估然后优化。
6.final变量通常要求全大写

注意一点,final的集合对象只是引用不可更改,但可以向其中添加、删除、修改其元素。例如:

private final List list = new ArrayList();
list.add(“aaa”);

final只对引用的“值”(也即它所指向的那个对象的内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。这很类似==操作符:==操作符只负责引用的“值”相等,至于这个地址所指向的对象内容是否相等,==操作符是不管的。

为什么匿名内部类中的变量必须要求是final的?
如果定义一个匿名内部类,并且希望它使用一个在其外部定的对象,那么编译器会要求其参数引用是final 的。经研究,Java虚拟机的实现方式是,编译器会探测局部内部类中是否有直接使用外部定义变量的情况,如果有访问就会定义一个同类型的变量,然后在构造方法中用外部变量给自己定义的变量赋值。

1.4 transient:序列化时屏蔽掉变量

transient只能用于修饰变量,不能修饰类或方法.

当对象被序列化(写入字节序列到目标文件)时,transient修饰的实例变量不会被持久化;当对象被反序列化时(从源文件读取字节序列进行数据恢复),这样的实例变量也不会被持久化和恢复。也就是说,一个类中有一个transient的成员变量,这个类在经过序列化再反序列化之后,该transient变量是无法被恢复的。

transient的应用场景为,在对对象序列化时将不能被持久化存储的变量声明为transient,比如密码,这样在对象序列化再反序列化之后,transient的成员变量password的值将会消失。

2. Java高阶语法

2.1 内部类:在类的内部定义一个类

为什么要使用内部类?
《Thinking in Java》中说:使用内部类最吸引人的原因是:每个内部类都能独立地继承一个实现,所以无论外部类是否已经继承了某个实现,对内部类都没有影响。
也就是说,使用内部类可实现多重继承。

使用内部类还能有如下好处:
1.内部类可以有多个实例,每个实例都有自己的状态信息,并且与其他外部对象的信息相互独立。
2.在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。
3.创建内部对象的时刻,并不依赖于外部类对象的创建。
4.内部类并没有令人迷惑的“is-a”关系,它就是一个独立体。
5.内部类提供了很好的封装,除了该外部类,其他类都不能访问该内部类。

内部类可以直接访问其外部类:

public class OuterClass {
    private String name ;
    private int age;
 
    /**省略getter和setter方法**/
 
    public class InnerClass{
        public InnerClass(){
            name = "chenssy";
            age = 23;
        }
 
        public void display(){
            System.out.println("name:" + getName() +";age:" + getAge());
        }
    }
 
    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();
        innerClass.display();
    }
}

输出结果:

name:chenssy;age:23

可以看到,尽管name和age是外部类的private变量,但在创建内部类对象时,该对象必定会捕获一个指向外部类对象的引用。

内部类是编译时的概念,一旦编译成功后,内部类与他的外部类就属于两个完全不同的类。对于OuterClass外部类和其内部类InnerClass,在编译后,会出现下面两个class文件:OuterClass.class和OuterClass$InnerClass.class

2.1.1 Java的内部类分类

Java内部类分为:成员内部类、局部内部类、匿名内部类、静态内部类。

2.1.1.1 成员内部类:可无限制访问其外部类

成员内部类是最普通的内部类,可无限制的访问其外部类的所有成员属性和方法,哪怕是private的。
但其外部类要访问内部类的成员属性和方法,就必须通过内部类的实例来访问。

成员内部类中不能存在任何static变量和方法, 但可以存在static final的常量。

2.1.1.2 局部内部类:嵌套在方法中或作用域中的内部类

1.局部内部类主要用于当解决一个复杂问题是,需要创建一个类类辅助,但又不希望别人可以访问这个类
2.局部内部类和成员内部类一样被编译,只是他们的作用域不同,局部内部类只能在该方法或作用域中被使用。

方法内部类举例

public class Parcel5 {
       public Destionation destionation(String str) {
              class PDestionation implements Destionation {
                     private String label;
 
                     private PDestionation(String whereTo) {
                            label = whereTo;
                     }
 
                     public String readLabel() {
                            return label;
                     }
              }
              return new PDestionation(str);
       }
 
       public static void main(String[] args) {
              Parcel5 parcel5 = new Parcel5();
              Destionation d = parcel5.destionation("chenssy");
              System.out.println("label:" + d.readLabel());
       }
}
 
interface Destionation {
       public String readLabel();
}

输出结果:

label:chenssy

作用域内的内部类:

public class Parcel6 {
       private void internalTracking(boolean b){
        if(b){
            class TrackingSlip{
                private String id;
                TrackingSlip(String s) {
                    id = s;
                }
                String getSlip(){
                    return id;
                }
            }
            TrackingSlip ts = new TrackingSlip("chenssy");
            System.out.println(ts.getSlip());
        }
    }
 
    public void track(){
        internalTracking(true);
    }
 
    public static void main(String[] args) {
        Parcel6 parcel6 = new Parcel6();
        parcel6.track();
    }
}

输出结果:

chenssy
2.1.1.3 匿名内部类

匿名内部类没有名字。以常用的新建线程为例:
new Thread(new Runnable() {
@Override
public void run() {

}
}).start();

匿名内部类不过是直接使用new来生成一个对象的引用,当然这个引用是隐式的。
匿名内部类不能是抽象类,所以他必须实现他的抽象父类或接口中的所有抽象方法。
当然可以把上面的代码做这样的拆分:

class MyThread implements Runnable {
       @Override
       public void run() {}
}

MyThread myThread = new MyThread();
new Thread(myThread);

二者相比,匿名内部类存在一个缺陷,就是其实例myThread无法重复使用。

匿名内部类是局部内部类的一种。

在给匿名内部类传递参数的时候,若该形参在内部类中需要被使用,那么该形参必须是final的。

public class OuterClass {
    public void display(final String name,String age){
        class InnerClass{
            void display(){
                System.out.println(name);
            }
        }
    }
}

这是为了怕在外部类中改变name, 但这个变化却不会传递到内部类中。
简单理解就是,拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。

由于匿名内部类没有类名,也就没有构造器,那么匿名内部类怎样做初始化呢?答案是通过代码块。

public InnerClass getInnerClass(final int age,final String name){
    return new InnerClass() {
        int age_ ;
        String name_;
        //构造代码块完成初始化工作
        {
            if(0 < age && age < 200){
                age_ = age;
                name_ = name;
            }
        }
        public String getName() {
            return name_;
        }

        public int getAge() {
            return age_;
        }
    };
}
2.1.1.4 静态内部类

使用static修饰的成员内部类叫静态内部类,也叫作嵌套内部类。
静态内部类与成员内部类的区别是,成员内部类在编译完成后会隐含地保存其外部类的引用,但静态内部类确没有,这就意味着静态内部类仅能访问其外部类的static成员变量和方法。

2.2 泛型

为什么要引入泛型?
目的是为了避免一个集合可同时存放多种数据类型,使用时再强制转换。

2.2.1 泛型类:具有一个或多个类型参数的类
public class Pair<T, U> {
    private T first;
    private U second;
 
    public Pair(T first, U second) {
        this.first = first;
        this.second = second;
    }
 
    public T getFirst() {
        return first;
    }
 
    public U getSecond() {
        return second;
    }
 
    public void setFirst(T newValue) {
        first = newValue;
    }
 
    public void setSecond(U newValue) {
        second = newValue;
    }
}

实例化泛型类的时候,我们只需要把类型参数换成具体的类型即可,比如实例化一个Pair<T, U>类我们可以这样:

Pair<String, Integer> pair = new Pair<String, Integer>("abc", 111);
2.2.2 泛型方法:即带有泛型参数的方法

泛型方法既可以定义在普通类中,也可以定义在泛型类中。例如:

public class ArrayAlg {
    public static <T> T getMiddle(T[] a) {
        return a[a.length / 2];
    }
}

可以这样使用:

String[] strings = {"aa", "bb", "cc"};
String middle = ArrayAlg.getMiddle(strings);
         
Integer[] arr = { 1, 2, 3 };
int value = ArrayAlg.getMiddle(arr);
2.2.3 泛型的实现原理: 编译器帮忙做了强制类型转换

从JVM角度看,并不存在真正的“泛型”的概念。
比如对于上述的Pair类,JVM看来(编译后的子界面)它是长这样的:

public class Pair {
    private Object first;
    private Object second;
 
    public Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }
 
    public Object getFirst() {
        return first;
    }
 
    public Object getSecond() {
        return second;
    }
 
    public void setFirst(Object newValue) {
        first = newValue;
    }
 
    public void setSecond(Object newValue) {
        second = newValue;
    }
}

由于JVM中泛型类Pair变为它的raw type, 因而getFirst方法返回的是一个Object对象,从而从编译器的角度看,这个方法返回的是我们实例化类时指定的类型参数的对象。实际上,是编译器帮我们完成了强制类型转换工作。也就是说,编译器会把对Pair泛型类中的getFirst方法的调用转化为两条虚拟机指令:第一条是对raw type方法getFirst的调用,返回一个Object对象;第二条指令把返回的Object对象强制转换为当初我们指定的类型参数类型

综上,泛型机制实际上是编译器帮我们分担了一些麻烦的工作。一方面使用类型参数,可以告诉编译器在编译时进行类型检查;另一方面,原本需要我们做的强制类型转换的工作也由编译器替我们做了。

二、JVM知识

JVM与Hotspot的关系:JVM是规范,Hotspot是对规范的实现。
JVM三个部分必须非常清楚:内存划分、class加载机制、GC策略。

1. 内存划分

在这里插入图片描述
1.方法区:为多个线程共享
即“永久代”。存储:常量静态变量虚拟机加载的类信息
运行时常量池:是方法区的一部分。

2.虚拟机栈:线程私有
存放局部变量表、操作栈、方法出口等信息。每个方法从被调用到执行完的过程,都对应着一个栈帧在虚拟机栈中的从入栈到出栈的过程。

局部变量表存放的内容:
1)编译器已知的基本数据类型:boolean, byte, char, short, int, float, long, double.
2)对象引用(非对象本身)
注:double和long类型的数据会占用2个局部变量的空间,而其余数据类型只占1个。

3.本地方法栈:与虚拟机栈的区别是虚拟机栈为虚拟机执行的方法服务,而本地方法栈为Native方法服务。

4.堆:为多个线程共享
堆分为新生代和老年代

5.程序计数器:当前线程所执行字节码的行号指示器

怎样判断是局部变量表呢?局部变量表所需的空间在编译期间就完全确定,在运行期间不会改变。

直接内存:除了JVM内存,Java用到的还有直接内存,即不是JVM规范中定义的内存区域。在NIO中引入了通道与缓冲区的IO方式,可以调用native方法直接分配堆外的内存,这个堆外的内存就是本机内存,本机内存不会影响堆内存的大小。

2. classloader机制

JVM把class文件加载到内存,并对数据进行校验、解析、初始化,最终形成JVM可以直接使用的Java类型的过程,即为class loader机制。

类从被加载到JVM内存中开始,到卸载出内存为止,它的生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分成为连接。
在这里插入图片描述
解析和初始化的顺序有时会改变,有时在初始化之后再开始解析,如运行时动态绑定

1.加载

主要步骤为:
a) 通过“类的全名”来获取定义这个类的二进制字节流。
b) 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
c) 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

加载阶段可以使用系统提供的类加载器(ClassLoader)来完成,也可以自己实现类加载器。

2.验证:确保class文件的字节流是否符合当前虚拟机的要求。

3.准备:为静态变量内存分配

4.解析:为普通类内存分配

5.初始化:为成员变量赋初始值

3. classloader与双亲委派

3.1 classloader过程

类加载器ClassLoader用来加载class字节码到JVM中。

类加载体系及ClassLoader双亲委派机制:j
java程序中的 .java文件编译完会生成 .class文件,而 .class文件就是通过被称为类加载器的ClassLoader加载的,而ClassLoder在加载过程中会使用“双亲委派机制”来加载 .class文件

在这里插入图片描述
1.BootStrapClassLoader:加载jre/lib下的类库
启动类加载器,该ClassLoader是jvm在启动时创建的,用于加载 $JAVA_HOME/jre/lib下面的类库(或者通过参数-Xbootclasspath指定)。

2.ExtClassLoader:加载jre/ext下的类库
扩展类加载器,该ClassLoader是在sun.misc.Launcher里作为一个内部类ExtClassLoader定义的(即 sun.misc.Launcher$ExtClassLoader),ExtClassLoader会加载 $JAVA_HOME/jre/lib/ext下的类库(或者通过参数-Djava.ext.dirs指定)。

3.AppClassLoader:加载ClassPath下
应用程序类加载器,该ClassLoader同样是在sun.misc.Launcher里作为一个内部类AppClassLoader定义的(即 sun.misc.Launcher$AppClassLoader),AppClassLoader会加载java环境变量CLASSPATH所指定的路径下的类库。

4.CustomClassLoader:自定义类加载器,该ClassLoader是指我们自定义的ClassLoader,比如tomcat的StandardClassLoader属于这一类;当然,大部分情况下使用AppClassLoader就足够了。

3.2 ClassLoader双亲委派机制

ClassLoader的双亲委派机制是这样的(这里先忽略掉自定义类加载器CustomClassLoader):

当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

双亲委派模式的工作原理的是;如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
通俗说法:即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成。

双亲委派模式优势:避免类被重复加载,防止API库被随意篡改

4. GC相关

4.1 垃圾收集时如何判断对象是否可回收

跟踪收集器会全局记录所有对象之间的引用状态,执行时从GC Roots的对象作为起点,从起点向下搜索所有的引用链,当一个对象到GC roots没有任何引用链时,则证明对象是不可用的。
在这里插入图片描述
上图中,即使Object6, Object7, Object8互相引用,但由于对于GC roots不可达,所以他们被判定为可回收的对象。

4.2 可作为GC roots的对象包括:

1.虚拟机栈中的引用对象
2.方法区中的类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI的引用对象
总之就是各种“对象”

4.3 内存调优

4.3.1 JVM内存调优的目前:减少stop the world时间

JVM调优主要针对内存管理方面的调优,即如何控制各个代的大小,GC策略。
由于GC开始垃圾回收时会暂停应用线程,这样会严重影响系统性能,调优的目的是为了尽量降低GC所导致的应用线程暂停时间、减少Full GC次数。

4.3.2 JVM调优参数

最关键参数:-Xms(初始堆大小)、 -Xmx(最大堆大小) 、-Xmn(新生代大小) 、XX:SurvivorRatio(Eden, s0, s1比率)、-XX:MaxTenuringThreshold(垃圾最大年龄)、-XX:PermSize(持久代初始值)、-XX:MaxPermSize(持久带最大值)

1.-Xms、-Xmx:通常设置为相同的值,避免运行时要不断扩展JVM内存。
2.-XMn: 新生代大小,新生代Eden、s0、s1的比率通过-XX:ServivorRatio来控制(例如值为4,表示Eden:S0:S1 = 4:3:3)
3.-XX:MaxTenuringThreshold: 对象在经过多少次minor GC之后进入老年代
4.-XX:PermSize、-XX:MaxPermSize: 指定方法区的大小,通常设置为相同的值。

4.3.3 JVM设置参数要考虑的问题

1.避免新生代大小设置过小:问题是,一是minor GC频繁;二是可能导致minor GC对象直接进入老年代。老年代太大导致full GC时间长。

2.避免新生代大小设置过大:一是老年代变小,可能导致Full GC频繁;二是minor GC执行回收的时间增加。

3.避免Survivor区过大或过小:
-XX:SurvivorRatio参数的值越大,Eden区越大,minor GC次数会降低,但两块Survivor区域(S0、S1)区域变小,如果超过Survivor区内存大小的对象在minor GC后仍没被回收,那么将不会从S0, S1之间互相copy, 而是直接进入老年代。

-XX:SurvivorRatio参数值设置过小,Eden区变小,minor GC次数会增加,Survivor区变大,意味着可存储更多的在minor GC存活的对象,避免其进入老年代

4.合理设置对象在新生代的存活的周期
默认-XX:MaxTenuringThreshold的值为15,即如果15次还没回收掉,第16次直接移入老年代。

调优:
1)初始堆大小和最大堆大小保持一致。
2)持久代初始值大小和持久代最大值大小保持一致。
3)其余保持不变:如Eden:s0:s1为4:3:3, minor GC第16次进入老年代。

4.4 垃圾回收器

垃圾回收器分为4类:串行回收器、并行回收器、CMS回收器(Concurrent Mark Sweep, 并发标记清除)、G1回收器(JDK1.7开始全新的回收器,用来取代CMS)

参考我的博客:
https://blog.csdn.net/shijinghan1126/article/details/88412387

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值