JVM中实用性细节

1、设置Java虚拟机栈大小-Xss

\quad Java默认栈大小为1M,如果在多次使用递归函数的情况下可能会爆栈,例如示例程序,我是用递归函数dfs来求解1+2+3+...+n的值,在n=100000时就会爆栈。
在这里插入图片描述
\quad 我们修改配置,加上编译参数-Xss1024m就可以防止爆栈,但是也不能修改得太大,因为总的内存是一定的,如果给每个线程开启的栈内存过大会导致无法开启多个线程。
在这里插入图片描述
\quad 上述爆栈称为栈帧数目过多导致爆栈,在一个线程里面,每一次调用函数都会形成一个栈帧放入栈内,上述例子会调用100000次dfs函数,会形成10w个栈帧,如果栈内存太小则会爆栈。
\quad 还有一种情况是栈帧过大导致溢出,比如某个函数里面使用的局部变量数目特别多,导致该栈帧过大。一般这种情况不太会出现,因为栈保存的只有局部变量等信息,这些信息内存较小。

2、线程安全问题

\quad 当一个函数被多个线程同时运行时,如何保证某个变量不被重复修改而导致线程不安全。如果要保证该变量线程安全,则需要把该变量作为方法体内部变量,且不作为函数参数或返回值。因为一旦作为函数参数,则多个线程同时调用该函数时会多次修改改变量;同样的,作为返回值也会存在返回值被另一个线程的方法修改该返回值的情况。

public class ThreadSecure {
    public static void main(String[] args) {
        m1();  // 12
        new Thread(()->{m1();}).start();  // 12


        StringBuilder sb = new StringBuilder();
        m2(sb);  // 12
        new Thread(()->{m2(sb);}).start();  // 1212

        StringBuilder sb2 = m3();
        new Thread(()->{
            sb2.append(999);
            System.out.println(sb2);  // 12999
        }).start();
        System.out.println(sb2);  // 可能是12或者12999
    }

    public static void m1(){  // 方法内线程安全
        StringBuilder sb = new StringBuilder();
        sb.append(1); sb.append(2);
        System.out.println(sb.toString());
    }
    // 多个线程共享了同一个对象,因此线程不安全
    public static void m2(StringBuilder sb){
        sb.append(1); sb.append(2);
        System.out.println(sb.toString());
    }
    // 另一个线程可能会取到该返回值进而修改StringBuilder对象,因此也不是线程安全的
    public static StringBuilder m3(){
        StringBuilder sb = new StringBuilder();
        sb.append(1); sb.append(2);
        return sb;
    }
}

\quad 总结:一个变量是方法的私有变量则线程安全,即该变量没有逃离方法的作用范围,即没有作为参数或者返回值。如果该变量是基本数据类型,则每次是传值而不是传引用,便不存在线程安全问题。

3、堆内存

\quad 函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。所有通过new关键字创建的对象都会在堆中。其特点是堆内存是线程共享的,堆中对象都需要考虑线程安全问题,堆内存有垃圾回收机制。
\quad 一般说的程序爆内存就是指堆内存不够了,可以收用-Xmx1024m控制堆内存大小为1G。
\quad 下列程序在-Xmx1m的情况下输出的字符串长度为98304,在默认情况下输出的字符串长度为201326592。说明在我的电脑上默认堆最大内存为2G左右。

public class HeapMem {
    public static void main(String[] args) {
        String s = "abc";
        while(true){
            s = s + s;
            System.out.println(s.length());
        }
    }
}

4、方法区

\quad 存放java类的信息(类方法,字段,构造器等数据)、类加载器和常量池(例如字符串常量池StringTable)。

  • 在jdk1.6之前,方法区使用永久代实现,大小有限,过多类方法会导致永久代溢出,例如使用spring或者mybatis时产生大量类;在jdk1.8之后,使用元空间实现,该空间占用系统内存,因此系统有多大内存它就能使用多大内存,不太可能产生移溢出了。
  • 在jdk1.6之前,方法区的字符串常量放置在永久代;在1.8后,方法区的字符串常量放在堆内存中去了,但仍属于方法区,方法区更多是一种逻辑分割。
  • 动态拼接的字符串仅存在于堆中,串池中没有
  • 常量池中的字符串仅是符号,第一次用到时才变为对象。
  • 利用串池的机制,来避免重复创建字符串对象。
  • 字符串变量拼接的原理是 StringBuilder (1.8)。
  • 字符串常量拼接的原理是编译期优化。
  • 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池。
    1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回;1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
public class StringTable {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";  // 放在字符串常量池
        String s4 = s1 + s2;  // 相当于new String("ab"),放在堆中
        String s5 = "a" + "b";  // javac 在编译期优化,结果在编译期确定为ab
        System.out.println(s3 == s4);  // false
        System.out.println(s3 == s5);  // true
        
        String s = new String("c") + new String("d");  // 串池中有a,b,但"ab"是动态创建的,因此没有放入串池
        System.out.println(s == "cd");  // false
        
        String ss = s.intern();  // 将字符串对象放入串池
        System.out.println(ss == "cd");
    }
}

5、java垃圾回收

a) 如何判断对象可以回收

  • 引用计数法:用计数器记录每个对象被引用次数,当对象被引用时计数器加1,引用失效时计数器减1,计数器为 0的对象是垃圾对象。缺点:如果两个对象相互引用,则它们的计数器至少同时为1,将永远不会被回收。JVM不采用此种方式判断对象是否可以被回收。
  • 可达性分析算法:从GC根对象(永远不会被回收的对象)出发,这些跟对象会跟各种对象组成若干个连通图,如果一个对象可以从某个根对象直接或间接找到,则该对象不可以被回收,否则可以被回收。Java虚拟机种垃圾回收器采用可达性分析来探索所有存活的对象。

哪些对象可以作为GC root对象

  • 1.虚拟机栈:栈帧中的本地变量表引用的对象
  • 2.native方法引用的对象
  • 3.方法区中的静态变量和常量引用的对象

Java种四种引用

在这里插入图片描述

b) 垃圾回收算法

  • 标记清除算法:根据GC root查找出所有存活对象,将垃圾对象地址存放起来,以后有新的对象需要内存的时候就可以在垃圾对象地址段找出一段合适的空间存放新的对象。标记-清除算法不需要对垃圾对象进行移动,因此清除垃圾对象后会产生大量不连续的内存碎片,空间碎片太多会导致以后遇到需要分配较大内存的对象时无法找到足够的内存以至于触发又一次垃圾回收工作。
  • 标记整理算法:从根集合GC root进行扫描,对存活对象进行标记;移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。
  • 复制算法:将内存划分为两个区:对象区和空闲区。对象在对象区创建,当进行垃圾回收时,把对象区种存活的对象拷贝到空闲区,然后清空对象区,最后把空闲区对象拷贝到对象区。

\quad JVM中根据不同情况使用上述三种垃圾回收算法,称为分代回收算法

分代回收算法

在这里插入图片描述

  • 在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old)(Java8以前还有永久代)。新生代 ( Young ) 又被划分为三个区域:Eden、幸存区From和幸存区To。
  • 老年代用于存放需要长期使用的对象,新生代用于存放用完就可以释放的对象。因此老年代垃圾回收频率要远小于新生代
  • 当一个新对象产生时,会放在伊甸园(Eden)区,当Eden区满后,会触发一次小的垃圾回收,称为Minor GC,然后使用复制算法把留下的对象复制到幸存区To中,并让这些对象的寿命+1,之后交换幸存区From和幸存区To的位置,这样幸存区To和Eden区又空闲了,接下来就可以继续产生新对象了。如此循环,当幸存区中对象寿命超过15时,则该对象晋升到老年代。当老年代内存不足时,会触发一次Full GC。有一种特殊情况,当一个对象内存过大无法放入新生代时会直接放入老年代
  • 把java堆分成新生代和老年代,这样就可以根据各个年待的特点采用最适合的收集算法。在新生代中,每次垃圾收集都会有大量的对象死去,只有少量存活,所以选用复制算法。老年代因为对象存活率高,没有额外空间对他进行分配担保,所以一般采用标记整理或者标记清除算法进行回收。

6、Java类加载机制
在这里插入图片描述
\quad 主要是如何从Java源代码编译成Java字节码(class)文件和字节码文件经过类加载器进行类加载,之后在虚拟机中就可以由执行引擎执行字节码指令。计算机只认识二进制。.java 源文件 通过javac 转换成.class 字节码文件,这个时候计算机还是不能直接识别的,然后jvm加载class文件,再翻译成二进制指令,这个就是一个大概完整的过程。字节码大全
\quad javap可以帮助我们分析字节码文件,弄成人看得懂的形式。

类成员初始化顺序

  • 1.静态代码块先初始化
  • 2.构造函数
  • 3.静态代码块随着类的加载而执行,只执行一次,并优先于主函数。具体说,静态代码块是由类调用的。类调用时,先执行静态代码块,然后才执行主函数的。静态代码块其实就是给类初始化的,而构造代码块是给对象初始化的。

什么时候会导致类初始化

  • main方法所在的类,总是会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new会导致初始化
  • 注意:访问类的static final静态常量(需要时基础类型)不会触发初始化
  • 类初始化
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页