Java 和 C++ 之间有一堵由内存动态分配和垃圾收集技术所围成的高墙。
1、验证(Verify)
2、准备(Prepare)
public class Test01 {
private static int a = 3; // prepare阶段:a = 0;Initial:a = 3
public static void main(String[] args) {
System.out.println(a);
}
}
3、解析(Resolve)
public class Test02 {
private static int num = 1; // linking 的 prepare 阶段: num = 0; initial: num = 1 -> num = 2
// 静态代码块只能访问到定义在静态代码块之前的变量,定义在它之后的变量,静态代码块可以赋值,但是不能访问
static {
num = 2;
number = 20;
System.out.println("->" + num);
// System.out.println(number); 报错:非法的前向引用
}
private static int number = 10; // linking 的 prepare 阶段: number = 0; initial: number = 20 -> number = 10
public static void main(String[] args) {
System.out.println(num); // 2
System.out.println(number); // 10
}
}
public class ClassLoaderTest {
public static void main(String[] args) {
// 获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// 获取其上层:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader); // sun.misc.Launcher$ExtClassLoader@1b6d3586
// 获取不到其上层:引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader); // null
// 对于用户自定义类来说:默认使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// String 类使用引导类加载器进行加载 ---> Java 的核心类库都是使用引导类加载器进行加载的
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1); // null
}
}
package java.lang;
public class String {
public static void main(String[] args) {
// 在类 java.lang.String 中找不到 main 方法
System.out.println("自定义 java.lang.String");
}
}
package java.lang;
public class ShkStart {
public static void main(String[] args) {
// Prohibited package name: java.lang
System.out.println("java.lang.ShkStart");
}
}
JVM 必须知道一个类型是由启动类加载器加载还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么 JVM 会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM 需要保证这两个类型的类加载器是相同的。
由于跨平台性的设计,Java 的指令都是根据栈来设计的。不同平台 CPU 架构不同,所以不能设计为基于寄存器的,因为基于寄存器的话它和具体的 CPU 耦合度高。
优点是跨平台,指令集小,编译器容易实现。缺点是性能比基于寄存器的差,实现同样的功能需要更多的指令。
-Xss
选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。可以在 idea 的 Run->Edit Configurations->VM options(例如填入:-Xss256k)
public class SlotTest {
public void test(){
int a = 0;
{
// 变量 b 出了大括号后就销毁了,但数组空间已经开辟了
int b = 1;
}
// 变量 c 是使用之前已经销毁的变量 b 占用的 slot 的位置
int c = 2;
}
}
基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这意味着将需要更多的指令分派和内存读/写次数。
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM 的设计者们提出了栈顶缓存(ToS)技术:将栈顶元素全部缓存在物理 CPU 的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
// 变量本身有类型信息
Java: String s = "abc";
// 变量本身没有类型信息,变量值有
JS: var name = "abc"; var name = 123;
栈帧中还允许携带与 Java 虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。
public class ThreadSafty {
// 该方法中 s 的声明方式是线程安全的
public static void method1(){
StringBuilder s = new StringBuilder();
s.append("a");
s.append("b");
}
// s 的操作过程:是线程不安全的。因为 s 是从外面传进来的,有可能由多个线程所调用
// 严格上 s 不算是方法内定义变量,算是形参的变量
public static void method2(StringBuilder s){
s.append("a");
s.append("b");
}
// s 的操作过程:是线程不安全的。因为将 s 返回出去后就有可能被其他位置上的多个线程所调用
public static StringBuilder method3(){
StringBuilder s = new StringBuilder();
s.append("a");
s.append("b");
return s;
}
// s 的操作过程:是线程安全的。因为 s 其实就在该方法内部消亡了,没有传到外面去。
public static String method4(){
StringBuilder s = new StringBuilder();
s.append("a");
s.append("b");
return s.toString();
}
}
目前本地方法使用的越来越少了,除非是与硬件有关的应用,比如通过 Java 程序驱动打印机或者 Java 系统管理生产设备。在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用 Socket 通信,也可以使用 Web Service 等等。
-Xms
:用来设置堆空间(年轻代+老年代)的初始内存大小。-Xmx
:用来设置堆空间(年轻代+老年代)的最大内存大小。-XX:+PrintGCDetails
public class HeapSpace {
public static void main(String[] args) {
// 返回 Java 虚拟机中的初始堆内存大小
long initialSize = Runtime.getRuntime().totalMemory() / 1024 / 1024;
// 返回 Java 虚拟机中的最大堆内存大小
long maxSize = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms: " + initialSize + "M"); // -Xms: 575M
System.out.println("-Xmx: " + maxSize + "M"); // -Xmx: 575M
}
}
-XX:+PrintGCDetails
我们可知,新生代的 total 是 179200K,而 eden + from + to 一共有 204 800 k,但因为只算一份幸存者区的内存,所以结果是 153600 + 25600 = 179200。-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=
进行设置。java.lang.OutOfMemoryError: Java heap space
-XX:MaxTenuringThreshold
来设置。-XX:TLABWasteTargetPercent
设置 TLAB 空间所占 Eden 空间的百分比大小。-XX:UseTLAB
设置是否开启 TLAB 空间,默认是开启了 TLAB 空间。-XX:+DoEscapeAnalysis
显示开启逃逸分析,还可以通过选项-XX:+PrintEscapeAnalysis
查看逃逸分析的筛选结果。public class EscapeAnalysis {
// 如何快速判断是否发生逃逸:大家就看 new 的对象实体是否有可能在方法外被使用。
public EscapeAnalysis obj;
// 方法返回 EscapeAnalysis 对象,发生逃逸。
public EscapeAnalysis getInstance(){
return obj == null ? new EscapeAnalysis() : obj;
}
// 为成员属性赋值,发生逃逸
public void setObj(){
obj = new EscapeAnalysis();
}
// 对象的作用域仅在当前方法内有效,没有发生逃逸
public void useEscapeAnalysis(){
EscapeAnalysis e = new EscapeAnalysis();
}
// 引用成员变量的值,发生逃逸。因为判断的是对象实体能否被方法外调用,对象实体才是放在堆空间中的。变量 e 对应的对象实体可以通过 obj 在方法外被调用
public void useEscapeAnalysis1(){
EscapeAnalysis e = getInstance();
}
}
-XX:+EliminateAllocations
:开启了标量替换(默认打开),允许将对象打散分配在栈上。-XX:PermSize
来设置永久代初始分配空间。默认值是 20.75 M。-XX:MaxPermSize
来设置永久代最大可分配空间。32 位机器默认是 64 M,64 位及其默认是 82 M。-XX:MetaspaceSize
来设置元空间初始分配空间;通过 -XX:MaxMetaspaceSize
来设置元空间最大可分配空间。默认值依赖于平台。Windows 下,-XX:MetaspaceSize
是 21 M,-XX:MaxMetaspaceSize
的值是 -1,表示没有限制-XX:MetaspaceSize
:设置元空间的初始大小。对于一个 64 位的服务器端 JVM 来说,-XX:MetaspaceSize
的默认值是 21 M。这就是初始的高水位线,一旦触及这个水位线,Full GC 将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置。新的高水位线的值取决于 GC 后释放了多少元空间。如果释放的空间不足,那么在不超过 MaxMetaspaceSize 时,适当提高该值。如果释放空间过多,则适当降低该值。-XX:MetaspaceSize
设置为一个相对较高的值。方法区内部包含了运行时常量池;字节码文件内部包含了常量池。
字节码文件当中的常量池被加载到方法区以后对应的结构就称为运行时常量池。
字节码文件:
一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息那就是常量池表(Constant Pool Table),包括各种字面量以及对类型、域和方法的符号引用。
注:常量池可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等数据。
public class CustomerTest {
public static void main(String[] args) {
Customer cust = new Customer();
}
}
class Customer{
int id = 1001;
String name;
Account acct;
{
name = "匿名用户";
}
public Customer(){
acct = new Account();
}
}
class Account{}
-XX:MaxDirectMemorySize
设置,如果不指定,默认与堆的最大值 -Xmx 参数值一致。-XX:CompileThreshold
来人为设定。-Xint
:完全采用解释器模式执行程序-Xcomp
:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行-Xint
:采用解释器和即时编译器的混合模式共同执行程序-client
:指定 Java 虚拟机运行在 Client 模式下,并使用 C1 编译器;C1 编译器会对字节码进行简单和可靠的优化,耗时短,以达到更快的编译速度。-server
:指定 Java 虚拟机运行在 Server 模式下,并使用 C2 编译器;C2 编译器会进行耗时较长的优化,以及激进优化,但优化的代码执行效率更高。-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler
去激活,才可以使用。.java -> .class -> .so
,字节码文件可以跨平台,而 .so 文件已经是机器码了,与具体硬件相关。-XX:StringTableSize
可设置 StringTable 的长度。public class StringTest {
// 常量与常量的拼接
public void test1(){
// 在生成字节码文件的时候,就直接将 "a" + "b" + "c" 等同于 "abc"(可查看字节码文件,或者查看反编译的结果)
String s1 = "a" + "b" + "c"; // 等同于 "abc"
String s2 = "abc"; // "abc" 一定是放在字符串常量池中的,然后将其地址赋给 s2
System.out.println(s1 == s2); // true
System.out.println(s1.equals(s2)); // true
}
// "+" 两边至少有一个是变量
public void test2(){
String s1 = "a";
String s2 = "b";
String s3 = "ab";
/*
如下的 s1 + s2 细节:(变量 s 是我临时定义的,底层没说变量叫 s)
(1) StringBuilder s = new StringBuilder()
(2) s.append("a")
(3) s.append("b")
(4) s.toString() --> 约等于 new String("ab"),但是字符串常量池中并不会存在 "ab"(通过 StringBuilder 中的 toString() 的字节码可知)
补充:jdk5 及之后使用的是 StringBuilder,jdk5 之前使用的是 StringBuffer
*/
String s4 = s1 + s2; // s4 变量记录的地址为:new String("ab"),但是字符串常量池中并不会存在 "ab"
System.out.println(s3 == s4); // false
}
@Test
public void test3(){
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";
// 如果拼接符号的前后出现了变量,则相当于在堆空间中 new String(),具体的内容为拼接后的结果:javaEEhadoop
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4); // true
System.out.println(s3 == s5); // false
System.out.println(s3 == s6); // false
System.out.println(s3 == s7); // false
System.out.println(s5 == s6); // false
System.out.println(s5 == s7); // false
System.out.println(s6 == s7); // false
/* intern():判断字符串常量池中是否存在 javaEEhadoop 值,如果存在,则返回常量池中 javaEEhadoop 的地址;
如果不存在,则在常量池中加载一份 javaEEhadoop,并返回加载的 javaEEhadoop 在常量池中的地址 */
String s8 = s6.intern();
System.out.println(s3 == s8); // true
}
// 常量与常量的拼接
public void test4(){
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
// 下面的字符串拼接操作仍然是编译期优化,可以看成是两个常量相加(final 修饰),而不是使用 StringBuilder 的方式
String s4 = s1 + s2; // 因为加了 final 修饰,所以 s1 和 s2 就是常量
System.out.println(s3 == s4); // true
}
}
public class StringTest {
/**
* 执行效率:使用 StringBuilder 的 append() 方式添加字符串的效率要远高于使用 String 的字符串拼接方式
* 为什么:
* StringBuilder 的 append() 方式,自始至终只创建过一个 StringBuilder 对象
* 使用 String 的字符串拼接方式:创建了许多 StringBuilder 对象和 String 对象,占用内存过大;如果进行 GC,还要花费额外的时间
*
* StringBuilder 的改进:
* StringBuilder s = new StringBuilder() 的方式,value 数组的默认大小是 16。如果字符串长度过大则需要对数组进行扩容。
* 因此如果可以确定前前后后添加的字符串长度不高于某个最大值 highLevel,则可以使用 StringBuilder s = new StringBuilder(highLevel);
*/
@Test
public void test5(){
long start = System.currentTimeMillis();
// method1(100000); // 6092 ms
method2(100000); // 9 ms
long end = System.currentTimeMillis();
System.out.println(end - start);
}
public void method1(int highLevel){
String s = "";
for(int i = 0; i < highLevel; i++){
s = s + "a"; // 每次循环都会创建一个 StringBuilder 和 String (调用 StringBuilder 的 toString() 方法时会 new String)
}
}
public void method2(int highLevel){
StringBuilder s = new StringBuilder();
for(int i = 0; i < highLevel; i++){
s.append("a");
}
}
}
public class StringTest {
/**
* new String("ab") 会创建多少个对象
* 看字节码文件,就知道是两个:
* (1)一个对象是:new 关键字在堆空间创建的
* (2)另一个对象是:字符串常量池中的对象 "ab"。字节码指令:ldc
* 其实严谨点也可能是一个,因为有可能在 new String("ab") 之前字符串常量池中已经有 "ab",此时就直接用已有的 "ab"
*/
public void test6(){
String s = new String("ab");
}
}
public class StringTest {
/**
* new String("a") + new String("b") 会创建多少个对象:
* (1)new StringBuilder()
* (2)new String("a")
* (3)常量池中的 "a"
* (4)new String("b")
* (5)常量池中的 "b"
* 深入剖析:StringBuilder 的 toString():
* (6)new String("ab")
* 注意:toString() 的调用,在字符串常量池中,并没有生成 "ab"
*/
public void test6(){
String s = new String("a") + new String("b");
}
}
public class StringTest1 {
public static void main(String[] args) {
String s1 = new String("1");
s1.intern(); // 调用此方法之前,字符串常量池中已经存在了 "1"
String s2 = "1";
System.out.println(s1 == s2); // jdk6:false jdk7/8:false
String s3 = new String("1") + new String("1"); // s3 变量记录的地址为:new String("11")
// 执行完上一行代码以后,字符串常量池中不存在 "11"
s3.intern(); // 在字符串常量池中生成 "11"。如何理解:jdk6:在常量池中创建了一个新的对象 "11";
// jdk7/8:常量池中并没有创建 "11",而是创建了一个指向堆空间中 new String("11") 的地址
String s4 = "11";
System.out.println(s3 == s4); // jdk6:false jdk7/8:true
}
}
public class StringTest2 {
public static void main(String[] args) {
String s = new String("a") + new String("b"); // s 变量记录的地址为:new String("ab"),但是字符串常量池中并不存在 "ab"
String s2 = s.intern(); // jdk6中:在字符串常量池中创建一份 "ab"
// jdk7/8 中:字符串常量池中并没有创建 "ab",而是创建了一个引用,指向 new String("ab"),并返回该引用地址
System.out.println(s2 == "ab"); // jdk6:true jdk7/8:true
System.out.println(s == "ab"); // jdk6:false jdk7/8:true
}
}
-XX:+DisableExplicitGC
设置成true,则不会进行回收)。public class SystemGCTest {
public static void main(String[] args) {
new SystemGCTest();
System.gc(); // 提醒 JVM 的垃圾回收器执行 GC,但是不保证会马上执行
System.runFinalization(); // 强制调用失去引用的对象的 finalize() 方法
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("重写了 finalize() 方法");
}
}
public class LocalVarGC {
public void localvarGC1(){
byte[] buffer = new byte[10 * 1024 * 1024]; // 10MB
System.gc(); // buffer 不会被回收掉空间
}
public void localvarGC2(){
byte[] buffer = new byte[10 * 1024 * 1024];
buffer = null;
System.gc(); // buffer 会被回收掉空间
}
public void localvarGC3(){
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
System.gc(); // buffer 不会被回收掉空间。在 gc 时,buffer 还占用着局部变量表中下标为 1 的位置,因此回收不了
}
public void localvarGC4(){
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
int value = 1;
System.gc(); // buffer 会被回收掉空间。由于 buffer 过了作用域,导致 value 会复用局部变量表中索引为 1 的位置(slot 重用),导致 buffer 这个引用就不存在了
}
public void localvarGC5(){
localvarGC1();
System.gc(); // 会回收掉 localvarGC1() 中的 buffer 的空间
}
public static void main(String[] args) {
LocalVarGC local = new LocalVarGC();
local.localvarGC5();
}
}
-XX:+PrintCommandLineFlags
:查看命令行相关参数(包含使用的垃圾收集器)jinfo -flag 相关垃圾回收器参数 进程ID
。使用命令行指令 jps
查看进程ID-XX:+UseSerialGC
参数可以指定年轻代和老年代都使用串行收集器。也就是新生代使用 Serial GC,老年代使用 Serial Old GC。-XX:+UseParNewGC
:指定新生代使用 ParNew 收集器。-xx:ParallelGCThreads
:限制垃圾收集线程的数量,默认开启和 CPU 数目相同的线程数。-XX:+UseAdaptiveSizePolicy
:设置 Parallel Scavenge 收集器具有自适应调节策略。-XX:G1HeapRegionSize
来设定,取值范围为 1MB~32MB,且应为 2 的 N 次幂,即 1MB、2MB、4MB、8MB、16MB、32MB。所有的 Region 大小相同,且在 JVM 生命周期内不会被改变。-Xmn
或-XX:NewRatio
等相关选项显式设置年轻代大小。因为固定年轻代的大小会覆盖暂停时间目标。年轻代的 YGC 是独占式的,如果设置的年轻代大小不合理就会导致无法达到暂停时间的目标,所以需要让 JVM 动态地调整。