Here's how I do it. I found out how by referring to The Java Native Interface by Sheng Liang; see page 96. I have a class called ThreadEnv that owns a JNIEnv pointer for the current thread and creates it when it's first needed:
class ThreadEnv
    {
    public:
    JNIEnv* GetEnv()
        {
        if (m_env == nullptr)
#if defined(ANDROID) || defined(__ANDROID__)
            TheJvm->AttachCurrentThread(&m_env,nullptr);
#else
            TheJvm->AttachCurrentThread((void**)&m_env,nullptr);
#endif
        return m_env;
        }
    ~ThreadEnv()
        {
        if (m_env)
            TheJvm->DetachCurrentThread();
        }
    private:
    JNIEnv* m_env = nullptr;
    };
I then use it by making a ThreadEnv object a member of any C++ class that needs it in my JNI code, and calling GetEnv to get the JNIEnv pointer. Here's an example of how I use it in one of my classes: take a look at the OnChange member function.
class MyFrameworkObserver: public MFrameworkObserver, public MUserData
    {
    public:
    MyFrameworkObserver(jobject aFrameworkObject): m_framework_object(aFrameworkObject) { }
    ~MyFrameworkObserver()
        {
        JNIEnv* env = m_thread_env.GetEnv();
        if (env)
            env->DeleteGlobalRef(m_framework_object);
        }
    private:
    void OnViewChange() override { OnChange(TheFrameworkOnViewChangeMethodId); }
    void OnMainDataChange() override { OnChange(TheFrameworkOnMainDataChangeMethodId); }
    void OnDynamicDataChange() override { OnChange(TheFrameworkOnDynamicDataChangeMethodId); }
    void OnChange(jmethodID aMethodID)
        {
        JNIEnv* env = m_thread_env.GetEnv();
        if (env)
            env->CallVoidMethod(m_framework_object,aMethodID);
        }
    jobject m_framework_object;
    ThreadEnv m_thread_env;
    };