请选择 进入手机版 | 继续访问电脑版
MSIPO技术圈 首页 IT技术 查看内容

JVM学习笔记(二)

2023-07-13

学习黑马视频:01_什么是jvm_哔哩哔哩_bilibili

一些文章:

一、JVM内存结构

程序计数器
虚拟机栈
本地方法栈

方法区

程序计数器、栈、本地方法栈,都是线程私有的。堆、方法区是线程共享的区域。

下图来源:https://www.cnblogs.com/lanqingzhou/p/12374544.html 

 

1. 虚拟机栈(JVM Stacks)

1)定义

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

问题辨析
1. 垃圾回收是否涉及栈内存?
        不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。

2. 栈内存分配越大越好吗?
        不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。

3. 方法内的局部变量是否线程安全

  • 如果方法内部的变量没有逃离方法的作用访问,它是线程安全的;(如果是对象,才需要考虑此问题;如果是基本类型变量,是可以保证它是线程安全的)
  • 如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。

总之,如果变量是线程私有的,就不用考虑线程安全问题;如果是共享的,如加了static之后,就需要考虑线程安全问题。

public class main1 {
    public static void main(String[] args) {

    }
    //下面各个方法会不会造成线程安全问题?

    //不会
    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;
    }
    
}

2)栈内存溢出

Java.lang.stackOverflowError 栈内存溢出

导致栈内存溢出的情况:栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出
设置虚拟机栈内存大小:

package cn.itcast.jvm.t1.stack;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Arrays;
import java.util.List;

/**
 * json 数据转换
 */
public class Demo1_19 {

    public static void main(String[] args) throws JsonProcessingException {
        Dept d = new Dept();
        d.setName("Market");

        Emp e1 = new Emp();
        e1.setName("zhang");
        e1.setDept(d);

        Emp e2 = new Emp();
        e2.setName("li");
        e2.setDept(d);

        d.setEmps(Arrays.asList(e1, e2));

        // { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
        ObjectMapper mapper = new ObjectMapper();
        System.out.println(mapper.writeValueAsString(d));
    }
}

class Emp {
    private String name;
    
    //使用该注解避免循环调用问题,转换时忽略这个属性————只通过部门去关联员工,员工不再关联部门了
    @JsonIgnore
    private Dept dept;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Dept getDept() {
        return dept;
    }

    public void setDept(Dept dept) {
        this.dept = dept;
    }
}
class Dept {
    private String name;
    private List<Emp> emps;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Emp> getEmps() {
        return emps;
    }

    public void setEmps(List<Emp> emps) {
        this.emps = emps;
    }
}

3) 线程运行诊断

案例1:CPU占用过高

Linux环境下运行某些程序的时候,可能导致CPU的占用过高,这时需要定位占用CPU过高的线程。

第一步:用top命令定位哪个进程对cpu的占用过高;

通过top命令可以看到,PID为32655的进程编号占了CPU的99.7%,那如何进一步定位问题呢?

第二步:用ps命令进一步定位是哪个线程引起的cpu占用过高;(如下图,线程32665有问题)

ps H -eo pid,tid,%cpu

ps H -eo pid,tid,%cpu | grep 进程id ,刚才通过top查到的进程号

通过下面这个命令发现占用CPU过高的线程编号为32665,该线程编号为十进制的,换算为十六进制为7f99;

第三步:jstack 进程id,可以根据线程id找到有问题的线程,进一步定位问题代码的源码行号 

案例2:程序运行很长时间没有结果

2. 本地方法栈(Native Method Stacks)

定义:实际上就是在java虚拟机调用一些本地方法时,需要给这些本地方法提供的内存空间。本地方法运行时,使用的内存就叫本地方法栈。

作用:给本地方法的运行提供内存空间。

本地方法:指那些不是由java代码编写的方法,因为java代码是有一定限制的,有的时候它不能直接与操作系统底层打交道,所以就需要用C或者C++语言编写的本地方法来真正与操作系统打交道。java代码可以间接的通过本地方法来调用到底层的一些功能。

这样的方法多吗?当然!不管是在一些java类库,还是在执行引擎,它们都会去调用这些本地方法。比如,在Object类的方法中,clone()方法是带有native的,这种native方法它是没有方法实现的,方法实现都是通过c或者c++语言编写的,java代码通过间接的去调用c或者c++的方法实现。

3.  堆(Heap)

前面讲的程序计数器、栈、本地方法栈,都是线程私有的。堆、方法区是线程共享的区域。

1)定义

        通过new关键字创建的对象,都会使用堆内存。

2)特点

  • 堆中的对象是线程共享的,堆中的对象一般都需要考虑线程安全问题(有例外);(前面说的虚拟机栈中的局部变量只要不逃逸出方法的作用范围,都是线程私有的,都是线程安全的);
  • 垃圾回收机制;(Heap中不再被引用的对象,就会被当作垃圾进行回收,以释放堆内存)。

3)堆内存溢出

堆空间大小设置:使用-Xmx参数

         所以,这里需要注意,当内存足够大时不太容易暴露内存溢出的问题,随着时间的累积有可能会导致内存溢出。但是,有可能你运行了很短的一段时间发现它没问题。所以,排查这种堆内存问题,最好把运行内存设的稍微小一些,这样会比较早的暴露堆内存溢出的问题。

4)堆内存诊断

1. jps 工具
        查看当前系统中有哪些 java 进程
2. jmap 工具
        查看堆内存占用情况 jmap - heap 进程id

 注意:它只能查询某个时刻堆内存的使用情况。如果想连续监测,需要使用下面这个工具。

 3. jconsole 工具
        图形界面的,多功能的监测工具,可以连续监测

除了监测堆内存,还可以监测CPU,线程。 

演示上面几个工具的使用:

3.1 演示jmap工具的使用

jmap -heap 进程id       ——检查该进程堆内存的占用情况;

 

 3.2 jconsole 工具的使用

5)案例:垃圾回收后,内存占用仍然很高

jvisualvm可视化工具 (视频21)

 

4. 方法区

官网地址:Chapter 2. The Structure of the Java Virtual Machine

 1)定义

        对于 HotSpotJVM 而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。

        Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域。方法区域类似于用于传统语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。它存储每个类的结构信息,例如运行时常量池成员变量方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口初始化。

        方法区域是在虚拟机启动时创建的。尽管方法区域在逻辑上是堆的一部分,但简单的实现可能会选择不进行垃圾收集或压缩。此规范不强制指定方法区的位置或用于管理已编译代码的策略。方法区域可以具有固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,则可以收缩。方法区域的内存不需要是连续的。

Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code.

可以这样理解:“方法区”在逻辑上是堆的一部分。虽然在概念上定义了“方法区”,但是不同的JVM厂商去实现时,不一定会去遵从JVM逻辑上的定义。规范不强制方法区的位置,比如Oracle的HotSpot虚拟机在JDK1.8以前,它的实现叫做永久代,永久代就是使用了堆的一部分作为方法区;但JDK1.8以后,它把永久代移除了,换了一种实现,这种实现就元空间。元空间用的就不是堆内存,而是本地内存,即操作系统的内存。所以,不同的实现对于方法区所在位置的选择就会有所不同。如果网络上有人提到永久代,它只是HotSpot JDK1.8以前的一个实现而已,方法区是规范,永久代和元空间都只是一种实现而已。

永久区和元空间的区别,参考:https://blog.csdn.net/m0_53284765/article/details/131693910?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22131693910%22%2C%22source%22%3A%22m0_53284765%22%7Dicon-default.png?t=N658https://blog.csdn.net/m0_53284765/article/details/131693910?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22131693910%22%2C%22source%22%3A%22m0_53284765%22%7D

 

相关阅读

热门文章

    手机版|MSIPO技术圈 皖ICP备19022944号-2

    Copyright © 2024, msipo.com

    返回顶部