首页 文章 精选 留言 我的

精选列表

搜索[流量结构],共10000篇文章
优秀的个人博客,低调大师

浅谈JVM - 内存结构(三)- 虚拟机栈

2.1 定义 Java Virtual Machine Stacks(Java虚拟机栈) Java 虚拟机栈描述的是 Java 方法执行的内存模型,用于存储栈帧,是线程私有的,生命周期随着线程启动而产生,线程结束而消亡 线程启动时会创建虚拟机栈,每个方法在执行时会在虚拟机栈中创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态连接、方法返回地址等信息。每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中的入栈(压栈)到出栈(弹栈)的过程 每个线程只能有一个活动栈帧,对应着正在执行的那个方法 问题辨析 垃圾回收是否涉及栈内存? 不涉及。栈内存无非就是一次次的方法调用产生的栈帧内存,栈帧内存在每一次方法调用后都会被弹出栈,也就是这部分内存会被自动的回收掉,所以并不需要垃圾回收来回收栈内存。 栈内存分配越大越好吗? 不是。 栈内存可以在代码运行时通过一个虚拟机参数来指定其大小 -Xss size。不指定的话,除了windows系统,默认都是1M,windows系统是依据虚拟内存大小分配。 栈内存分配的越大,只是能够进行更多次的方法递归调用,并不会增快运行的效率,反而会使得可执行的线程数变少(总内存不变,每个线程的栈内存变大,数量变少) 方法内的局部变量是否线程安全? 变量是否是线程安全的,取决于这个变量被多线程共享时,每次运行结果和单线程运行的结果是否是一样的。 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的。 如果局部变量是引用对象,且逃离了方法的作用范围,那么就需要考虑线程安全(基本数据类型不会有这个问题) 示例代码1 static void m1() { int x=0; for(int i=0;i<500;i++){ x++; } System.out.println(x); } x这个局部变量是线程安全的。每个线程对应一个栈,然后线程内每次方法调用都会产生一个新的栈帧,所以x变量处于不同线程的栈的栈帧中,互不影响,也就是线程安全的。 示例代码2 public static void m1() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static void m2(StringBuilder sb) { sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static StringBuilder m3() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); return sb; } m1方法中sb是线程安全的,因为其是线程内的局部变量。 m2方法中sb是线程不安全的,因为其是作为方法的参数传递进来,那么就有可能有其他线程能够访问到这个变量,那么这个变量就是多个线程共享的,可能造成值不一致,也就是线程不安全的。 m3方法中sb是线程不安全的,虽然其是线程中的局部变量,但是其作为返回值返回了,那么就有可能被其他线程使用,也就是多线程共享,可能造成值不一致,线程不安全。 2.2 栈帧 栈帧存在于 Java 虚拟机栈中,是 Java 虚拟机栈中的单位元素,每个线程中调用同一个方法或者不同的方法,都会创建不同的栈帧(可以简单理解为,一个线程调用一个方法创建一个栈帧),所以,调用的方法链越多,创建的栈帧越多(例如:递归)。每调用一个新的方法,被调用方法对应的栈帧就会被放到栈顶(入栈),也就是成为新的当前栈帧。当一个方法执行完成退出的时候,此方法对应的栈帧也相应销毁(出栈)。 2.2.1 局部变量表(Local 2.2.1 局部变量表(Local Variable Table) 每个栈帧中都包含一组称为局部变量表的变量列表,用于存放方法参数和方法内部定义的局部变量。在 Java 程序编译成 Class 文件时,在 Class 文件格式属性表中 Code 属性的 max_locals(局部变量表所需的存储空间,单位是 Slot) 数据项中确定了需要分配的局部变量表的最大容量。 局部变量表的容量以变量槽(Variable Slot)为最小单位,不过 Java 虚拟机规范中并没有明确规定每个 Slot 所占据的内存空间大小,只是有导向性地说明每个 Slot 都应该存放的8种类型: byte、short、int、float、char、boolean、reference(对象引用就是存到这个栈帧中的局部变量表里的,这里的引用指的是局部变量的对象引用,而不是成员变量的引用。成员变量的对象引用是存储在 Java 堆(Heap)中)、returnAddress(虚拟机数据类型,returnAddress 类型的值就是指向特定指令内存地址的指针,JVM支持多线程,每个线程有自己的程序计数器(pc register),而 pc 中的值就是当前指令所在的内存地址,即 returnAddress 类型的数据,当线程执行 native 方法时,pc 中的值为 undefined)类型的数据,这8种类型的数据,都可以使用32位或者更小的空间去存储。Java 虚拟机规范允许 Slot 的长度可以随着处理器、操作系统或者虚拟机的不同而发生变化。对于64位的数据类型,虚拟机会以高位在前的方式为其分配两个连续的 Slot 空间。即 long 和 double 两种类型。做法是将 long 和 double 类型速写分割为32位读写的做法。不过由于局部变量表建立在线程的堆栈上,是线程的私有数据,无论读写两个连续的 Slot 是否是原子操作,都不会引起数据安全问题。 Java 虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的 Slot 数量。如果是32位数据类型的数据,索引 n 就表示使用第 n 个 Slot,如果是64位数据类型的变量,则说明要使用第 n 和第 n+1 两个 Slot。 在方法执行过程中,Java 虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程。如果是实例方法(非 static 方法),那么局部变量表中的第0位索引的 Slot 默认是用来传递方法所属对象实例的引用,在方法中可以通过关键字 this 来访问这个隐含的参数。其余参数按照参数表的顺序来排列,占用从1开始的局部变量 Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的 Slot。 局部变量表中的 Slot 是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码程序计数器的值已经超过了某个变量的作用域,那么这个变量相应的 Slot 就可以交给其他变量去使用,节省栈空间,但也有可能会影响到系统的垃圾收集行为。 局部变量无初始值(实例变量和类变量都会被赋予初始值),类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予开发者定义的值。因此即使在初始化阶段开发者没有为类变量赋值也没有关系,类变量仍然具有一个确定的默认值。但局部变量就不一样了,如果一个局部变量定义了但没有赋初始值是不能使用的。 使用一段代码说明一下局部变量表: // java 代码 public int test() { int x = 0; int y = 1; return x + y; } // javac 编译后的字节码,使用 javap -v 查看 public int test(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: iconst_0 1: istore_1 2: iconst_1 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: ireturn LineNumberTable: line 7: 0 line 8: 2 line 9: 4 LocalVariableTable: Start Length Slot Name Signature 0 8 0 this Lcom/alibaba/uc/TestClass; 2 6 1 x I 4 4 2 y I 对应上面的解释说明,通过 LocalVariableTable 也可以看出来: Code 属性: stack(int x(1个栈深度)+ int y(1个栈深度))=2, locals(this(1 Slot)+ int x(1 Slot)+ int y(1 Slot))=3, args_size(非 static 方法,this 隐含参数)=1 验证 Slot 复用,运行以下代码时,在 VM 参数中添加 -verbose:gc: public void test() { { byte[] placeholder = new byte[64 * 1024 * 1024]; } int a = 0; // 当这段代码注释掉时,System.gc() 执行后,也并不会回收这64MB内存。当这段代码执行时,内存被回收了 System.gc(); } 局部变量表中的 Slot 是否还存在关于 placeholder 数组对象的引用。当 int a = 0; 不执行时,代码虽然已经离开了 placeholder 的作用域,但是后续并没有任何对局部变量表的读写操作,placeholder 原本所占用的 Slot 还没有被其他变量所复用,所以 placeholder 作为 GC Roots(所有 Java 线程当前活跃的栈帧里指向 Java 堆里的对象的引用) 仍然是可达对象。当 int a = 0; 执行时,placeholder 的 Slot 被变量 a 复用,所以 GC 触发时,placeholder 变成了不可达对象,即可被 GC 回收。 2.2.2 操作数栈(Operand Stack) 操作数栈是一个后入先出(Last In First Out)栈,方法的执行操作在操作数栈中完成,每一个字节码指令往操作数栈进行写入和提取的过程,就是入栈和出栈的过程。 同局部变量表一样,操作数栈的最大深度也是Java 程序编译成 Class 文件时被写入到 Class 文件格式属性表的 Code 属性的 max_stacks 数据项中。 操作数栈的每一个元素可以是任意的 Java 数据类型,32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2,在方法执行的任何时候,操作数栈的深度都不会超过在 max_stacks 数据项中设定的最大值(指的是进入操作数栈的 “同一批操作” 的数据类型的栈容量的和)。 当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,通过一些字节码指令从局部变量表或者对象实例字段中复制常量或者变量值到操作数栈中,也提供一些指令向操作数栈中写入和提取值,及结果入栈,也用于存放调用方法需要的参数及接受方法返回的结果。例如,整数加法的字节码指令 iadd(使用 iadd 指令时,相加的两个元素也必须是 int 型) 在运行的时候将操作数栈中最接近栈顶的两个 int 数值元素出栈相加,然后将相加结果入栈。 2.2.3 动态连接(Dynamic Linking) 每个栈帧都包含一个指向**运行时常量池(JVM 运行时数据区域)**中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。 <span style='color:red'>在 Class 文件格式的常量池(存储字面量和符号引用)中存有大量的符号引用(1.类的全限定名,2.字段名和描述符,3.方法名和描述符),字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载过程的解析阶段的时候转化为直接引用(指向目标的指针、相对偏移量或者是一个能够直接定位到目标的句柄),这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。</span> 看看以下代码的 Class 文件格式的常量池: // java 代码 public Test test() { return new Test(); } // 字节码指令 // Class文件的常量池 Constant pool: #1 = Methodref #4.#19 // java/lang/Object."<init>":()V #2 = Fieldref #3.#20 // com/alibaba/uc/Test.i:I #3 = Class #21 // com/alibaba/uc/Test #4 = Class #22 // java/lang/Object #5 = Utf8 i #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/alibaba/uc/Test; #14 = Utf8 test #15 = Utf8 ()I #16 = Utf8 <clinit> #17 = Utf8 SourceFile #18 = Utf8 Test.java #19 = NameAndType #7:#8 // "<init>":()V #20 = NameAndType #5:#6 // i:I #21 = Utf8 com/alibaba/uc/Test #22 = Utf8 java/lang/Object public int test(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: getstatic #2 // Field i:I 3: areturn LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 4 0 this Lcom/alibaba/uc/Test; 从上面字节码指令看出 0: getstatic #2 // Field i:I 这行字节码指令指向 Constant pool 中的 #2,而 #2 中指向了 #3 和 #20 为符号引用,在类加载过程的解析阶段会被转化为直接引用(指向方法区的指针)。 2.2.4 方法返回地址 当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令(例如:areturn),这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。 另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用 athrow 字节码指令产生的异常,只要在本方法的异常处理器表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。 无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的程序计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。 方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整程序计数器的值以指向方法调用指令后面的一条指令等。 2.3 栈内存溢出 栈帧过多导致栈内存溢出 例如递归方法,当方法调用层级过多,产生大量的栈帧,却没有出栈,就会导致栈内存溢出,抛出StackOverflowError异常 示例代码 public class Demo { private static int count; public static void main(String[] args) { try { method1(); } catch (Throwable e) { e.printStackTrace(); System.out.println(count); } } private static void method1() { count++; method1(); } } 执行结果 java.lang.StackOverflowError at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) 14602 可以看到方法总共执行了14602次就导致了栈溢出,可以在虚拟机运行参数中设置-Xss128k调整栈内存大小,结果执行次数就会变小 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将抛出一个 OutOfMemoryError 异常 2.4 线程运行诊断 2.4.1 cpu占用过高 示例代码 public class Demo { public static void main(String[] args) { new Thread(null, () -> { System.out.println("1..."); while(true) { } }, "thread1").start(); new Thread(null, () -> { System.out.println("2..."); try { Thread.sleep(1000000L); } catch (InterruptedException e) { e.printStackTrace(); } }, "thread2").start(); new Thread(null, () -> { System.out.println("3..."); try { Thread.sleep(1000000L); } catch (InterruptedException e) { e.printStackTrace(); } }, "thread3").start(); } } linux上运行此代码 javac Demo.java nohup java Demo & 查看输出 tail -f nohup.out 1... 2... 3... 1... 2... 3... 查看cpu状况 top 可以看到刚才运行的java程序占用cpu很高,进程号为10526 查询该进程下所有线程的运行状态 top -Hp 10526 可以看到占用cpu最高的线程是10536 使用jstack工具获取10526进程中所有线程运行信息 jstack 10526 2019-12-17 17:24:00 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.11-b03 mixed mode): "Attach Listener" #12 daemon prio=9 os_prio=0 tid=0x00007f94d0001000 nid=0x294a waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "DestroyJavaVM" #11 prio=5 os_prio=0 tid=0x00007f94f8008800 nid=0x291f waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "thread3" #10 prio=5 os_prio=0 tid=0x00007f94f80e7000 nid=0x292a waiting on condition [0x00007f94fd9e3000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at Demo.lambda$main$2(Demo.java:24) at Demo$$Lambda$3/1523554304.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "thread2" #9 prio=5 os_prio=0 tid=0x00007f94f80e5000 nid=0x2929 waiting on condition [0x00007f94fdae4000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at Demo.lambda$main$1(Demo.java:15) at Demo$$Lambda$2/1072591677.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "thread1" #8 prio=5 os_prio=0 tid=0x00007f94f80e3800 nid=0x2928 runnable [0x00007f94fdbe5000] java.lang.Thread.State: RUNNABLE at Demo.lambda$main$0(Demo.java:6) at Demo$$Lambda$1/640070680.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007f94f80a8800 nid=0x2926 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007f94f80a5800 nid=0x2925 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007f94f80a3000 nid=0x2924 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007f94f80a1000 nid=0x2923 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007f94f8071800 nid=0x2922 in Object.wait() [0x00007f94fe1eb000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000e3520e78> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:142) - locked <0x00000000e3520e78> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:158) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209) "Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007f94f806d800 nid=0x2921 in Object.wait() [0x00007f94fe2ec000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000e3521030> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:502) at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157) - locked <0x00000000e3521030> (a java.lang.ref.Reference$Lock) "VM Thread" os_prio=0 tid=0x00007f94f8068800 nid=0x2920 runnable "VM Periodic Task Thread" os_prio=0 tid=0x00007f94f80ad800 nid=0x2927 waiting on condition JNI global references: 152 其中thread1、thread2、thread3是我们自己创建的线程,其他都是jvm的线程 获取占用cpu最高的线程号的十六进制 printf '%x\n' 10536 2928 匹配jstack得到的线程信息 "thread1" #8 prio=5 os_prio=0 tid=0x00007f94f80e3800 nid=0x2928 runnable [0x00007f94fdbe5000] java.lang.Thread.State: RUNNABLE at Demo.lambda$main$0(Demo.java:6) at Demo$$Lambda$1/640070680.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) 最后,匹配代码的第6行,得出占用cpu过高的原因的在线程中无限循环执行导致。 2.4.2 程序执行很长时间没有结果 示例代码 public class Demo { static A a = new A(); static B b = new B(); public static void main(String[] args) throws InterruptedException { new Thread(()->{ synchronized (a) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (b) { System.out.println("我获得了 a 和 b"); } } }).start(); Thread.sleep(1000); new Thread(()->{ synchronized (b) { synchronized (a) { System.out.println("我获得了 a 和 b"); } } }).start(); } } class A { } class B { } linux运行此代码 javac Demo.java nohup java Demo & 进程号为10633,查看输出时,发现一直没有反应 根据进程号获取所有线程信息 jstack 10633 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.11-b03 mixed mode): "Attach Listener" #11 daemon prio=9 os_prio=0 tid=0x00007fc3a8001000 nid=0x299f waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "DestroyJavaVM" #10 prio=5 os_prio=0 tid=0x00007fc3d0008800 nid=0x298a waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Thread-1" #9 prio=5 os_prio=0 tid=0x00007fc3d00dd800 nid=0x2994 waiting for monitor entry [0x00007fc3ad8d2000] java.lang.Thread.State: BLOCKED (on object monitor) at Demo.lambda$main$1(Demo.java:21) - waiting to lock <0x00000000e3539580> (a A) - locked <0x00000000e3539590> (a B) at Demo$$Lambda$2/1072591677.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Thread-0" #8 prio=5 os_prio=0 tid=0x00007fc3d00db800 nid=0x2993 waiting for monitor entry [0x00007fc3ad9d3000] java.lang.Thread.State: BLOCKED (on object monitor) at Demo.lambda$main$0(Demo.java:13) - waiting to lock <0x00000000e3539590> (a B) - locked <0x00000000e3539580> (a A) at Demo$$Lambda$1/640070680.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007fc3d00a8800 nid=0x2991 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007fc3d00a5800 nid=0x2990 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007fc3d00a3000 nid=0x298f waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007fc3d00a1000 nid=0x298e runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007fc3d0071800 nid=0x298d in Object.wait() [0x00007fc3c05fc000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000e3520e78> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:142) - locked <0x00000000e3520e78> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:158) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209) "Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007fc3d006d800 nid=0x298c in Object.wait() [0x00007fc3c06fd000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000e3521030> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:502) at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157) - locked <0x00000000e3521030> (a java.lang.ref.Reference$Lock) "VM Thread" os_prio=0 tid=0x00007fc3d0068800 nid=0x298b runnable "VM Periodic Task Thread" os_prio=0 tid=0x00007fc3d00ad800 nid=0x2992 waiting on condition JNI global references: 151 Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x00007fc3b4003778 (object 0x00000000e3539580, a A), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x00007fc3b40062c8 (object 0x00000000e3539590, a B), which is held by "Thread-1" Java stack information for the threads listed above: =================================================== "Thread-1": at Demo.lambda$main$1(Demo.java:21) - waiting to lock <0x00000000e3539580> (a A) - locked <0x00000000e3539590> (a B) at Demo$$Lambda$2/1072591677.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Thread-0": at Demo.lambda$main$0(Demo.java:13) - waiting to lock <0x00000000e3539590> (a B) - locked <0x00000000e3539580> (a A) at Demo$$Lambda$1/640070680.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) Found 1 deadlock. Found one Java-level deadlock可以在底部看到这句话,意思是死锁,根据后面具体信息可知,在代码29行,Thread-1锁住对象b,在等待对象a的锁,而在代码13行,Thread-0锁住对象a,在等待对象b的锁,从而造成死锁,程序无反应。 欢迎关注公众号,后续文章更新通知,一起讨论技术问题 。

优秀的个人博客,低调大师

浅谈JVM - 内存结构(二)- 虚拟机栈

2.1 定义 Java Virtual Machine Stacks(Java虚拟机栈) Java 虚拟机栈描述的是 Java 方法执行的内存模型,用于存储栈帧,是线程私有的,生命周期随着线程启动而产生,线程结束而消亡 线程启动时会创建虚拟机栈,每个方法在执行时会在虚拟机栈中创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态连接、方法返回地址等信息。每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中的入栈(压栈)到出栈(弹栈)的过程 每个线程只能有一个活动栈帧,对应着正在执行的那个方法 问题辨析 垃圾回收是否涉及栈内存? 不涉及。栈内存无非就是一次次的方法调用产生的栈帧内存,栈帧内存在每一次方法调用后都会被弹出栈,也就是这部分内存会被自动的回收掉,所以并不需要垃圾回收来回收栈内存。 栈内存分配越大越好吗? 不是。 栈内存可以在代码运行时通过一个虚拟机参数来指定其大小 -Xss size。不指定的话,除了windows系统,默认都是1M,windows系统是依据虚拟内存大小分配。 栈内存分配的越大,只是能够进行更多次的方法递归调用,并不会增快运行的效率,反而会使得可执行的线程数变少(总内存不变,每个线程的栈内存变大,数量变少) 方法内的局部变量是否线程安全? 变量是否是线程安全的,取决于这个变量被多线程共享时,每次运行结果和单线程运行的结果是否是一样的。 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的。 如果局部变量是引用对象,且逃离了方法的作用范围,那么就需要考虑线程安全(基本数据类型不会有这个问题) 示例代码1 static void m1() { int x=0; for(int i=0;i<500;i++){ x++; } System.out.println(x); } x这个局部变量是线程安全的。每个线程对应一个栈,然后线程内每次方法调用都会产生一个新的栈帧,所以x变量处于不同线程的栈的栈帧中,互不影响,也就是线程安全的。 示例代码2 public static void m1() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static void m2(StringBuilder sb) { sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static StringBuilder m3() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); return sb; } m1方法中sb是线程安全的,因为其是线程内的局部变量。 m2方法中sb是线程不安全的,因为其是作为方法的参数传递进来,那么就有可能有其他线程能够访问到这个变量,那么这个变量就是多个线程共享的,可能造成值不一致,也就是线程不安全的。 m3方法中sb是线程不安全的,虽然其是线程中的局部变量,但是其作为返回值返回了,那么就有可能被其他线程使用,也就是多线程共享,可能造成值不一致,线程不安全。 2.2 栈帧 栈帧存在于 Java 虚拟机栈中,是 Java 虚拟机栈中的单位元素,每个线程中调用同一个方法或者不同的方法,都会创建不同的栈帧(可以简单理解为,一个线程调用一个方法创建一个栈帧),所以,调用的方法链越多,创建的栈帧越多(例如:递归)。每调用一个新的方法,被调用方法对应的栈帧就会被放到栈顶(入栈),也就是成为新的当前栈帧。当一个方法执行完成退出的时候,此方法对应的栈帧也相应销毁(出栈)。 2.2.1 局部变量表(Local 2.2.1 局部变量表(Local Variable Table) 每个栈帧中都包含一组称为局部变量表的变量列表,用于存放方法参数和方法内部定义的局部变量。在 Java 程序编译成 Class 文件时,在 Class 文件格式属性表中 Code 属性的 max_locals(局部变量表所需的存储空间,单位是 Slot) 数据项中确定了需要分配的局部变量表的最大容量。 局部变量表的容量以变量槽(Variable Slot)为最小单位,不过 Java 虚拟机规范中并没有明确规定每个 Slot 所占据的内存空间大小,只是有导向性地说明每个 Slot 都应该存放的8种类型: byte、short、int、float、char、boolean、reference(对象引用就是存到这个栈帧中的局部变量表里的,这里的引用指的是局部变量的对象引用,而不是成员变量的引用。成员变量的对象引用是存储在 Java 堆(Heap)中)、returnAddress(虚拟机数据类型,returnAddress 类型的值就是指向特定指令内存地址的指针,JVM支持多线程,每个线程有自己的程序计数器(pc register),而 pc 中的值就是当前指令所在的内存地址,即 returnAddress 类型的数据,当线程执行 native 方法时,pc 中的值为 undefined)类型的数据,这8种类型的数据,都可以使用32位或者更小的空间去存储。Java 虚拟机规范允许 Slot 的长度可以随着处理器、操作系统或者虚拟机的不同而发生变化。对于64位的数据类型,虚拟机会以高位在前的方式为其分配两个连续的 Slot 空间。即 long 和 double 两种类型。做法是将 long 和 double 类型速写分割为32位读写的做法。不过由于局部变量表建立在线程的堆栈上,是线程的私有数据,无论读写两个连续的 Slot 是否是原子操作,都不会引起数据安全问题。 Java 虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的 Slot 数量。如果是32位数据类型的数据,索引 n 就表示使用第 n 个 Slot,如果是64位数据类型的变量,则说明要使用第 n 和第 n+1 两个 Slot。 在方法执行过程中,Java 虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程。如果是实例方法(非 static 方法),那么局部变量表中的第0位索引的 Slot 默认是用来传递方法所属对象实例的引用,在方法中可以通过关键字 this 来访问这个隐含的参数。其余参数按照参数表的顺序来排列,占用从1开始的局部变量 Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的 Slot。 局部变量表中的 Slot 是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码程序计数器的值已经超过了某个变量的作用域,那么这个变量相应的 Slot 就可以交给其他变量去使用,节省栈空间,但也有可能会影响到系统的垃圾收集行为。 局部变量无初始值(实例变量和类变量都会被赋予初始值),类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予开发者定义的值。因此即使在初始化阶段开发者没有为类变量赋值也没有关系,类变量仍然具有一个确定的默认值。但局部变量就不一样了,如果一个局部变量定义了但没有赋初始值是不能使用的。 使用一段代码说明一下局部变量表: // java 代码 public int test() { int x = 0; int y = 1; return x + y; } // javac 编译后的字节码,使用 javap -v 查看 public int test(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: iconst_0 1: istore_1 2: iconst_1 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: ireturn LineNumberTable: line 7: 0 line 8: 2 line 9: 4 LocalVariableTable: Start Length Slot Name Signature 0 8 0 this Lcom/alibaba/uc/TestClass; 2 6 1 x I 4 4 2 y I 对应上面的解释说明,通过 LocalVariableTable 也可以看出来: Code 属性: stack(int x(1个栈深度)+ int y(1个栈深度))=2, locals(this(1 Slot)+ int x(1 Slot)+ int y(1 Slot))=3, args_size(非 static 方法,this 隐含参数)=1 验证 Slot 复用,运行以下代码时,在 VM 参数中添加 -verbose:gc: public void test() { { byte[] placeholder = new byte[64 * 1024 * 1024]; } int a = 0; // 当这段代码注释掉时,System.gc() 执行后,也并不会回收这64MB内存。当这段代码执行时,内存被回收了 System.gc(); } 局部变量表中的 Slot 是否还存在关于 placeholder 数组对象的引用。当 int a = 0; 不执行时,代码虽然已经离开了 placeholder 的作用域,但是后续并没有任何对局部变量表的读写操作,placeholder 原本所占用的 Slot 还没有被其他变量所复用,所以 placeholder 作为 GC Roots(所有 Java 线程当前活跃的栈帧里指向 Java 堆里的对象的引用) 仍然是可达对象。当 int a = 0; 执行时,placeholder 的 Slot 被变量 a 复用,所以 GC 触发时,placeholder 变成了不可达对象,即可被 GC 回收。 2.2.2 操作数栈(Operand Stack) 操作数栈是一个后入先出(Last In First Out)栈,方法的执行操作在操作数栈中完成,每一个字节码指令往操作数栈进行写入和提取的过程,就是入栈和出栈的过程。 同局部变量表一样,操作数栈的最大深度也是Java 程序编译成 Class 文件时被写入到 Class 文件格式属性表的 Code 属性的 max_stacks 数据项中。 操作数栈的每一个元素可以是任意的 Java 数据类型,32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2,在方法执行的任何时候,操作数栈的深度都不会超过在 max_stacks 数据项中设定的最大值(指的是进入操作数栈的 “同一批操作” 的数据类型的栈容量的和)。 当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,通过一些字节码指令从局部变量表或者对象实例字段中复制常量或者变量值到操作数栈中,也提供一些指令向操作数栈中写入和提取值,及结果入栈,也用于存放调用方法需要的参数及接受方法返回的结果。例如,整数加法的字节码指令 iadd(使用 iadd 指令时,相加的两个元素也必须是 int 型) 在运行的时候将操作数栈中最接近栈顶的两个 int 数值元素出栈相加,然后将相加结果入栈。 2.2.3 动态连接(Dynamic Linking) 每个栈帧都包含一个指向**运行时常量池(JVM 运行时数据区域)**中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。 <span style='color:red'>在 Class 文件格式的常量池(存储字面量和符号引用)中存有大量的符号引用(1.类的全限定名,2.字段名和描述符,3.方法名和描述符),字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载过程的解析阶段的时候转化为直接引用(指向目标的指针、相对偏移量或者是一个能够直接定位到目标的句柄),这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。</span> 看看以下代码的 Class 文件格式的常量池: // java 代码 public Test test() { return new Test(); } // 字节码指令 // Class文件的常量池 Constant pool: #1 = Methodref #4.#19 // java/lang/Object."<init>":()V #2 = Fieldref #3.#20 // com/alibaba/uc/Test.i:I #3 = Class #21 // com/alibaba/uc/Test #4 = Class #22 // java/lang/Object #5 = Utf8 i #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/alibaba/uc/Test; #14 = Utf8 test #15 = Utf8 ()I #16 = Utf8 <clinit> #17 = Utf8 SourceFile #18 = Utf8 Test.java #19 = NameAndType #7:#8 // "<init>":()V #20 = NameAndType #5:#6 // i:I #21 = Utf8 com/alibaba/uc/Test #22 = Utf8 java/lang/Object public int test(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: getstatic #2 // Field i:I 3: areturn LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 4 0 this Lcom/alibaba/uc/Test; 从上面字节码指令看出 0: getstatic #2 // Field i:I 这行字节码指令指向 Constant pool 中的 #2,而 #2 中指向了 #3 和 #20 为符号引用,在类加载过程的解析阶段会被转化为直接引用(指向方法区的指针)。 2.2.4 方法返回地址 当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令(例如:areturn),这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口(Normal Method Invocation Completion)。 另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用 athrow 字节码指令产生的异常,只要在本方法的异常处理器表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。 无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的程序计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。 方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整程序计数器的值以指向方法调用指令后面的一条指令等。 2.3 栈内存溢出 栈帧过多导致栈内存溢出 例如递归方法,当方法调用层级过多,产生大量的栈帧,却没有出栈,就会导致栈内存溢出,抛出StackOverflowError异常 示例代码 public class Demo { private static int count; public static void main(String[] args) { try { method1(); } catch (Throwable e) { e.printStackTrace(); System.out.println(count); } } private static void method1() { count++; method1(); } } 执行结果 java.lang.StackOverflowError at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) 14602 可以看到方法总共执行了14602次就导致了栈溢出,可以在虚拟机运行参数中设置-Xss128k调整栈内存大小,结果执行次数就会变小 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将抛出一个 OutOfMemoryError 异常 2.4 线程运行诊断 2.4.1 cpu占用过高 示例代码 public class Demo { public static void main(String[] args) { new Thread(null, () -> { System.out.println("1..."); while(true) { } }, "thread1").start(); new Thread(null, () -> { System.out.println("2..."); try { Thread.sleep(1000000L); } catch (InterruptedException e) { e.printStackTrace(); } }, "thread2").start(); new Thread(null, () -> { System.out.println("3..."); try { Thread.sleep(1000000L); } catch (InterruptedException e) { e.printStackTrace(); } }, "thread3").start(); } } linux上运行此代码 javac Demo.java nohup java Demo & 查看输出 tail -f nohup.out 1... 2... 3... 1... 2... 3... 查看cpu状况 top 可以看到刚才运行的java程序占用cpu很高,进程号为10526 查询该进程下所有线程的运行状态 top -Hp 10526 可以看到占用cpu最高的线程是10536 使用jstack工具获取10526进程中所有线程运行信息 jstack 10526 2019-12-17 17:24:00 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.11-b03 mixed mode): "Attach Listener" #12 daemon prio=9 os_prio=0 tid=0x00007f94d0001000 nid=0x294a waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "DestroyJavaVM" #11 prio=5 os_prio=0 tid=0x00007f94f8008800 nid=0x291f waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "thread3" #10 prio=5 os_prio=0 tid=0x00007f94f80e7000 nid=0x292a waiting on condition [0x00007f94fd9e3000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at Demo.lambda$main$2(Demo.java:24) at Demo$$Lambda$3/1523554304.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "thread2" #9 prio=5 os_prio=0 tid=0x00007f94f80e5000 nid=0x2929 waiting on condition [0x00007f94fdae4000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at Demo.lambda$main$1(Demo.java:15) at Demo$$Lambda$2/1072591677.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "thread1" #8 prio=5 os_prio=0 tid=0x00007f94f80e3800 nid=0x2928 runnable [0x00007f94fdbe5000] java.lang.Thread.State: RUNNABLE at Demo.lambda$main$0(Demo.java:6) at Demo$$Lambda$1/640070680.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007f94f80a8800 nid=0x2926 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007f94f80a5800 nid=0x2925 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007f94f80a3000 nid=0x2924 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007f94f80a1000 nid=0x2923 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007f94f8071800 nid=0x2922 in Object.wait() [0x00007f94fe1eb000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000e3520e78> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:142) - locked <0x00000000e3520e78> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:158) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209) "Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007f94f806d800 nid=0x2921 in Object.wait() [0x00007f94fe2ec000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000e3521030> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:502) at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157) - locked <0x00000000e3521030> (a java.lang.ref.Reference$Lock) "VM Thread" os_prio=0 tid=0x00007f94f8068800 nid=0x2920 runnable "VM Periodic Task Thread" os_prio=0 tid=0x00007f94f80ad800 nid=0x2927 waiting on condition JNI global references: 152 其中thread1、thread2、thread3是我们自己创建的线程,其他都是jvm的线程 获取占用cpu最高的线程号的十六进制 printf '%x\n' 10536 2928 匹配jstack得到的线程信息 "thread1" #8 prio=5 os_prio=0 tid=0x00007f94f80e3800 nid=0x2928 runnable [0x00007f94fdbe5000] java.lang.Thread.State: RUNNABLE at Demo.lambda$main$0(Demo.java:6) at Demo$$Lambda$1/640070680.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) 最后,匹配代码的第6行,得出占用cpu过高的原因的在线程中无限循环执行导致。 2.4.2 程序执行很长时间没有结果 示例代码 public class Demo { static A a = new A(); static B b = new B(); public static void main(String[] args) throws InterruptedException { new Thread(()->{ synchronized (a) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (b) { System.out.println("我获得了 a 和 b"); } } }).start(); Thread.sleep(1000); new Thread(()->{ synchronized (b) { synchronized (a) { System.out.println("我获得了 a 和 b"); } } }).start(); } } class A { } class B { } linux运行此代码 javac Demo.java nohup java Demo & 进程号为10633,查看输出时,发现一直没有反应 根据进程号获取所有线程信息 jstack 10633 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.11-b03 mixed mode): "Attach Listener" #11 daemon prio=9 os_prio=0 tid=0x00007fc3a8001000 nid=0x299f waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "DestroyJavaVM" #10 prio=5 os_prio=0 tid=0x00007fc3d0008800 nid=0x298a waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Thread-1" #9 prio=5 os_prio=0 tid=0x00007fc3d00dd800 nid=0x2994 waiting for monitor entry [0x00007fc3ad8d2000] java.lang.Thread.State: BLOCKED (on object monitor) at Demo.lambda$main$1(Demo.java:21) - waiting to lock <0x00000000e3539580> (a A) - locked <0x00000000e3539590> (a B) at Demo$$Lambda$2/1072591677.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Thread-0" #8 prio=5 os_prio=0 tid=0x00007fc3d00db800 nid=0x2993 waiting for monitor entry [0x00007fc3ad9d3000] java.lang.Thread.State: BLOCKED (on object monitor) at Demo.lambda$main$0(Demo.java:13) - waiting to lock <0x00000000e3539590> (a B) - locked <0x00000000e3539580> (a A) at Demo$$Lambda$1/640070680.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007fc3d00a8800 nid=0x2991 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007fc3d00a5800 nid=0x2990 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007fc3d00a3000 nid=0x298f waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007fc3d00a1000 nid=0x298e runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007fc3d0071800 nid=0x298d in Object.wait() [0x00007fc3c05fc000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000e3520e78> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:142) - locked <0x00000000e3520e78> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:158) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209) "Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007fc3d006d800 nid=0x298c in Object.wait() [0x00007fc3c06fd000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000e3521030> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:502) at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157) - locked <0x00000000e3521030> (a java.lang.ref.Reference$Lock) "VM Thread" os_prio=0 tid=0x00007fc3d0068800 nid=0x298b runnable "VM Periodic Task Thread" os_prio=0 tid=0x00007fc3d00ad800 nid=0x2992 waiting on condition JNI global references: 151 Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x00007fc3b4003778 (object 0x00000000e3539580, a A), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x00007fc3b40062c8 (object 0x00000000e3539590, a B), which is held by "Thread-1" Java stack information for the threads listed above: =================================================== "Thread-1": at Demo.lambda$main$1(Demo.java:21) - waiting to lock <0x00000000e3539580> (a A) - locked <0x00000000e3539590> (a B) at Demo$$Lambda$2/1072591677.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Thread-0": at Demo.lambda$main$0(Demo.java:13) - waiting to lock <0x00000000e3539590> (a B) - locked <0x00000000e3539580> (a A) at Demo$$Lambda$1/640070680.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) Found 1 deadlock. Found one Java-level deadlock可以在底部看到这句话,意思是死锁,根据后面具体信息可知,在代码29行,Thread-1锁住对象b,在等待对象a的锁,而在代码13行,Thread-0锁住对象a,在等待对象b的锁,从而造成死锁,程序无反应。 欢迎关注公众号,后续文章更新通知,一起讨论技术问题 。

优秀的个人博客,低调大师

Java设计模式系列-享元模式(结构型)

一、概述 享元模式:“享”就是分享之意,指一物被众人共享,而这也正是该模式的终旨所在。 享元模式有点类似于单例模式,都是只生成一个对象来被共享使用。这里有个问题,那就是对共享对象的修改,为了避免出现这种情况,我们将这些对象的公共部分,或者说是不变化的部分抽取出来形成一个对象。这个对象就可以避免到修改的问题。 享元的目的是为了减少不会要额内存消耗,将多个对同一对象的访问集中起来,不必为每个访问者创建一个单独的对象,以此来降低内存的消耗。 二、示例 下面我们来看一个简单的例子: 建筑接口:JianZhu public interface Jianzhu { void use(); } 体育馆实现类:TiYuGuan public class TiYuGuan implements Jianzhu { private String name; private String shape; private String yundong; public TiYuGuan(String yundong){ this.setYundong(yundong); } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getShape() { return shape; } public void setShape(String shape) { this.shape = shape; } public String getYundong() { return yundong; } public void setYundong(String yundong) { this.yundong = yundong; } @Override public void use() { System.out.println("该体育馆被使用来召开奥运会" + " 运动为:"+ yundong+" 形状为:"+shape+ " 名称为:"+name); } } 建筑工厂类:JianZhuFactory import java.util.*; public class JianZhuFactory { private static final Map<String,TiYuGuan> tygs = new HashMap<String,TiYuGuan>(); public static TiYuGuan getTyg(String yundong){ TiYuGuan tyg = tygs.get(yundong); if(tyg == null){ tyg = new TiYuGuan(yundong); tygs.put(yundong,tyg); } return tyg; } public static int getSize(){ return tygs.size(); } } 测试类:Clienter public class Clienter { public static void main(String[] args) { String yundong ="足球"; for(int i = 1;i <= 5;i++){ TiYuGuan tyg = JianZhuFactory.getTyg(yundong); tyg.setName("中国体育馆"); tyg.setShape("圆形"); tyg.use(); System.out.println("对象池中对象数量为:"+JianZhuFactory.getSize()); } } } 执行结果: 该体育馆被使用来召开奥运会 运动为:足球 形状为:圆形 名称为:中国体育馆 对象池中对象数量为:1 该体育馆被使用来召开奥运会 运动为:足球 形状为:圆形 名称为:中国体育馆 对象池中对象数量为:1 该体育馆被使用来召开奥运会 运动为:足球 形状为:圆形 名称为:中国体育馆 对象池中对象数量为:1 该体育馆被使用来召开奥运会 运动为:足球 形状为:圆形 名称为:中国体育馆 对象池中对象数量为:1 该体育馆被使用来召开奥运会 运动为:足球 形状为:圆形 名称为:中国体育馆 对象池中对象数量为:1 三、模式解析 如上示例中,使用工厂模式进行配合,创建对象池,测试类中的循环,你可以想象成为要举行5场比赛,每场比赛的场地就是体育馆 通过执行结果可以看出,在这个对象池(HashMap)中,一直都只有一个对象存在,第一次使用的时候创建对象,之后的每次调用都用的是那个对象,不会再重新创建。 其实在Java中就存在这种类型的实例:String。 Java中将String类定义为final(不可改变的),JVM中字符串一般保存在字符串常量池中,这个字符串常量池在jdk 6.0以前是位于常量池中,位于永久代,而在JDK 7.0中,JVM将其从永久代拿出来放置于堆中。 我们使用如下代码定义的两个字符串指向的其实是同一个字符串常量池中的字符串值。 String s1 = "abc"; String s2 = "abc"; 如果我们以s1==s2进行比较的话所得结果为:true,因为s1和s2保存的是字符串常量池中的同一个字符串地址。这就类似于我们今天所讲述的享元模式,字符串一旦定义之后就可以被共享使用,因为他们是不可改变的,同时被多处调用也不会存在任何隐患。 四、使用场景 当我们项目中创建很多对象,而且这些对象存在许多相同模块,这时,我们可以将这些相同的模块提取出来采用享元模式生成单一对象,再使用这个对象与之前的诸多对象进行配合使用,这样无疑会节省很多空间。

优秀的个人博客,低调大师

Java结构型模式(1)享元模式

2019年阿里云双11活动拼团:https://www.aliyun.com/1111/2019/group-buying-share 一、概述 享元模式:“享”就是分享之意,指一物被众人共享,而这也正是该模式的终旨所在。 享元模式有点类似于单例模式,都是只生成一个对象来被共享使用。这里有个问题,那就是对共享对象的修改,为了避免出现这种情况,我们将这些对象的公共部分,或者说是不变化的部分抽取出来形成一个对象。这个对象就可以避免到修改的问题。 享元的目的是为了减少不会要额内存消耗,将多个对同一对象的访问集中起来,不必为每个访问者创建一个单独的对象,以此来降低内存的消耗。 二、示例 下面我们来看一个简单的例子: 建筑接口:JianZhu public interface Jianzhu { void use(); } 体育馆实现类:TiYuGuan public class TiYuGuan implements Jianzhu { private String name; private String shape; private String yundong; public TiYuGuan(String yundong){ this.setYundong(yundong); } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getShape() { return shape; } public void setShape(String shape) { this.shape = shape; } public String getYundong() { return yundong; } public void setYundong(String yundong) { this.yundong = yundong; } @Override public void use() { System.out.println("该体育馆被使用来召开奥运会" + " 运动为:"+ yundong+" 形状为:"+shape+ " 名称为:"+name); } } 建筑工厂类:JianZhuFactory import java.util.*; public class JianZhuFactory { private static final Map<String,TiYuGuan> tygs = new HashMap<String,TiYuGuan>(); public static TiYuGuan getTyg(String yundong){ TiYuGuan tyg = tygs.get(yundong); if(tyg == null){ tyg = new TiYuGuan(yundong); tygs.put(yundong,tyg); } return tyg; } public static int getSize(){ return tygs.size(); } } 测试类:Clienter public class Clienter { public static void main(String[] args) { String yundong ="足球"; for(int i = 1;i <= 5;i++){ TiYuGuan tyg = JianZhuFactory.getTyg(yundong); tyg.setName("中国体育馆"); tyg.setShape("圆形"); tyg.use(); System.out.println("对象池中对象数量为:"+JianZhuFactory.getSize()); } } } 执行结果: 该体育馆被使用来召开奥运会 运动为:足球 形状为:圆形 名称为:中国体育馆 对象池中对象数量为:1 该体育馆被使用来召开奥运会 运动为:足球 形状为:圆形 名称为:中国体育馆 对象池中对象数量为:1 该体育馆被使用来召开奥运会 运动为:足球 形状为:圆形 名称为:中国体育馆 对象池中对象数量为:1 该体育馆被使用来召开奥运会 运动为:足球 形状为:圆形 名称为:中国体育馆 对象池中对象数量为:1 该体育馆被使用来召开奥运会 运动为:足球 形状为:圆形 名称为:中国体育馆 对象池中对象数量为:1 三、模式解析 如上示例中,使用工厂模式进行配合,创建对象池,测试类中的循环,你可以想象成为要举行5场比赛,每场比赛的场地就是体育馆 通过执行结果可以看出,在这个对象池(HashMap)中,一直都只有一个对象存在,第一次使用的时候创建对象,之后的每次调用都用的是那个对象,不会再重新创建。 其实在Java中就存在这种类型的实例:String。 Java中将String类定义为final(不可改变的),JVM中字符串一般保存在字符串常量池中,这个字符串常量池在jdk 6.0以前是位于常量池中,位于永久代,而在JDK 7.0中,JVM将其从永久代拿出来放置于堆中。 我们使用如下代码定义的两个字符串指向的其实是同一个字符串常量池中的字符串值。 String s1 = "abc"; String s2 = "abc"; 如果我们以s1==s2进行比较的话所得结果为:true,因为s1和s2保存的是字符串常量池中的同一个字符串地址。这就类似于我们今天所讲述的享元模式,字符串一旦定义之后就可以被共享使用,因为他们是不可改变的,同时被多处调用也不会存在任何隐患。 四、使用场景 当我们项目中创建很多对象,而且这些对象存在许多相同模块,这时,我们可以将这些相同的模块提取出来采用享元模式生成单一对象,再使用这个对象与之前的诸多对象进行配合使用,这样无疑会节省很多空间。

优秀的个人博客,低调大师

数据结构与算法之冒泡排序优化

冒泡排序优化 /** * 冒泡排序优化算法 * * @param array */ public static void bubbleSortFast(int[] array) { int len = array.length; for (int i = 0; i < len - 1; i++) { boolean change = false; for (int j = 0; j < len - i - 1; j++) { if (array[j] > array[j + 1]) { int temp = array[j]; array[j] = array[j + 1]; array[j + 1] = temp; change = true; } } if (!change) { break; } } } /** * 冒泡排序原始算法,未优化 * * @param array */ public static void bubbleSortSlow(int[] array) { int len = array.length; for (int i = 0; i < len - 1; i++) { for (int j = 0; j < len - i - 1; j++) { if (array[j] > array[j + 1]) { int temp = array[j]; array[j] = array[j + 1]; array[j + 1] = temp; } } } } 测试代码: int[] array = {-30, -40, -99, 27, 88, 66, 89, 99}; 上面数组进行排序,调用传统算法需要遍历28次,调用优化后的算法只需要遍历18次

优秀的个人博客,低调大师

数据结构与算法之八皇后问题

八皇后问题代码实现 package com.pionner.recursion; public class Queen8 { int max = 8; int[] array = new int[max]; static int i = 0; public static void main(String[] args) { Queen8 queen8 = new Queen8(); queen8.check(0); System.out.println(i); } private void print() { for (int item : array) { System.out.print(item + " "); } System.out.println(); } private boolean judge(int n) { for (int i = 0; i < n; i++) { if (array[i] == array[n] || Math.abs(n - i) == Math.abs(array[n] - array[i])) { return false; } } return true; } private void check(int n) { if (n == max) { i++; print(); return; } for (int i = 0; i < max; i++) { array[n] = i; if (judge(n)) { check(n+1); } } } }

优秀的个人博客,低调大师

数据结构(3):队列的原理和实现

完整代码拉到最底下 一、介绍 队列顾名思义就像我们生活中排队一样,先进先出。 如上图所示,25、16、5、9依次在队列中,按照顺序拿出的数据也分别是25、26、5、9。 二、实现过程及思路 底层使用数组来实现,实现的功能有插入数据到队尾、移除队首数据、查看队首数据、判断队列是否为空、判断队列是否存满。 将队列的元素存储在数组的某个区间内,队列在数组中是连续的,所以使用变量标记队列在数组中的位置。 1、编写类及属性 我们可以使用elements变量标记队列中元素的数量,使用front变量标记队首元素在数组的索引,end变量标记队尾元素在数组中的索引。 public class MyQueue { private Object[] arr;//存放队列元素的数组 private int elements;//队列元素数量 private int front;//队头元素在数组中的索引 private int end;//队尾元素在数组中的索引 public MyQueue() { arr = new Object[10]; elements = 0; front = 0; end = -1; } public MyQueue(int maxSize) { arr = new Object[maxSize]; elements = 0; front = 0; end = -1; } } 2、队列是否为空 标记队列元数量的变量 elements 为 0 即为空 public boolean isEmpty() { return elements == 0; } 3、队列是否已经满了 队列元素个数与数组的长度相等即为满 public boolean isFull() { return elements == arr.length; } 4、获取队头元素 获取数组中索引为 front的元素 public Object peek() { return arr[front]; } 5、移除队首元素 每次都是移除数组中索引为 front 的元素,下一个元素就变成了队首,即front+1,队列元素个数elements-1。共有三种情况要考虑,如果队列已经空了就无须做任何操作,如果已经是最后一个元素,直接将标记位置的变量重置即可,其他情况正常操作。 public Object remove() { if (isEmpty()) { throw new RuntimeException("队列已经是空的,放心使用吧"); } Object value = arr[front++]; //如果已经是最后一个元素了,将指针重置即可 if (elements == 1) { end = -1; front = 0; elements = 0; } else { elements--; } return value; } 6、插入 我们编写一个持续可用的队列,所以要考虑到以下情况。 (1)存储队列的数组满了(队列满了),这个好理解,满了就无法向队尾加入元素了。 (2)因为队列在数组中是连续的,如果队列的元素在数组中最后,需要将元素从队首到队尾移到数组中第一位,也就是将后面的位置空出来(参考下图)。 public void insert(Object value) { //检测队列是否已经满了 if (isFull()) { throw new RuntimeException("队列内元素已达到设定长度"); } //如果后面没有空位置,将余下元素放到数组的头 if (elements > 1 && end == arr.length - 1) { int i = 0; for (; i < elements; i++, front++) { arr[i] = arr[front]; } front = 0; end = i-1; } //其他情况正常向后添加元素 arr[++end] = value; elements++; } 7、测试 public static void main(String[] args) { MyQueue queue = new MyQueue(4); queue.insert(11); queue.insert(12); queue.insert(13); queue.insert(14); queue.remove(); queue.remove(); queue.insert(16); //queue.remove(); //queue.remove(); //queue.insert(19); //queue.insert(20); queue.remove(); queue.remove(); queue.insert(21); queue.insert(22); while (!queue.isEmpty()) { System.out.println(queue.remove()); } } 三、完整代码 package com.jikedaquan.datastruct; public class MyQueue { private Object[] arr; private int elements;//队列元素数量 private int front;//队头元素在数组中的索引 private int end;//队尾元素在数组中的索引 public MyQueue() { arr = new Object[10]; elements = 0; front = 0; end = -1; } public MyQueue(int maxSize) { arr = new Object[maxSize]; elements = 0; front = 0; end = -1; } //从队尾插入 public void insert(Object value) { //检测队列是否已经满了 if (isFull()) { throw new RuntimeException("队列内元素已达到设定长度"); } //如果后面没有空位置,将余下元素放到数组的头 if (elements > 1 && end == arr.length - 1) { int i = 0; for (; i < elements; i++, front++) { arr[i] = arr[front]; } front = 0; end = i-1; } arr[++end] = value; elements++; } //删除数据,从队头删除 public Object remove() { if (isEmpty()) { throw new RuntimeException("队列已经是空的,放心使用吧"); } Object value = arr[front++]; //如果已经是最后一个元素了,将指针重置即可 if (elements == 1) { end = -1; front = 0; elements = 0; } else { elements--; } return value; } //查看数据,从队头查看 public Object peek() { return arr[front]; } //判断是否为空 public boolean isEmpty() { return elements == 0; } public boolean isFull() { return elements == arr.length; } public static void main(String[] args) { MyQueue queue = new MyQueue(4); queue.insert(11); queue.insert(12); queue.insert(13); queue.insert(14); queue.remove(); queue.remove(); queue.insert(16); // queue.remove(); // queue.remove(); // queue.insert(19); // queue.insert(20); queue.remove(); queue.remove(); queue.insert(21); queue.insert(22); while (!queue.isEmpty()) { System.out.println(queue.remove()); } } }

优秀的个人博客,低调大师

结构化大数据分析平台设计

前言 任何线上系统都离不开数据,有些数据是业务系统自身需要的,例如系统的账号,密码,页面展示的内容等。有些数据是业务系统或者用户实时产生的,例如业务系统的日志,用户浏览访问的记录,系统的购买订单,支付信息,会员的个人资料等。大多数企业对内,对外有很多这样的线上系统,这些数据是驱动业务发展,决策和创新最核心的东西。让这些数据更好的支撑线上系统的是数据库和数据分析平台。数据库主要承担的是线上系统的实时数据写入和根据预定好的需求进行查询,严格说就是数据库中的OLTP类型数据库。这类数据库中最为大家所熟知的就是Oracle和MySQL。业务系统对数据库的要求可能是多样的,近些年也由传统的关系型数据库融入了NoSQL数据库和NewSQL。业务系统中除了和业务直接相关的数据存储在数据库中并累积起来外,还有海量的系统监控数据,系统业务日志产生。

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册